sdess09 пре 2 година
родитељ
комит
b5a4bfdb89

+ 2 - 0
api/types/project.go

@@ -14,6 +14,7 @@ type Project struct {
 	AzureEnabled           bool    `json:"azure_enabled"`
 	AzureEnabled           bool    `json:"azure_enabled"`
 	HelmValuesEnabled      bool    `json:"helm_values_enabled"`
 	HelmValuesEnabled      bool    `json:"helm_values_enabled"`
 	MultiCluster           bool    `json:"multi_cluster"`
 	MultiCluster           bool    `json:"multi_cluster"`
+	FullAddOns             bool    `json:"full_add_ons"`
 	EnableReprovision      bool    `json:"enable_reprovision"`
 	EnableReprovision      bool    `json:"enable_reprovision"`
 	ValidateApplyV2        bool    `json:"validate_apply_v2"`
 	ValidateApplyV2        bool    `json:"validate_apply_v2"`
 }
 }
@@ -28,6 +29,7 @@ type FeatureFlags struct {
 	AzureEnabled               bool   `json:"azure_enabled,omitempty"`
 	AzureEnabled               bool   `json:"azure_enabled,omitempty"`
 	HelmValuesEnabled          bool   `json:"helm_values_enabled,omitempty"`
 	HelmValuesEnabled          bool   `json:"helm_values_enabled,omitempty"`
 	MultiCluster               bool   `json:"multi_cluster,omitempty"`
 	MultiCluster               bool   `json:"multi_cluster,omitempty"`
+	FullAddOns                 bool   `json:"full_add_ons,omitempty"`
 	EnableReprovision          bool   `json:"enable_reprovision,omitempty"`
 	EnableReprovision          bool   `json:"enable_reprovision,omitempty"`
 	ValidateApplyV2            bool   `json:"validate_apply_v2"`
 	ValidateApplyV2            bool   `json:"validate_apply_v2"`
 }
 }

+ 3 - 0
api/types/template.go

@@ -33,6 +33,9 @@ type PorterTemplateSimple struct {
 
 
 	// The repo URL for the template
 	// The repo URL for the template
 	RepoURL string `json:"repo_url,omitempty"`
 	RepoURL string `json:"repo_url,omitempty"`
+
+	//
+	Tags []string `json:"tags,omitempty"`
 }
 }
 
 
 // ListTemplatesResponse is how a chart gets displayed when listed
 // ListTemplatesResponse is how a chart gets displayed when listed

+ 9 - 0
dashboard/src/assets/fire.svg

@@ -0,0 +1,9 @@
+<svg width="18" height="22" viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <defs>
+        <linearGradient id="Gradient" x1="0" x2="0" y1="0" y2="1">
+            <stop offset="0%" stop-color="red"/>
+            <stop offset="100%" stop-color="#f78600"/>
+        </linearGradient>
+    </defs>
+    <path d="M9.00004 20.5999C5.02999 20.5999 1.79999 17.5578 1.79999 13.8186C1.79999 8.5999 9.00009 1.3999 9.00009 1.3999C9.00009 1.3999 16.2 8.5999 16.2 13.8186C16.2 17.5579 12.9701 20.5999 9.00004 20.5999ZM9.00004 20.5999C7.01502 20.5999 5.39999 19.0789 5.39999 17.2093C5.39999 14.5999 9.00004 10.9999 9.00004 10.9999C9.00004 10.9999 12.6 14.5999 12.6 17.2093C12.6 19.0789 10.9851 20.5999 9.00004 20.5999Z" stroke="url(#Gradient)" stroke-width="2" stroke-linejoin="round"/>
+</svg>

+ 129 - 99
dashboard/src/main/home/add-on-dashboard/AddOnDashboard.tsx

@@ -1,9 +1,9 @@
-import React, { 
-  useEffect, 
-  useState, 
-  useContext, 
-  useMemo, 
-  useCallback 
+import React, {
+  useEffect,
+  useState,
+  useContext,
+  useMemo,
+  useCallback
 } from "react";
 } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import _ from "lodash";
 import _ from "lodash";
@@ -56,8 +56,9 @@ const templateBlacklist = [
   "umbrella",
   "umbrella",
 ];
 ];
 
 
