فهرست منبع

Merge branch 'nico/por-559-support-environment-group-creation-from' of github.com:porter-dev/porter into dev

jnfrati 3 سال پیش
والد
کامیت
16f2a656a6

+ 22 - 1
api/server/handlers/namespace/get_env_group.go

@@ -1,6 +1,7 @@
 package namespace
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -13,6 +14,8 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/stacks"
+	"gorm.io/gorm"
 )
 
 type GetEnvGroupHandler struct {
@@ -62,5 +65,23 @@ func (c *GetEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	c.WriteResult(w, r, envGroup)
+	stackId, err := stacks.GetStackForEnvGroup(c.Config(), cluster.ProjectID, cluster.ID, envGroup)
+
+	if err != nil {
+
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.WriteResult(w, r, &types.GetEnvGroupResponse{EnvGroup: envGroup})
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := &types.GetEnvGroupResponse{
+		EnvGroup: envGroup,
+		StackID:  stackId,
+	}
+
+	c.WriteResult(w, r, res)
 }

+ 5 - 0
api/types/namespace.go

@@ -210,3 +210,8 @@ type GetJobRunsRequest struct {
 type StreamJobRunsRequest struct {
 	Name string `schema:"name"`
 }
+
+type GetEnvGroupResponse struct {
+	*EnvGroup
+	StackID string `json:"stack_id,omitempty"`
+}

+ 1 - 0
dashboard/src/components/porter-form/types.ts

@@ -222,6 +222,7 @@ export type PopulatedEnvGroup = {
   };
   applications: any[];
   meta_version: number;
+  stack_id?: string;
 };
 export interface KeyValueArrayFieldState {
   values: {

+ 42 - 15
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -546,23 +546,39 @@ const EnvGroupSettings = ({
           </Helper>
           {!canDelete && (
             <Helper color="#f5cb42">
-              Looks like you still have applications syncedto this env group.
-              Please remove this env group from those applications to delete
+              Applications are still synced to this env group. Navigate to
+              "Linked Applications" and remove this env group from all
+              applications to delete.
             </Helper>
           )}
-          <Button
-            color="#b91133"
-            onClick={() => {
-              setCurrentOverlay({
-                message: `Are you sure you want to delete ${name}?`,
-                onYes: handleDeleteEnvGroup,
-                onNo: () => setCurrentOverlay(null),
-              });
-            }}
-            disabled={!canDelete}
-          >
-            Delete {envGroup.name}
-          </Button>
+          {envGroup.stack_id?.length ? (
+            <>
+              <Helper color="#f5cb42">
+                You have to delete the stack to remove this env group.
+              </Helper>
+              <CloneButton
+                as={DynamicLink}
+                color="#5561C0"
+                to={`/stacks/${envGroup.namespace}/${envGroup.stack_id}`}
+              >
+                Go to the stack
+              </CloneButton>
+            </>
+          ) : (
+            <Button
+              color="#b91133"
+              onClick={() => {
+                setCurrentOverlay({
+                  message: `Are you sure you want to delete ${envGroup.name}?`,
+                  onYes: handleDeleteEnvGroup,
+                  onNo: () => setCurrentOverlay(null),
+                });
+              }}
+              disabled={!canDelete}
+            >
+              Delete {envGroup.name}
+            </Button>
+          )}
         </InnerWrapper>
       )}
     </TabWrapper>
@@ -705,6 +721,17 @@ const Button = styled.button`
   }
 `;
 
+const CloneButton = styled(Button)`
+  display: flex;
+  width: fit-content;
+  align-items: center;
+  justify-content: center;
+  background-color: #ffffff11;
+  :hover {
+    background-color: #ffffff18;
+  }
+`;
+
 const InnerWrapper = styled.div<{ full?: boolean }>`
   width: 100%;
   height: ${(props) => (props.full ? "100%" : "calc(100% - 65px)")};

+ 20 - 5
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -19,6 +19,7 @@ import { Link } from "react-router-dom";
 import { isDeployedFromGithub } from "shared/release/utils";
 import TagSelector from "./TagSelector";
 import { PORTER_IMAGE_TEMPLATES } from "shared/common";
+import DynamicLink from "components/DynamicLink";
 
 type PropsType = {
   currentChart: ChartType;
@@ -316,10 +317,24 @@ const SettingsSection: React.FC<PropsType> = ({
           )}
 
           <Heading>Additional Settings</Heading>
-
-          <Button color="#b91133" onClick={() => setShowDeleteOverlay(true)}>
-            Delete {currentChart.name}
-          </Button>
+          {currentChart.stack_id?.length ? (
+            <>
+              <Helper>
+                You have to delete the stack to remove this application.
+              </Helper>
+              <CloneButton
+                as={DynamicLink}
+                color="#5561C0"
+                to={`/stacks/${currentChart.namespace}/${currentChart.stack_id}`}
+              >
+                Go to the stack
+              </CloneButton>
+            </>
+          ) : (
+            <Button color="#b91133" onClick={() => setShowDeleteOverlay(true)}>
+              Delete {currentChart.name}
+            </Button>
+          )}
         </StyledSettingsSection>
       ) : (
         <Loading />
@@ -368,7 +383,7 @@ const Button = styled.button`
 
 const CloneButton = styled(Button)`
   display: flex;
-  width: min-content;
+  width: fit-content;
   align-items: center;
   justify-content: center;
   background-color: #ffffff11;

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx

@@ -54,11 +54,11 @@ const Dashboard = () => {
               options={[
                 {
                   value: "created_at",
-                  label: "Created at",
+                  label: "Created At",
                 },
                 {
                   value: "updated_at",
-                  label: "Last updated",
+                  label: "Last Updated",
                 },
                 {
                   value: "alphabetical",

+ 138 - 63
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx

@@ -3,6 +3,7 @@ import Placeholder from "components/Placeholder";
 import TabSelector from "components/TabSelector";
 import TitleSection from "components/TitleSection";
 import React, { useContext, useEffect, useState } from "react";
+import backArrow from "assets/back_arrow.png";
 import { useParams } from "react-router";
 import api from "shared/api";
 import { Context } from "shared/Context";
@@ -26,6 +27,8 @@ import { FullStackRevision, Stack, StackRevision } from "../types";
 import EnvGroups from "./components/EnvGroups";
 import RevisionList from "./_RevisionList";
 import SourceConfig from "./_SourceConfig";
+import { NavLink } from "react-router-dom";
+import Settings from "./components/Settings";
 
 const ExpandedStack = () => {
   const { namespace, stack_id } = useParams<{
@@ -40,8 +43,8 @@ const ExpandedStack = () => {
   );
 
   const [stack, setStack] = useState<Stack>();
-  const [sortType, setSortType] = useState("Alphabetical");
   const [isLoading, setIsLoading] = useState(true);
+  const [isDeleting, setIsDeleting] = useState(false);
   const [currentTab, setCurrentTab] = useState("apps");
 
   const [currentRevision, setCurrentRevision] = useState<FullStackRevision>();
@@ -71,6 +74,28 @@ const ExpandedStack = () => {
     }
   };
 
+  const handleDelete = () => {
+    setIsDeleting(true);
+    api
+      .deleteStack(
+        "<token>",
+        {},
+        {
+          namespace,
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          stack_id: stack.id,
+        }
+      )
+      .then(() => {
+        pushFiltered("/stacks", []);
+      })
+      .catch((err) => {
+        setCurrentError(err);
+        setIsDeleting(false);
+      });
+  };
+
   useEffect(() => {
     getStack();
   }, [stack_id]);
@@ -79,14 +104,25 @@ const ExpandedStack = () => {
     return <Loading />;
   }
 
+  if (isDeleting) {
+    return (
+      <Placeholder height="400px">
+        <div>
+          <h1>Deleting Stack</h1>
+          <p>This may take some time...</p>
+          <Loading />
+        </div>
+      </Placeholder>
+    );
+  }
+
   return (
     <div>
       <StackTitleWrapper>
-        <TitleSection
-          materialIconClass="material-icons-outlined"
-          icon={"lan"}
-          capitalize
-        >
+        <BackButton to="/stacks">
+          <BackButtonImg src={backArrow} />
+        </BackButton>
+        <TitleSection materialIconClass="material-icons-outlined" icon={"lan"}>
           {stack.name}
         </TitleSection>
         <NamespaceTag.Wrapper>
@@ -94,41 +130,13 @@ const ExpandedStack = () => {
           <NamespaceTag.Tag>{stack.namespace}</NamespaceTag.Tag>
         </NamespaceTag.Wrapper>
       </StackTitleWrapper>
-      <RevisionList
-        revisions={stack.revisions}
-        currentRevision={currentRevision}
-        latestRevision={stack.latest_revision}
-        stackId={stack.id}
-        stackNamespace={namespace}
-        onRevisionClick={(revision) => setCurrentRevision(revision)}
-        onRollback={() => getStack()}
-      ></RevisionList>
-      <Br />
-      <InfoWrapper>
-        <LastDeployed>
-          <Status
-            status={getStackStatus(stack)}
-            message={getStackStatusMessage(stack)}
-          />
-          <SepDot>•</SepDot>
-          <Text color="#aaaabb">
-            {!stack.latest_revision?.id
-              ? `No version found`
-              : `v${stack.latest_revision.id}`}
-          </Text>
-          <SepDot>•</SepDot>
-          Last updated {readableDate(stack.updated_at)}
-        </LastDeployed>
-      </InfoWrapper>
 
       {/* Stack error message */}
       {currentRevision &&
       currentRevision?.reason &&
       currentRevision?.message?.length > 0 ? (
         <StackErrorMessageStyles.Wrapper>
-          <StackErrorMessageStyles.Title color="#b7b7c9">
-            Revision message:
-          </StackErrorMessageStyles.Title>
+          <i className="material-icons">history</i>
           <StackErrorMessageStyles.Text color="#aaaabb">
             {currentRevision?.status === "failed" ? "Error: " : ""}
             {currentRevision?.message}
@@ -136,6 +144,28 @@ const ExpandedStack = () => {
         </StackErrorMessageStyles.Wrapper>
       ) : null}
 
+      <Break />
+      <InfoWrapper>
+        <LastDeployed>
+          <Status
+            status={getStackStatus(stack)}
+            message={getStackStatusMessage(stack)}
+          />
+          <SepDot>•</SepDot>
+          Last updated {readableDate(stack.updated_at)}
+        </LastDeployed>
+      </InfoWrapper>
+
+      <RevisionList
+        revisions={stack.revisions}
+        currentRevision={currentRevision}
+        latestRevision={stack.latest_revision}
+        stackId={stack.id}
+        stackNamespace={namespace}
+        onRevisionClick={(revision) => setCurrentRevision(revision)}
+        onRollback={() => getStack()}
+      ></RevisionList>
+      <Br />
       <TabSelector
         currentTab={currentTab}
         options={[
@@ -148,32 +178,24 @@ const ExpandedStack = () => {
                 {currentRevision.id !== stack.latest_revision.id ? (
                   <ChartListWrapper>
                     <Placeholder>
-                      Not available when previewing versions
+                      Not available when previewing revisions
                     </Placeholder>
                   </ChartListWrapper>
                 ) : (
-                  <>
-                    <SortSelector
-                      setSortType={setSortType}
-                      sortType={sortType}
+                  <ChartListWrapper>
+                    <ChartList
+                      currentCluster={currentCluster}
                       currentView="stacks"
+                      namespace={namespace}
+                      sortType="Alphabetical"
+                      appFilters={
+                        stack?.latest_revision?.resources?.map(
+                          (res) => res.name
+                        ) || []
+                      }
+                      closeChartRedirectUrl={`${window.location.pathname}${window.location.search}`}
                     />
-
-                    <ChartListWrapper>
-                      <ChartList
-                        currentCluster={currentCluster}
-                        currentView="stacks"
-                        namespace={namespace}
-                        sortType="Alphabetical"
-                        appFilters={
-                          stack?.latest_revision?.resources?.map(
-                            (res) => res.name
-                          ) || []
-                        }
-                        closeChartRedirectUrl={`${window.location.pathname}${window.location.search}`}
-                      />
-                    </ChartListWrapper>
-                  </>
+                  </ChartListWrapper>
                 )}
               </>
             ),
@@ -193,7 +215,7 @@ const ExpandedStack = () => {
             ),
           },
           {
-            label: "Env groups",
+            label: "Env Groups",
             value: "env_groups",
             component: (
               <>
@@ -202,21 +224,68 @@ const ExpandedStack = () => {
               </>
             ),
           },
+          {
+            label: "Settings",
+            value: "settings",
+            component: (
+              <>
+                <Gap></Gap>
+                <Settings stackName={stack.name} onDelete={handleDelete} />
+              </>
+            ),
+          },
         ]}
         setCurrentTab={(tab) => {
           setCurrentTab(tab);
         }}
       ></TabSelector>
+      <PaddingBottom />
     </div>
   );
 };
 
 export default ExpandedStack;
 
+const PaddingBottom = styled.div`
+  width: 100%;
+  height: 150px;
+`;
+
+const Break = styled.div`
+  width: 100%;
+  height: 20px;
+`;
+
+const BackButton = styled(NavLink)`
+  position: absolute;
+  top: 0px;
+  right: 0px;
+  display: flex;
+  width: 36px;
+  cursor: pointer;
+  height: 36px;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+    > img {
+      opacity: 1;
+    }
+  }
+`;
+
+const BackButtonImg = styled.img`
+  width: 16px;
+  opacity: 0.75;
+`;
+
 const ChartListWrapper = styled.div`
   width: 100%;
   margin: auto;
-  margin-top: 20px;
   padding-bottom: 125px;
 `;
 
@@ -228,13 +297,18 @@ const Gap = styled.div`
 
 const StackErrorMessageStyles = {
   Text: styled(Text)`
-    font-size: 14px;
-    margin-bottom: 10px;
+    font-size: 13px;
   `,
   Wrapper: styled.div`
     display: flex;
-    flex-direction: column;
+    align-items: center;
+
     margin-top: 5px;
+    > i {
+      color: #ffffff44;
+      margin-right: 8px;
+      font-size: 20px;
+    }
   `,
   Title: styled(Text)`
     font-size: 16px;
@@ -245,11 +319,12 @@ const StackErrorMessageStyles = {
 const StackTitleWrapper = styled.div`
   width: 100%;
   display: flex;
-  justify-content: space-between;
+  position: relative;
   align-items: center;
 
   // Hotfix to make sure the title section and the namespace tag are aligned
   ${NamespaceTag.Wrapper} {
-    margin-bottom: 15px;
+    margin-left: 17px;
+    margin-bottom: 13px;
   }
 `;

+ 8 - 2
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_RevisionList.tsx

@@ -143,8 +143,9 @@ const _RevisionList = ({
         >
           <RevisionPreview>
             {currentRevision.id === latestRevision.id
-              ? `Current Revision v${currentRevision.id}`
-              : `Previewing Revision (Not Deployed) v${currentRevision.id}`}
+              ? `Current Revision`
+              : `Previewing Revision (Not Deployed)`}{" "}
+              - <Revision>No. {currentRevision.id}</Revision>
             <i className="material-icons">arrow_drop_down</i>
           </RevisionPreview>
         </RevisionHeader>
@@ -167,6 +168,11 @@ const _RevisionList = ({
 
 export default _RevisionList;
 
+const Revision = styled.div`
+  color: #ffffff;
+  margin-left: 5px;
+`;
+
 const StyledRevisionSection = styled.div`
   display: flex;
   flex-direction: column;

+ 3 - 2
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx

@@ -159,12 +159,13 @@ const SourceConfigStyles = {
   `,
   ItemContainer: styled.div`
     background: #ffffff11;
-    border-radius: 15px;
-    padding: 20px 15px;
+    border-radius: 8px;
+    padding: 30px 35px 35px;
   `,
   ItemTitle: styled.div`
     font-size: 16px;
     width: fit-content;
+    font-weight: 500;
   `,
   TooltipItem: styled.div`
     font-size: 14px;

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/EnvGroups.tsx

@@ -81,9 +81,9 @@ const EnvGroups = ({ stack }: { stack: Stack }) => {
     <Card.Grid style={{ marginTop: "0px" }}>
       {envGroups.map((envGroup) => {
         return (
-          <Card.Wrapper>
+          <Card.Wrapper variant="unclickable">
             <Card.Title>
-              <Card.Icon src={sliders}></Card.Icon>
+              <Card.SmallerIcon src={sliders}></Card.SmallerIcon>
               {envGroup.name}
             </Card.Title>
 

+ 5 - 10
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx

@@ -92,7 +92,7 @@ const Select = <T extends unknown>({
         <SelectStyles.Selector
           className={className}
           onClick={() => setExpanded(!expanded)}
-          expanded={expanded}
+          expanded={!readOnly && expanded}
           readOnly={readOnly}
         >
           <SelectStyles.CurrentValue>
@@ -153,18 +153,15 @@ export const SelectStyles = {
     height: 35px;
     border: 1px solid #ffffff55;
     font-size: 13px;
+    color: ${props => props.readOnly ? "#ffffff44" : ""};
     padding: 5px 10px;
     padding-left: 15px;
     border-radius: 3px;
     display: flex;
     justify-content: space-between;
     align-items: center;
-    cursor: ${(props) => (props.readOnly ? "normal" : "pointer")};
+    cursor: ${(props) => (props.readOnly ? "not-allowed" : "pointer")};
     background: ${(props) => {
-      if (props.readOnly) {
-        return "#ffffff55";
-      }
-
       if (props.expanded) {
         return "#ffffff33";
       }
@@ -174,10 +171,8 @@ export const SelectStyles = {
     :hover {
       background: ${(props) => {
         if (props.readOnly) {
-          return "#ffffff55";
-        }
-
-        if (props.expanded) {
+          return "#ffffff11";
+        } else if (props.expanded) {
           return "#ffffff33";
         }
         return "#ffffff22";

+ 79 - 0
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx

@@ -0,0 +1,79 @@
+import Heading from "components/form-components/Heading";
+import React, { useContext } from "react";
+import { Context } from "shared/Context";
+import styled from "styled-components";
+
+const Settings = ({
+  stackName,
+  onDelete,
+}: {
+  stackName: string;
+  onDelete: () => void;
+}) => {
+  const { setCurrentOverlay } = useContext(Context);
+
+  const handleDelete = () => {
+    setCurrentOverlay({
+      message: `Are you sure you want to delete ${stackName}?`,
+      onYes: () => {
+        onDelete();
+        setCurrentOverlay(null);
+      },
+      onNo: () => setCurrentOverlay(null),
+    });
+  };
+  return (
+    <Wrapper>
+      <StyledSettingsSection>
+        <Heading>Settings</Heading>
+        <Button color="#b91133" onClick={handleDelete}>
+          Delete stack
+        </Button>
+      </StyledSettingsSection>
+    </Wrapper>
+  );
+};
+
+export default Settings;
+
+const Wrapper = styled.div`
+  width: 100%;
+  padding-bottom: 65px;
+  height: 100%;
+`;
+
+const StyledSettingsSection = styled.div`
+  width: 100%;
+  background: #ffffff11;
+  padding: 0 35px;
+  padding-bottom: 15px;
+  position: relative;
+  border-radius: 8px;
+  overflow: auto;
+  height: calc(100% - 55px);
+`;
+
+const Button = styled.button`
+  height: 35px;
+  font-size: 13px;
+  margin-top: 20px;
+  margin-bottom: 30px;
+  font-weight: 500;
+  font-family: "Work Sans", sans-serif;
+  color: white;
+  padding: 6px 20px 7px 20px;
+  text-align: left;
+  border: 0;
+  border-radius: 5px;
+  background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
+  box-shadow: ${(props) =>
+    !props.disabled ? "0 2px 5px 0 #00000030" : "none"};
+  cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
+  user-select: none;
+  :focus {
+    outline: 0;
+  }
+  :hover {
+    filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
+  }
+`;

+ 41 - 7
dashboard/src/main/home/cluster-dashboard/stacks/launch/NewEnvGroup.tsx

@@ -3,7 +3,7 @@ import Heading from "components/form-components/Heading";
 import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import TitleSection from "components/TitleSection";
-import React, { useContext, useState } from "react";
+import React, { useContext, useMemo, useState } from "react";
 import { isAlphanumeric } from "shared/common";
 import { useRouting } from "shared/routing";
 import styled from "styled-components";
@@ -26,9 +26,6 @@ const NewEnvGroup = () => {
 
   const { pushFiltered } = useRouting();
 
-  const isDisabled = () =>
-    !isAlphanumeric(name) || name === "" || !envVariables.length;
-
   const handleOnSubmit = () => {
     const variables = envVariables.filter(
       (variable) => !variable.locked && !variable.hidden
@@ -49,6 +46,22 @@ const NewEnvGroup = () => {
     return;
   };
 
+  const hasError = useMemo(() => {
+    if (!isAlphanumeric(name) || name === "") {
+      return { message: "Name cannot be empty." };
+    }
+
+    if (!envVariables.length) {
+      return { message: "Please add at least one environment variable." };
+    }
+
+    if (envVariables.some((variable) => !variable.value || !variable.key)) {
+      return { message: "Please fill in all environment variables." };
+    }
+
+    return null;
+  }, [name, envVariables]);
+
   return (
     <>
       <TitleSection>
@@ -58,7 +71,7 @@ const NewEnvGroup = () => {
           </BackButton>
         </DynamicLink>
         <Polymer>
-          <Icon src={sliders} />
+          <SliderIcon src={sliders} />
         </Polymer>
         Add a Env Group to Stack
       </TitleSection>
@@ -98,7 +111,9 @@ const NewEnvGroup = () => {
         makeFlush
         clearPosition
         text="Save env group"
-        disabled={isDisabled()}
+        disabled={!!hasError}
+        statusPosition="left"
+        status={hasError?.message || ""}
       />
     </>
   );
@@ -106,8 +121,27 @@ const NewEnvGroup = () => {
 
 export default NewEnvGroup;
 
+export const SliderIcon = styled.img`
+  width: 25px;
+  margin-right: 16px;
+
+  opacity: 0;
+  animation: floatIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;
+
 const Subtitle = styled.div`
-  padding: 11px 0px 16px;
+  padding: 11px 0px 0px;
   font-family: "Work Sans", sans-serif;
   font-size: 13px;
   color: #aaaabb;

+ 16 - 3
dashboard/src/main/home/cluster-dashboard/stacks/launch/Overview.tsx

@@ -20,6 +20,7 @@ import TitleSection from "components/TitleSection";
 import DynamicLink from "components/DynamicLink";
 import { hardcodedIcons } from "shared/hardcodedNameDict";
 import sliders from "assets/sliders.svg";
+import DocsHelper from "components/DocsHelper";
 
 const Overview = () => {
   const {
@@ -151,12 +152,20 @@ const Overview = () => {
         />
       </ClusterSection>
 
-      <Heading>Env groups</Heading>
+      <Heading>
+        Env Groups
+        <InlineDocsHelper
+          disableMargin={true}
+          tooltipText="Environment Groups"
+          link="https://docs.porter.run/deploying-applications/environment-groups"
+        />
+      </Heading>
+      <Helper>Add scoped environment groups to this stack:</Helper>
       <Card.Grid>
         {newStack.env_groups.map((envGroup) => (
           <Card.Wrapper variant="unclickable">
             <Card.Title>
-              <Card.Icon src={sliders} />
+              <Card.SmallerIcon src={sliders} />
               {envGroup.name}
             </Card.Title>
             <Card.Actions>
@@ -189,7 +198,7 @@ const Overview = () => {
       </Helper>
       <Card.Grid>
         {newStack.app_resources.map((app) => (
-          <Card.Wrapper>
+          <Card.Wrapper variant="unclickable">
             <Card.Title>
               <Card.Icon src={hardcodedIcons[app.template_name]}></Card.Icon>
               {app.name}
@@ -300,3 +309,7 @@ const Icon = styled.div`
     color: #aaaabb;
   }
 `;
+
+const InlineDocsHelper = styled(DocsHelper)`
+  display: inline-block;
+`;

+ 5 - 1
dashboard/src/main/home/cluster-dashboard/stacks/launch/components/styles.tsx

@@ -55,7 +55,11 @@ export const Card = {
     font-size: 14px;
     font-weight: 500;
   `,
-
+  SmallerIcon: styled.img`
+    height: 20px;
+    margin-right: 18px;
+    margin-left: 8px;
+  `,
   Icon: styled.img`
     height: 30px;
     margin-right: 15px;

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/stacks/launch/index.tsx

@@ -50,4 +50,5 @@ const StyledLaunchFlow = styled.div`
   min-width: 300px;
   margin-top: ${(props: { disableMarginTop?: boolean }) =>
     props.disableMarginTop ? "inherit" : "calc(50vh - 380px)"};
+  margin-bottom: 50px;
 `;

+ 29 - 0
internal/stacks/hooks.go

@@ -166,3 +166,32 @@ func UpdateEnvGroupVersion(config *config.Config, projID, clusterID uint, envGro
 
 	return err
 }
+
+func GetStackForEnvGroup(config *config.Config, projID, clusterID uint, envGroup *types.EnvGroup) (string, error) {
+	// read stack env group by params
+	stackEnvGroup, err := config.Repo.Stack().ReadStackEnvGroupFirstMatch(projID, clusterID, envGroup.Namespace, envGroup.Name)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return "", nil
+		}
+
+		return "", err
+	}
+
+	// read the revision number corresponding and create a new revision of the stack
+	oldStackRevision, err := config.Repo.Stack().ReadStackRevision(stackEnvGroup.StackRevisionID)
+
+	if err != nil {
+		return "", err
+	}
+
+	// get the latest revision for that stack
+	stack, err := config.Repo.Stack().ReadStackByID(projID, oldStackRevision.StackID)
+
+	if err != nil {
+		return "", err
+	}
+
+	return stack.UID, nil
+}