-const AppDashboard: React.FC<Props> = ({
+const AddOnDashboard: React.FC<Props> = ({
 }) => {
 }) => {
+
   const { currentProject, currentCluster } = useContext(Context);
   const { currentProject, currentCluster } = useContext(Context);
   const [addOns, setAddOns] = useState([]);
   const [addOns, setAddOns] = useState([]);
   const [searchValue, setSearchValue] = useState("");
   const [searchValue, setSearchValue] = useState("");
@@ -67,7 +68,7 @@ const AppDashboard: React.FC<Props> = ({
   const filteredAddOns = useMemo(() => {
   const filteredAddOns = useMemo(() => {
     const filtered = addOns.filter((app: any) => {
     const filtered = addOns.filter((app: any) => {
       return (
       return (
-        !namespaceBlacklist.includes(app.namespace) && 
+        !namespaceBlacklist.includes(app.namespace) &&
         !templateBlacklist.includes(app.chart.metadata.name)
         !templateBlacklist.includes(app.chart.metadata.name)
       );
       );
     });
     });
@@ -153,100 +154,122 @@ const AppDashboard: React.FC<Props> = ({
       {currentCluster?.status === "UPDATING_UNAVAILABLE" ? (
       {currentCluster?.status === "UPDATING_UNAVAILABLE" ? (
         <ClusterProvisioningPlaceholder />
         <ClusterProvisioningPlaceholder />
       ) : (
       ) : (
-        <>
-          <Container row spaced>
-            <SearchBar 
-              value={searchValue}
-              setValue={setSearchValue}
-              placeholder="Search add-ons . . ."
-              width="100%"
-            />
-            <Spacer inline x={2} />
-            <Toggle
-              items={[
-                { label: <ToggleIcon src={grid} />, value: "grid" },
-                { label: <ToggleIcon src={list} />, value: "list" },
-              ]}
-              active={view}
-              setActive={setView}
-            />
-            <Spacer inline x={2} />
-            <Link to="/addons/new">
-              <Button onClick={() => {}} height="30px" width="130px">
-                <I className="material-icons">add</I> New add-on
-              </Button>
-            </Link>
-          </Container>
-          <Spacer y={1} />
-          {(!isLoading && filteredAddOns.length === 0) && (
-            <Fieldset>
-              <Container row>
-                <PlaceholderIcon src={notFound} />
-                <Text color="helper">No add-ons were found.</Text>
-              </Container>
-            </Fieldset>
-          )}
-          {isLoading ? <Loading offset="-150px" /> : view === "grid" ? (
-            <GridList>
-              {(filteredAddOns ?? []).map((app: any, i: number) => {
-                return (
-                  <Block to={getExpandedChartLinkURL(app)} key={i}>
-                    <Container row>
-                      <Icon 
-                        src={
-                          hardcodedIcons[app.chart.metadata.name] ||
-                          app.chart.metadata.icon
-                        }
-                      />
-                      <Text size={14}>{app.name}</Text>
-                    </Container>
-                    <StatusIcon src={healthy} />
-                    <Container row>
-                      <SmallIcon opacity="0.4" src={time} />
-                      <Text size={13} color="#ffffff44">
-                        {readableDate(app.info.last_deployed)}
-                      </Text>
-                    </Container>
-                  </Block>
-                );
-              })}
-          </GridList>
-          ) : (
-            <List>
-              {(filteredAddOns ?? []).map((app: any, i: number) => {
-                return (
-                  <Row to={getExpandedChartLinkURL(app)} key={i}>
-                    <Container row>
-                      <MidIcon
-                        src={
-                          hardcodedIcons[app.chart.metadata.name] ||
-                          app.chart.metadata.icon
-                        }
-                      />
-                      <Text size={14}>{app.name}</Text>
-                      <Spacer inline x={1} />
-                      <MidIcon src={healthy} height="16px" />
-                    </Container>
-                    <Spacer height="15px" />
-                    <Container row>
-                      <SmallIcon opacity="0.4" src={time} />
-                      <Text size={13} color="#ffffff44">
-                        {readableDate(app.info.last_deployed)}
-                      </Text>
-                    </Container>
-                  </Row>
-                );
-              })}
-            </List>
-          )}
-        </>
-      )}
+
+
+        (filteredAddOns.length === 0) ? (
+
+          isLoading ?
+            (<Loading offset="-150px" />) : (
+              <Fieldset>
+
+                <CentralContainer>
+                  <Text size={16}>
+                    No add-ons have been deployed yet.
+                  </Text>
+                  <Spacer y={1} />
+
+                  <Text color={"helper"}>
+                    Deploy from our suite of curated add-ons.
+                  </Text>
+                  <Spacer y={.5} />
+                  <Link to="/addons/new">
+                    <Button onClick={() => { }} height="40px" >
+                      Deploy add-ons <Spacer inline x={1} /> <i className="material-icons">east</i>
+                    </Button>
+                  </Link>
+                </CentralContainer>
+
+
+              </Fieldset >
+            )
+        ) : (
+          <>
+            <Container row spaced>
+              <SearchBar
+                value={searchValue}
+                setValue={setSearchValue}
+                placeholder="Search add-ons . . ."
+                width="100%"
+              />
+              <Spacer inline x={2} />
+              <Toggle
+                items={[
+                  { label: <ToggleIcon src={grid} />, value: "grid" },
+                  { label: <ToggleIcon src={list} />, value: "list" },
+                ]}
+                active={view}
+                setActive={setView}
+              />
+              <Spacer inline x={2} />
+              <Link to="/addons/new">
+                <Button onClick={() => { }} height="30px" width="130px">
+                  <I className="material-icons">add</I> New add-on
+                </Button>
+              </Link>
+            </Container>
+            <Spacer y={1} />
+
+            {isLoading ? <Loading offset="-150px" /> : view === "grid" ? (
+              <GridList>
+                {(filteredAddOns ?? []).map((app: any, i: number) => {
+                  return (
+                    <Block to={getExpandedChartLinkURL(app)} key={i}>
+                      <Container row>
+                        <Icon
+                          src={
+                            hardcodedIcons[app.chart.metadata.name] ||
+                            app.chart.metadata.icon
+                          }
+                        />
+                        <Text size={14}>{app.name}</Text>
+                      </Container>
+                      <StatusIcon src={healthy} />
+                      <Container row>
+                        <SmallIcon opacity="0.4" src={time} />
+                        <Text size={13} color="#ffffff44">
+                          {readableDate(app.info.last_deployed)}
+                        </Text>
+                      </Container>
+                    </Block>
+                  );
+                })}
+              </GridList>
+            ) : (
+              <List>
+                {(filteredAddOns ?? []).map((app: any, i: number) => {
+                  return (
+                    <Row to={getExpandedChartLinkURL(app)} key={i}>
+                      <Container row>
+                        <MidIcon
+                          src={
+                            hardcodedIcons[app.chart.metadata.name] ||
+                            app.chart.metadata.icon
+                          }
+                        />
+                        <Text size={14}>{app.name}</Text>
+                        <Spacer inline x={1} />
+                        <MidIcon src={healthy} height="16px" />
+                      </Container>
+                      <Spacer height="15px" />
+                      <Container row>
+                        <SmallIcon opacity="0.4" src={time} />
+                        <Text size={13} color="#ffffff44">
+                          {readableDate(app.info.last_deployed)}
+                        </Text>
+                      </Container>
+                    </Row>
+                  );
+                })}
+              </List>
+            )}
+          </>
+        ))}
       <Spacer y={5} />
       <Spacer y={5} />
-    </StyledAppDashboard>
+    </StyledAppDashboard >
   );
   );
 };
 };
 
 
-export default AppDashboard;
+export default AddOnDashboard;
 
 
 const PlaceholderIcon = styled.img`
 const PlaceholderIcon = styled.img`
   height: 13px;
   height: 13px;
@@ -254,7 +277,7 @@ const PlaceholderIcon = styled.img`
   opacity: 0.65;
   opacity: 0.65;
 `;
 `;
 
 
-const Row = styled(Link)<{ isAtBottom?: boolean }>`
+const Row = styled(Link) <{ isAtBottom?: boolean }>`
   cursor: pointer;
   cursor: pointer;
   display: block;
   display: block;
   padding: 15px;
   padding: 15px;
@@ -347,4 +370,11 @@ const I = styled.i`
 const StyledAppDashboard = styled.div`
 const StyledAppDashboard = styled.div`
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
+`;
+
+const CentralContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: left;
+  align-items: left;   
 `;
 `;

+ 84 - 54
dashboard/src/main/home/add-on-dashboard/ExpandedTemplate.tsx

@@ -1,7 +1,6 @@
 import React, { useEffect, useState, useContext, useMemo } from "react";
 import React, { useEffect, useState, useContext, useMemo } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import _ from "lodash";
 import _ from "lodash";
-
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import api from "shared/api";
 import api from "shared/api";
 
 
@@ -12,7 +11,8 @@ import Container from "components/porter/Container";
 import Text from "components/porter/Text";
 import Text from "components/porter/Text";
 import Markdown from "markdown-to-jsx";
 import Markdown from "markdown-to-jsx";
 
 
-import { hardcodedNames, hardcodedIcons } from "shared/hardcodedNameDict";
+import { hardcodedNames, hardcodedIcons, DISPLAY_TAGS_MAP } from "shared/hardcodedNameDict";
+import Icon from "components/porter/Icon";
 
 
 type Props = {
 type Props = {
   currentTemplate: any;
   currentTemplate: any;
@@ -31,7 +31,6 @@ const ExpandedTemplate: React.FC<Props> = ({
   const [values, setValues] = useState("");
   const [values, setValues] = useState("");
   const [markdown, setMarkdown] = useState<any>(null);
   const [markdown, setMarkdown] = useState<any>(null);
   const [keywords, setKeywords] = useState<any[]>([]);
   const [keywords, setKeywords] = useState<any[]>([]);
-
   const getTemplateInfo = async () => {
   const getTemplateInfo = async () => {
     setIsLoading(true);
     setIsLoading(true);
     let params = {
     let params = {
@@ -65,27 +64,41 @@ const ExpandedTemplate: React.FC<Props> = ({
     <StyledExpandedTemplate>
     <StyledExpandedTemplate>
       <Container row spaced>
       <Container row spaced>
         <Container row>
         <Container row>
-          <Button 
-            onClick={goBack}
-            alt
-          >
-            <I className="material-icons">first_page</I>
-            <Spacer inline x={1} />
-            Select template
-          </Button>
-          <Spacer x={1} inline />
-          <Icon src={hardcodedIcons[currentTemplate.name] || currentTemplate.icon} />
-          <Text size={16}>
-            <Capitalize>
-              {hardcodedNames[currentTemplate.name] || currentTemplate.name}
-            </Capitalize>
-          </Text>
+          <TitleIcon src={hardcodedIcons[currentTemplate.name] || currentTemplate.icon} />
+
+          <TitleContainer >
+            <Container row spaced>
+              <Text size={20}>
+
+                <Capitalize>
+                  {hardcodedNames[currentTemplate.name] || currentTemplate.name}
+                </Capitalize>
+              </Text>
+              {Object.keys(DISPLAY_TAGS_MAP).map(tagKey => (
+                currentTemplate.tags?.includes(tagKey) &&
+                <Tag
+                  style={{ background: DISPLAY_TAGS_MAP[tagKey].color }}
+                >
+                  {DISPLAY_TAGS_MAP[tagKey].label}
+                </Tag>))}
+            </Container>
+            <Text color={"helper"} size={10}>
+              {currentTemplate.description}
+            </Text>
+          </TitleContainer>
+
         </Container>
         </Container>
+
         <Button onClick={() => proceed(form)}>
         <Button onClick={() => proceed(form)}>
           <AddI className="material-icons">add</AddI>
           <AddI className="material-icons">add</AddI>
           Deploy add-on
           Deploy add-on
         </Button>
         </Button>
       </Container>
       </Container>
+      {currentTemplate.tags?.includes("DATA_STORE") && <><Spacer y={1} />
+        <i className="material-icons" style={{ marginTop: '2px', marginRight: '2px', fontSize: '12px', color: '#fcba03' }}>error_outline</i>
+        <Text color={"#d6b43e"} >For development use only. Does not support persistance and should not be used in production grade applications</Text>
+      </>}
+
       <Spacer height="15px" />
       <Spacer height="15px" />
       {
       {
         isLoading ? <Loading offset="-150px" /> : (
         isLoading ? <Loading offset="-150px" /> : (
@@ -101,60 +114,77 @@ const ExpandedTemplate: React.FC<Props> = ({
           )
           )
         )
         )
       }
       }
-    </StyledExpandedTemplate>
+    </StyledExpandedTemplate >
   );
   );
 };
 };
 
 
 export default ExpandedTemplate;
 export default ExpandedTemplate;
 
 
 const MarkdownWrapper = styled.div`
 const MarkdownWrapper = styled.div`
-  font-size: 13px;
-  line-height: 1.5;
-  color: #aaaabb;
+        font-size: 13px;
+        line-height: 1.5;
+        color: #aaaabb;
   > div {
   > div {
     > h1 {
     > h1 {
-      color: ${({ theme }) => theme.text.primary};
-      font-size: 16px;
-      font-weight: 400;
+          color: ${({ theme }) => theme.text.primary};
+        font-size: 16px;
+        font-weight: 400;
     }
     }
     > h2 {
     > h2 {
-      color: ${({ theme }) => theme.text.primary};
-      font-size: 16px;
-      font-weight: 400;
+          color: ${({ theme }) => theme.text.primary};
+        font-size: 16px;
+        font-weight: 400;
     }
     }
     > h3 {
     > h3 {
-      color: ${({ theme }) => theme.text.primary};
-      font-size: 16px;
-      font-weight: 400;
+          color: ${({ theme }) => theme.text.primary};
+        font-size: 16px;
+        font-weight: 400;
     }
     }
   }
   }
-  padding-bottom: 80px;
-`;
-
-const Icon = styled.img`
-  height: 22px;
-  margin-right: 15px;
-`;
+        padding-bottom: 80px;
+        `;
+
+const TitleIcon = styled.img`
+        height: 46px;
+        margin-right: 15px;
+        border-radius: 10px;
+        background: #272727;
+        padding: 8px;
+        box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
+        `;
 
 
 const Capitalize = styled.span`
 const Capitalize = styled.span`
-  text-transform: capitalize;
-`;
+        text-transform: capitalize;
+        `;
 
 
-const I = styled.i`
-  color: white;
-  font-size: 16px;
-`;
 
 
 const AddI = styled.i`
 const AddI = styled.i`
-  color: white;
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  margin-right: 10px;
-  justify-content: center;
-`;
+        color: white;
+        font-size: 14px;
+        display: flex;
+        align-items: center;
+        margin-right: 10px;
+        justify-content: center;
+        `;
 
 
 const StyledExpandedTemplate = styled.div`
 const StyledExpandedTemplate = styled.div`
-  width: 100%;
-  height: 100%;
-`;
+        width: 100%;
+        height: 100%;
+        `;
+
+const Tag = styled.div<{ size?: string, right?: string, bottom?: string, left?: string }>`
+          margin-left: 15px;
+          font-size: 10px;
+          background: #480ca8;
+          padding: 5px;
+          border-radius: 4px;
+          opacity: 0.85;
+          `;
+
+const TitleContainer = styled.div`
+          display: flex;
+          flex-direction: column;
+          align-items: start;
+          justify-content: center;
+          max-width: 500px;
+        `;

+ 121 - 33
dashboard/src/main/home/add-on-dashboard/NewAddOnFlow.tsx

@@ -21,22 +21,30 @@ import Back from "components/porter/Back";
 import Fieldset from "components/porter/Fieldset";
 import Fieldset from "components/porter/Fieldset";
 import Text from "components/porter/Text";
 import Text from "components/porter/Text";
 import Container from "components/porter/Container";
 import Container from "components/porter/Container";
+import Select from "components/porter/Select";
 
 
 type Props = {
 type Props = {
 };
 };
 
 
-const HIDDEN_CHARTS = ["porter-agent", "loki"];
+const HIDDEN_CHARTS = ["porter-agent", "loki", "agent"];
+
+//For Charts that don't exist locally we need to add them in manually
+const TAG_MAPPING = {
+  "DATA_STORE": ["mysql"],
+  "DATA_BASE": ["mysql"]
+}
 
 
 const NewAddOnFlow: React.FC<Props> = ({
 const NewAddOnFlow: React.FC<Props> = ({
 }) => {
 }) => {
-  const { capabilities, currentProject, currentCluster } = useContext(Context);
+  const { capabilities, currentProject, currentCluster, user } = useContext(Context);
   const [isLoading, setIsLoading] = useState<boolean>(true);
   const [isLoading, setIsLoading] = useState<boolean>(true);
   const [searchValue, setSearchValue] = useState("");
   const [searchValue, setSearchValue] = useState("");
   const [addOnTemplates, setAddOnTemplates] = useState<any[]>([]);
   const [addOnTemplates, setAddOnTemplates] = useState<any[]>([]);
   const [currentTemplate, setCurrentTemplate] = useState<any>(null);
   const [currentTemplate, setCurrentTemplate] = useState<any>(null);
   const [currentForm, setCurrentForm] = useState<any>(null);
   const [currentForm, setCurrentForm] = useState<any>(null);
+  const [selectedTag, setSelectedTag] = useState<string | null>(null);
 
 
-  const filteredTemplates = useMemo(() => {
+  const allFilteredTemplates = useMemo(() => {
     const filteredBySearch = search(
     const filteredBySearch = search(
       addOnTemplates ?? [],
       addOnTemplates ?? [],
       searchValue,
       searchValue,
@@ -49,6 +57,22 @@ const NewAddOnFlow: React.FC<Props> = ({
     return _.sortBy(filteredBySearch);
     return _.sortBy(filteredBySearch);
   }, [addOnTemplates, searchValue]);
   }, [addOnTemplates, searchValue]);
 
 
+  const appTemplates = useMemo(() => {
+    return allFilteredTemplates.filter(template =>
+      template.tags?.includes("APP"));
+  }, [allFilteredTemplates]);
+
+  const dataStoreTemplates = useMemo(() => {
+    return allFilteredTemplates.filter(template => template.tags?.includes("DATA_STORE"));
+  }, [allFilteredTemplates]);
+
+  const filteredTemplates = useMemo(() => {
+    return _.differenceBy(
+      allFilteredTemplates,
+      [...appTemplates, ...dataStoreTemplates]
+    );
+  }, [allFilteredTemplates, appTemplates, dataStoreTemplates]);
+
   const getTemplates = async () => {
   const getTemplates = async () => {
     setIsLoading(true);
     setIsLoading(true);
     const default_addon_helm_repo_url = capabilities?.default_addon_helm_repo_url;
     const default_addon_helm_repo_url = capabilities?.default_addon_helm_repo_url;
@@ -76,12 +100,26 @@ const NewAddOnFlow: React.FC<Props> = ({
       sortedVersionData = sortedVersionData.filter(
       sortedVersionData = sortedVersionData.filter(
         (template: any) => !HIDDEN_CHARTS.includes(template?.name)
         (template: any) => !HIDDEN_CHARTS.includes(template?.name)
       );
       );
+
+      sortedVersionData = sortedVersionData.map((template: any) => {
+        let testTemplate: string[] = template?.tags || []
+        console.log(testTemplate)
+        // Assign tags based on TAG_MAPPING
+        for (let tag in TAG_MAPPING) {
+          if (TAG_MAPPING[tag].includes(template.name)) {
+            testTemplate?.push(tag);
+          }
+        }
+
+        return { ...template, tags: testTemplate };
+      });
       setAddOnTemplates(sortedVersionData);
       setAddOnTemplates(sortedVersionData);
     } catch (error) {
     } catch (error) {
       setIsLoading(false);
       setIsLoading(false);
     }
     }
   };
   };
 
 
+
   useEffect(() => {
   useEffect(() => {
     getTemplates();
     getTemplates();
   }, [currentProject, currentCluster]);
   }, [currentProject, currentCluster]);
@@ -115,15 +153,25 @@ const NewAddOnFlow: React.FC<Props> = ({
                 />
                 />
               ) : (
               ) : (
                 <>
                 <>
-                  <SearchBar
-                    value={searchValue}
-                    setValue={setSearchValue}
-                    placeholder="Search available add-ons . . ."
-                    width="100%"
-                  />
+                  <Container row>
+                    <SearchBar
+                      value={searchValue}
+                      setValue={setSearchValue}
+                      placeholder="Search available add-ons . . ."
+                      width="100%"
+                    />
+                    <Spacer inline x={1} />
+                    {/* <Select
+                      width={"150px"}
+                      options={[
+                        { label: "Filter...", value: "" },
+                        { label: "Worker", value: "worker" },
+                        { label: "Cron Job", value: "job" },]}
+                      height={"25px"} /> */}
+                  </Container>
                   <Spacer y={1} />
                   <Spacer y={1} />
 
 
-                  {filteredTemplates.length === 0 && (
+                  {allFilteredTemplates.length === 0 && (
                     <Fieldset>
                     <Fieldset>
                       <Container row>
                       <Container row>
                         <PlaceholderIcon src={notFound} />
                         <PlaceholderIcon src={notFound} />
@@ -134,10 +182,50 @@ const NewAddOnFlow: React.FC<Props> = ({
                   {isLoading ? <Loading offset="-150px" /> : (
                   {isLoading ? <Loading offset="-150px" /> : (
                     <>
                     <>
                       <DarkMatter />
                       <DarkMatter />
+
+                      {appTemplates?.length > 0 &&
+                        <>
+                          <Spacer y={1.5} />
+                          <div>
+                            <Text color="#fff" size={15}>Apps and Services</Text>
+                          </div>
+                          <div>
+                            <Text color="helper">For developer productivity.</Text>
+                          </div>
+                        </>}
                       <TemplateList
                       <TemplateList
-                        templates={filteredTemplates}
+                        templates={appTemplates} // This is where you provide only APP templates
                         setCurrentTemplate={(x) => setCurrentTemplate(x)}
                         setCurrentTemplate={(x) => setCurrentTemplate(x)}
                       />
                       />
+                      {dataStoreTemplates?.length > 0 &&
+                        <>
+                          <div>
+                            <Text color="#fff" size={15}>Pre-Production Datastores</Text>
+                          </div>
+                          <div>
+                            <Text color="helper">Pre-production datastores are not highly available and use ephemeral storage.</Text>
+                          </div>
+                        </>}
+                      <TemplateList
+                        templates={dataStoreTemplates} // This is where you provide only DATA_STORE templates
+                        setCurrentTemplate={(x) => setCurrentTemplate(x)}
+                      />
+
+                      {filteredTemplates?.length > 0 && (currentProject?.full_add_ons || user.isPorterUser) &&
+                        <>
+                          <div>
+                            <Text color="#fff" size={15}>All Add-Ons</Text>
+                          </div>
+                          <div>
+                            <Text color="helper">Full list of add-ons</Text>
+                          </div>
+
+                          <TemplateList
+                            templates={filteredTemplates} // This is where you provide only DATA_STORE templates
+                            setCurrentTemplate={(x) => setCurrentTemplate(x)}
+                          />
+                        </>
+                      }
                     </>
                     </>
                   )}
                   )}
                 </>
                 </>
@@ -146,38 +234,38 @@ const NewAddOnFlow: React.FC<Props> = ({
           </>
           </>
         )
         )
       }
       }
-    </StyledTemplateComponent>
+    </StyledTemplateComponent >
   );
   );
 };
 };
 
 
 export default NewAddOnFlow;
 export default NewAddOnFlow;
 
 
 const PlaceholderIcon = styled.img`
 const PlaceholderIcon = styled.img`
-  height: 13px;
-  margin-right: 12px;
-  opacity: 0.65;
-`;
+      height: 13px;
+      margin-right: 12px;
+      opacity: 0.65;
+      `;
 
 
 const DarkMatter = styled.div`
 const DarkMatter = styled.div`
-  width: 100%;
-  margin-top: -35px;
-`;
+      width: 100%;
+      margin-top: -35px;
+      `;
 
 
 const I = styled.i`
 const I = styled.i`
-  font-size: 16px;
-  padding: 4px;
-  cursor: pointer;
-  border-radius: 50%;
-  margin-right: 15px;
-  background: ${props => props.theme.fg};
-  color: ${props => props.theme.text.primary};
-  border: 1px solid ${props => props.theme.border};
-  :hover {
-    filter: brightness(150%);
+      font-size: 16px;
+      padding: 4px;
+      cursor: pointer;
+      border-radius: 50%;
+      margin-right: 15px;
+      background: ${props => props.theme.fg};
+      color: ${props => props.theme.text.primary};
+      border: 1px solid ${props => props.theme.border};
+      :hover {
+        filter: brightness(150%);
   }
   }
-`;
+      `;
 
 
 const StyledTemplateComponent = styled.div`
 const StyledTemplateComponent = styled.div`
-  width: 100%;
-  height: 100%;
-`;
+      width: 100%;
+      height: 100%;
+      `;

+ 127 - 99
dashboard/src/main/home/app-dashboard/AppDashboard.tsx

@@ -205,106 +205,127 @@ const AppDashboard: React.FC<Props> = ({ }) => {
       {currentCluster?.status === "UPDATING_UNAVAILABLE" ? (
       {currentCluster?.status === "UPDATING_UNAVAILABLE" ? (
         <ClusterProvisioningPlaceholder />
         <ClusterProvisioningPlaceholder />
       ) : (
       ) : (
-        <>
-          <Container row spaced>
-            <SearchBar
-              value={searchValue}
-              setValue={(x) => {
-                if (x === "open_sesame") {
-                  setFeaturePreview(true);
-                }
-                setSearchValue(x);
-              }}
-              placeholder="Search applications . . ."
-              width="100%"
-            />
-            <Spacer inline x={2} />
-            <Toggle
-              items={[
-                { label: <ToggleIcon src={grid} />, value: "grid" },
-                { label: <ToggleIcon src={list} />, value: "list" },
-              ]}
-              active={view}
-              setActive={setView}
-            />
-            <Spacer inline x={2} />
-            <PorterLink to="/apps/new/app">
-              <Button onClick={async () => updateStackStartedStep()} height="30px" width="160px">
-                <I className="material-icons">add</I> New application
-              </Button>
-            </PorterLink>
-          </Container>
-          <Spacer y={1} />
-          {!isLoading && filteredApps.length === 0 && (
-            <Fieldset>
-              <Container row>
-                <PlaceholderIcon src={notFound} />
-                <Text color="helper">No applications were found.</Text>
-              </Container>
-            </Fieldset>
-          )}
-          {isLoading ? (
-            <Loading offset="-150px" />
-          ) : view === "grid" ? (
-            <GridList>
-              {(filteredApps ?? []).map((app: any, i: number) => {
-                if (!namespaceBlacklist.includes(app.name)) {
-                  return (
-                    <Link to={`/apps/${app.name}`} key={i}>
-                      <Block>
-                        <Container row>
-                          {renderIcon(app["buildpacks"])}
-                          <Spacer inline width="12px" />
-                          <Text size={14}>{app.name}</Text>
-                          <Spacer inline x={2} />
-                        </Container>
-                        <StatusIcon src={healthy} />
-                        {renderSource(app)}
-                        <Container row>
-                          <SmallIcon opacity="0.4" src={time} />
-                          <Text size={13} color="#ffffff44">{app.last_deployed}</Text>
-                        </Container>
-                      </Block>
-                    </Link>
-                  );
-                }
-              })}
-            </GridList>
-          ) : (
-            <List>
-              {(filteredApps ?? []).map((app: any, i: number) => {
-                if (!namespaceBlacklist.includes(app.name)) {
-                  return (
-                    <Link to={`/apps/${app.name}`} key={i}>
-                      <Row>
-                        <Container row>
-                          <Spacer inline width="1px" />
-                          {renderIcon(app["buildpacks"], "larger")}
-                          <Spacer inline width="12px" />
-                          <Text size={14}>
-                            {app.name}
-                          </Text>
-                          <Spacer inline x={1} />
-                          <Icon height="16px" src={healthy} />
-                        </Container>
-                        <Spacer height="15px" />
-                        <Container row>
+        filteredApps.length === 0 ? (
+          isLoading ?
+            (<Loading offset="-150px" />) : (
+              <Fieldset>
+
+                <CentralContainer>
+                  <Text size={16}>
+                    No apps have been deployed yet.
+                  </Text>
+                  <Spacer y={1} />
+
+                  <Text color={"helper"}>
+                    Get started by deploying your app
+                  </Text>
+                  <Spacer y={.5} />
+                  <Link to="/addons/new">
+                    <Button onClick={() => { }} height="40px" >
+                      Deploy app <Spacer inline x={1} /> <i className="material-icons">east</i>
+                    </Button>
+                  </Link>
+                </CentralContainer>
+
+
+              </Fieldset >
+            )
+        ) : (
+          <>
+            <Container row spaced>
+              <SearchBar
+                value={searchValue}
+                setValue={(x) => {
+                  if (x === "open_sesame") {
+                    setFeaturePreview(true);
+                  }
+                  setSearchValue(x);
+                }}
+                placeholder="Search applications . . ."
+                width="100%"
+              />
+              <Spacer inline x={2} />
+              <Toggle
+                items={[
+                  { label: <ToggleIcon src={grid} />, value: "grid" },
+                  { label: <ToggleIcon src={list} />, value: "list" },
+                ]}
+                active={view}
+                setActive={setView}
+              />
+              <Spacer inline x={2} />
+              <PorterLink to="/apps/new/app">
+                <Button onClick={async () => updateStackStartedStep()} height="30px" width="160px">
+                  <I className="material-icons">add</I> New application
+                </Button>
+              </PorterLink>
+            </Container>
+            <Spacer y={1} />
+
+            {isLoading ? (
+              <Loading offset="-150px" />
+            ) : view === "grid" ? (
+              <GridList>
+                {(filteredApps ?? []).map((app: any, i: number) => {
+                  if (!namespaceBlacklist.includes(app.name)) {
+                    return (
+                      <Link to={`/apps/${app.name}`} key={i}>
+                        <Block>
+                          <Container row>
+                            {renderIcon(app["buildpacks"])}
+                            <Spacer inline width="12px" />
+                            <Text size={14}>{app.name}</Text>
+                            <Spacer inline x={2} />
+                          </Container>
+                          <StatusIcon src={healthy} />
                           {renderSource(app)}
                           {renderSource(app)}
-                          <Spacer inline x={1} />
-                          <SmallIcon opacity="0.4" src={time} />
-                          <Text size={13} color="#ffffff44">
-                            {app.last_deployed}
-                          </Text>
-                        </Container>
-                      </Row>
-                    </Link>
-                  );
-                }
-              })}
-            </List>
-          )}
-        </>
-      )}
+                          <Container row>
+                            <SmallIcon opacity="0.4" src={time} />
+                            <Text size={13} color="#ffffff44">{app.last_deployed}</Text>
+                          </Container>
+                        </Block>
+                      </Link>
+                    );
+                  }
+                })}
+              </GridList>
+            ) : (
+              <List>
+                {(filteredApps ?? []).map((app: any, i: number) => {
+                  if (!namespaceBlacklist.includes(app.name)) {
+                    return (
+                      <Link to={`/apps/${app.name}`} key={i}>
+                        <Row>
+                          <Container row>
+                            <Spacer inline width="1px" />
+                            {renderIcon(app["buildpacks"], "larger")}
+                            <Spacer inline width="12px" />
+                            <Text size={14}>
+                              {app.name}
+                            </Text>
+                            <Spacer inline x={1} />
+                            <Icon height="16px" src={healthy} />
+                          </Container>
+                          <Spacer height="15px" />
+                          <Container row>
+                            {renderSource(app)}
+                            <Spacer inline x={1} />
+                            <SmallIcon opacity="0.4" src={time} />
+                            <Text size={13} color="#ffffff44">
+                              {app.last_deployed}
+                            </Text>
+                          </Container>
+                        </Row>
+                      </Link>
+                    );
+                  }
+                })}
+              </List>
+            )}
+          </>
+        )
+      )
+      }
       <Spacer y={5} />
       <Spacer y={5} />
     </StyledAppDashboard>
     </StyledAppDashboard>
   );
   );
@@ -403,3 +424,10 @@ const StyledAppDashboard = styled.div`
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
 `;
 `;
+
+const CentralContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: left;
+  align-items: left;   
+`;

+ 61 - 9
dashboard/src/main/home/launch/TemplateList.tsx

@@ -4,14 +4,15 @@ import api from "shared/api";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
 import Loading from "components/Loading";
 import Loading from "components/Loading";
-import { hardcodedIcons, hardcodedNames } from "shared/hardcodedNameDict";
+import { DISPLAY_TAGS_MAP, hardcodedIcons, hardcodedNames } from "shared/hardcodedNameDict";
 import { PorterTemplate } from "shared/types";
 import { PorterTemplate } from "shared/types";
 import semver from "semver";
 import semver from "semver";
 
 
 import web from "assets/web.png";
 import web from "assets/web.png";
 import worker from "assets/worker.png";
 import worker from "assets/worker.png";
 import job from "assets/job.png";
 import job from "assets/job.png";
-
+import fire from "assets/fire.svg"
+import Spacer from "components/porter/Spacer";
 type Props = {
 type Props = {
   helm_repo_id?: number;
   helm_repo_id?: number;
   templates?: PorterTemplate[];
   templates?: PorterTemplate[];
@@ -130,27 +131,78 @@ const TemplateList: React.FC<Props> = ({
   return (
   return (
     <TemplateListWrapper>
     <TemplateListWrapper>
       {(templates || templateList)?.map((template: PorterTemplate) => {
       {(templates || templateList)?.map((template: PorterTemplate) => {
-        let { name, icon, description } = template;
+        let { name, icon, description, tags } = template;
         if (hardcodedNames[name]) {
         if (hardcodedNames[name]) {
           name = hardcodedNames[name];
           name = hardcodedNames[name];
         }
         }
+
         return (
         return (
           <TemplateBlock
           <TemplateBlock
             key={name}
             key={name}
-            onClick={() => setCurrentTemplate(template)}
+            onClick={() => {
+              setCurrentTemplate(template);
+            }}
           >
           >
+            {/* {tags?.includes("POPULAR") && <FireIcon src={fire} size="15px" top="10px" right="10px" />} */}
             {renderIcon(icon, template.name)}
             {renderIcon(icon, template.name)}
             <TemplateTitle>{name}</TemplateTitle>
             <TemplateTitle>{name}</TemplateTitle>
             <TemplateDescription>{description}</TemplateDescription>
             <TemplateDescription>{description}</TemplateDescription>
+            <Spacer y={0.5} />
+
+            {Object.keys(DISPLAY_TAGS_MAP).map(tagKey => (
+              tags?.includes(tagKey) &&
+              <Tag
+                bottom="10px"
+                left="12px"
+                style={{ background: DISPLAY_TAGS_MAP[tagKey].color }}
+              >
+                {DISPLAY_TAGS_MAP[tagKey].label}
+              </Tag>
+            ))}
           </TemplateBlock>
           </TemplateBlock>
         );
         );
       })}
       })}
     </TemplateListWrapper>
     </TemplateListWrapper>
+
   );
   );
 };
 };
 
 
 export default TemplateList;
 export default TemplateList;
 
 
+const FireIcon = styled.img<{ size?: string, top?: string, right?: string }>`
+  height: ${props => props.size || '25px'};
+  position: absolute;
+  top: ${props => props.top || 'auto'};
+  right: ${props => props.right || 'auto'};
+  
+  &:hover::after {
+    content: "Popular";
+    position: absolute;
+    top: 100%;
+    left: 50%;
+    transform: translateX(-50%);
+    background-color: white;
+    color: black;
+    padding: 2px 5px;
+    border-radius: 3px;
+    font-size: 10px;
+    font-weight: bold;
+    text-align: center;
+  }
+`;
+
+const Tag = styled.div<{ size?: string, bottom?: string, left?: string }>`
+  position: absolute;
+  bottom: ${props => props.bottom || 'auto'};
+  left: ${props => props.left || 'auto'};
+  font-size: 10px;
+  background: linear-gradient(45deg, rgba(88, 24, 219, 1) 0%, rgba(72, 12, 168, 1) 100%); // added gradient for shiny effect
+  padding: 5px;
+  border-radius: 4px; 
+  opacity: 0.85;
+  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1)
+`;
+
 const Placeholder = styled.div`
 const Placeholder = styled.div`
   padding-top: 200px;
   padding-top: 200px;
   width: 100%;
   width: 100%;
@@ -222,7 +274,7 @@ const TemplateBlock = styled.div`
   flex-direction: column;
   flex-direction: column;
   align-item: center;
   align-item: center;
   justify-content: space-between;
   justify-content: space-between;
-  height: 170px;
+  height: 180px;
   cursor: pointer;
   cursor: pointer;
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
@@ -246,10 +298,10 @@ const TemplateBlock = styled.div`
 
 
 const TemplateListWrapper = styled.div`
 const TemplateListWrapper = styled.div`
   overflow: visible;
   overflow: visible;
-  margin-top: 35px;
-  padding-bottom: 150px;
+  margin-top: 15px;
+  padding-bottom: 50px;
   display: grid;
   display: grid;
-  grid-column-gap: 25px;
-  grid-row-gap: 25px;
+  grid-column-gap: 30px;
+  grid-row-gap: 30px;
   grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
   grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
 `;
 `;

+ 12 - 1
dashboard/src/shared/hardcodedNameDict.tsx

@@ -69,4 +69,15 @@ const hardcodedIcons: { [key: string]: string } = {
   "porter-agent": lightning,
   "porter-agent": lightning,
 };
 };
 
 
-export { hardcodedNames, hardcodedIcons };
+const DISPLAY_TAGS_MAP = {
+  "ANALYITCS": { label: "Analytics", color: "#1CCAD8" },
+  "NETWORKING": { label: "Networking", color: "#FF680A" },
+  "DATA_BASE": { label: "Database", color: "#5FAD56" },
+  "LOGGING": { label: "Logging", color: "#F72585" },
+  "MONITORING": { label: "Monitoring", color: "#774B9E" },
+  "CACHE": { label: "Cache", color: "#F72C25" },
+  "SEARCH": { label: "Search", color: "#F7B32B" },
+  "MISC": { label: "Misc.", color: "#616163" },
+};
+
+export { hardcodedNames, hardcodedIcons, DISPLAY_TAGS_MAP };

+ 1 - 2
dashboard/src/shared/types.tsx

@@ -273,6 +273,7 @@ export interface ProjectType {
   azure_enabled: boolean;
   azure_enabled: boolean;
   helm_values_enabled: boolean;
   helm_values_enabled: boolean;
   multi_cluster: boolean;
   multi_cluster: boolean;
+  full_add_ons: boolean;
   enable_reprovision: boolean;
   enable_reprovision: boolean;
   roles: {
   roles: {
     id: number;
     id: number;
@@ -667,5 +668,3 @@ export interface CreateUpdatePorterAppOptions {
   override_release?: boolean;
   override_release?: boolean;
   full_helm_values?: string;
   full_helm_values?: string;
 }
 }
-
-

+ 1 - 0
internal/helm/loader/loader.go

@@ -40,6 +40,7 @@ func RepoIndexToPorterChartList(index *repo.IndexFile, repoURL string) types.Lis
 			Icon:        indexChart.Icon,
 			Icon:        indexChart.Icon,
 			Versions:    versions,
 			Versions:    versions,
 			RepoURL:     repoURL,
 			RepoURL:     repoURL,
+			Tags:        indexChart.Keywords,
 		}
 		}
 
 
 		porterCharts = append(porterCharts, porterChart)
 		porterCharts = append(porterCharts, porterChart)

+ 2 - 0
internal/models/project.go

@@ -68,6 +68,7 @@ type Project struct {
 	AzureEnabled           bool
 	AzureEnabled           bool
 	HelmValuesEnabled      bool
 	HelmValuesEnabled      bool
 	MultiCluster           bool `gorm:"default:false"`
 	MultiCluster           bool `gorm:"default:false"`
+	FullAddOns             bool `gorm:"default:false"`
 	ValidateApplyV2        bool `gorm:"default:false"`
 	ValidateApplyV2        bool `gorm:"default:false"`
 	EnableReprovision      bool `gorm:"default:false"`
 	EnableReprovision      bool `gorm:"default:false"`
 }
 }
@@ -96,5 +97,6 @@ func (p *Project) ToProjectType() *types.Project {
 		MultiCluster:           p.MultiCluster,
 		MultiCluster:           p.MultiCluster,
 		EnableReprovision:      p.EnableReprovision,
 		EnableReprovision:      p.EnableReprovision,
 		ValidateApplyV2:        p.ValidateApplyV2,
 		ValidateApplyV2:        p.ValidateApplyV2,
+		FullAddOns:             p.FullAddOns,
 	}
 	}
 }
 }