Sfoglia il codice sorgente

overhaul app + create app env fe (#4254)

jusrhee 2 anni fa
parent
commit
a388582ab4

+ 100 - 0
dashboard/src/components/porter/Expandable.tsx

@@ -0,0 +1,100 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+
+type Props = {
+  header: React.ReactNode;
+  children: React.ReactNode;
+  style?: React.CSSProperties;
+};
+
+// TODO: support footer for consolidation w/ app services
+const Expandable: React.FC<Props> = ({
+  header,
+  children,
+  style,
+}) => {
+  const [isExpanded, setIsExpanded] = useState(false);
+
+  return (
+    <StyledExpandable style={style}>
+      <Header
+        isExpanded={isExpanded}
+        onClick={() => { setIsExpanded(!isExpanded) }}
+      >
+        <span className="material-icons dropdown">
+          arrow_drop_down
+        </span>
+        <FullWidth>
+          {header}
+        </FullWidth>
+      </Header>
+      <ExpandedContents isExpanded={isExpanded}>
+        {children}
+      </ExpandedContents>
+    </StyledExpandable>
+  );
+};
+
+export default Expandable;
+
+const ExpandedContents = styled.div<{ isExpanded: boolean }>`
+  transition: all 0.5s;
+  overflow: hidden;
+  max-height: ${({ isExpanded }) => isExpanded ? "500px" : "0"};
+  padding: ${({ isExpanded }) => isExpanded ? "20px" : "0"};
+  border-bottom-left-radius: 5px;
+  border-bottom-right-radius: 5px;
+  background: ${(props) => props.theme.fg};
+  border: ${({ isExpanded }) => isExpanded && "1px solid #494b4f"};
+  border-top: 0;
+  color: ${(props) => props.theme.text.primary};
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+`;
+
+const FullWidth = styled.div`
+  width: 100%;
+`;
+
+const Header = styled.div<{ isExpanded: boolean }>`
+  transition: all 0.2s;
+  display: flex;
+  align-items: center;
+  height: 60px;
+  cursor: pointer;
+  padding: 20px;
+  color: ${(props) => props.theme.text.primary};
+  position: relative;
+  border-radius: 5px;
+  background: ${(props) => props.theme.clickable.bg};
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+
+  border-bottom-left-radius: ${({ isExpanded }) => isExpanded && "0"};
+  border-bottom-right-radius: ${({ isExpanded }) => isExpanded && "0"};
+
+  .dropdown {
+    font-size: 30px;
+    cursor: pointer;
+    border-radius: 20px;
+    margin-left: -5px;
+    margin-right: 8px;
+    transform: ${({ isExpanded }) => !isExpanded && "rotate(-90deg)"};
+  }
+
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const StyledExpandable = styled.div`
+  transition: all 0.2s;
+`;

+ 15 - 5
dashboard/src/components/porter/Tooltip.tsx

@@ -44,18 +44,28 @@ const TooltipContainer = styled.div`
 `;
 
 const TooltipContent = styled.div<{ position: string, width?: string }>`
-  background-color: #333;
   color: #fff;
-  padding: 8px;
-  border-radius: 4px;
-  font-size: 14px;
+  padding: 10px;
+  border-radius: 5px;
+  font-size: 13px;
   position: absolute;
   z-index: 10;
   max-width: ${({ width }) => width ?? "200px"};
   width: ${({ width }) => width ?? "200px"};
   text-align: center;
   white-space: pre-wrap;
-  word-wrap: break-word;
+  border: 1px solid #494b4f;
+  background: #42444933;
+  backdrop-filter: saturate(150%) blur(8px);
+  animation: fadeInModal 0.5s 0s;
+  @keyframes fadeInModal {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
 
   ${({ position }) => {
     switch (position) {

+ 4 - 4
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -521,21 +521,21 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
 
     if (latestProto.build) {
       base.push({
-        label: "Build Settings",
+        label: "Build settings",
         value: "build-settings",
       });
     } else {
       base.push({
-        label: "Image Settings",
+        label: "Image settings",
         value: "image-settings",
       });
     }
 
     if ((currentProject?.helm_values_enabled ?? false) || user?.isPorterUser) {
-      base.push({ label: "Helm Overrides", value: "helm-overrides" });
+      base.push({ label: "Helm overrides", value: "helm-overrides" });
     }
     if ((currentProject?.helm_values_enabled ?? false) || user?.isPorterUser) {
-      base.push({ label: "Latest Helm Values", value: "helm-values" });
+      base.push({ label: "Latest Helm values", value: "helm-values" });
     }
 
     base.push({ label: "Settings", value: "settings" });

+ 0 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx

@@ -249,7 +249,6 @@ const ActionButton = styled.button`
   position: relative;
   border: none;
   background: none;
-  color: white;
   padding: 5px;
   display: flex;
   justify-content: center;

+ 107 - 0
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroupRow.tsx

@@ -0,0 +1,107 @@
+import React, { useState, useEffect } from "react";
+import styled from "styled-components";
+
+import { useHistory } from "react-router";
+
+import doppler from "assets/doppler.png";
+import key from "assets/key.svg";
+
+import Container from "components/porter/Container";
+import Expandable from "components/porter/Expandable";
+import Image from "components/porter/Image";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import EnvGroupArray from "main/home/env-dashboard/EnvGroupArray";
+
+type Props = {
+  onRemove: (name: string) => void;
+  envGroup: any;
+};
+
+// TODO: support footer for consolidation w/ app services
+const EnvGroupRow: React.FC<Props> = ({ envGroup, onRemove }) => {
+  const history = useHistory();
+  const [isExpanded, setIsExpanded] = useState(false);
+  const [envVariables, setEnvVariables] = useState([]);
+
+  useEffect(() => {
+    const normalVariables = Object.entries(
+      envGroup.variables || {}
+    ).map(([key, value]) => ({
+      key,
+      value,
+      hidden: (value as string).includes("PORTERSECRET"),
+      locked: (value as string).includes("PORTERSECRET"),
+      deleted: false,
+    }));
+    const secretVariables = Object.entries(
+      envGroup.secret_variables || {}
+    ).map(([key, value]) => ({
+      key,
+      value,
+      hidden: true,
+      locked: true,
+      deleted: false,
+    }));
+    const variables = [...normalVariables, ...secretVariables];
+    setEnvVariables(variables);
+  }, [envGroup]);
+
+  return (
+    <Expandable
+      header={(
+        <Container row spaced>
+          <Container row>
+            <Image
+              size={20}
+              src={envGroup.type === "doppler" ? doppler : key}
+            />
+            <Spacer inline x={1} />
+            <Text size={14}>{envGroup.name}</Text>
+          </Container>
+          <Container row>
+            <Svg 
+              onClick={() => { 
+                history.push(`/environment-groups/${envGroup.name}/synced-apps`) 
+              }}
+              data-testid="geist-icon" fill="none" height="27px" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round" strokeLinejoin="round" stroke-width="2" viewBox="0 0 24 24" width="27px" data-darkreader-inline-stroke="" data-darkreader-inline-color=""><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path><path d="M15 3h6v6"></path><path d="M10 14L21 3"></path></Svg>
+            <Spacer inline x={.5} />
+            <I 
+              className="material-icons"
+              onClick={() => { onRemove(envGroup.name) }}
+            >
+              delete
+            </I>
+          </Container>
+        </Container>
+      )}
+    >
+      <EnvGroupArray
+        values={envVariables}
+        disabled={true}
+      />
+    </Expandable>
+  );
+};
+
+export default EnvGroupRow;
+
+const I = styled.i`
+  font-size: 20px;
+  cursor: pointer;
+  padding: 5px;
+  color: #aaaabb;
+  :hover {
+    color: white;
+  }
+`;
+
+const Svg = styled.svg`
+  stroke-width: 2;
+  cursor: pointer;
+  padding: 5px;
+  stroke: #aaaabb;
+  :hover {
+    stroke: white;
+  }
+`;

+ 49 - 32
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroups.tsx

@@ -2,19 +2,18 @@ import React, { useMemo, useState } from "react";
 import { useFieldArray, useFormContext } from "react-hook-form";
 import styled from "styled-components";
 import { type IterableElement } from "type-fest";
+import { useHistory } from "react-router";
 
-import Icon from "components/porter/Icon";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
+import { type PopulatedEnvGroup } from "./types";
 import { type PorterAppFormData } from "lib/porter-apps";
 
 import { valueExists } from "shared/util";
-import doppler from "assets/doppler.png";
-import sliders from "assets/sliders.svg";
 
 import EnvGroupModal from "./EnvGroupModal";
-import ExpandableEnvGroup from "./ExpandableEnvGroup";
-import { type PopulatedEnvGroup } from "./types";
+import Button from "components/porter/Button";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import EnvGroupRow from "./EnvGroupRow";
 
 type Props = {
   baseEnvGroups?: PopulatedEnvGroup[];
@@ -25,6 +24,7 @@ const EnvGroups: React.FC<Props> = ({
   baseEnvGroups = [],
   attachedEnvGroups = [],
 }) => {
+  const history = useHistory();
   const [showEnvModal, setShowEnvModal] = useState(false);
 
   const { control } = useFormContext<PorterAppFormData>();
@@ -91,10 +91,12 @@ const EnvGroups: React.FC<Props> = ({
     append(inp);
   };
 
-  const onRemove = (index: number): void => {
-    const name = populatedEnvWithFallback[index].envGroup.name;
-    remove(index);
-
+  const onRemove = (name: string): void => {
+    const index = populatedEnvWithFallback.findIndex(eg => eg.envGroup.name === name);
+    if (index !== -1) {
+      remove(index);
+    }
+  
     const existingEnvGroupNames = envGroups.map((eg) => eg.name);
     if (existingEnvGroupNames.includes(name)) {
       appendDeletion({ name });
@@ -102,34 +104,39 @@ const EnvGroups: React.FC<Props> = ({
   };
 
   return (
-    <div>
-      <LoadButton
-        disabled={false}
-        onClick={() => {
-          setShowEnvModal(true);
-        }}
-      >
-        <img src={sliders} /> Load from Env Group
-      </LoadButton>
+    <>
+      <Text size={16}>Synced environment groups</Text>
+      <Spacer y={0.5} />
+      <Text color="helper">
+        This application will be automatically redeployed when a synced env group is updated.
+      </Text>
+      <Spacer y={1} />
       {populatedEnvWithFallback.length > 0 && (
         <>
-          <Spacer y={0.5} />
-          <Text size={16}>Synced environment groups</Text>
           {populatedEnvWithFallback.map(({ envGroup, id, index }) => {
             return (
-              <ExpandableEnvGroup
-                key={id}
-                index={index}
-                envGroup={envGroup}
-                remove={onRemove}
-                icon={
-                  <Icon src={envGroup.type === "doppler" ? doppler : sliders} />
-                }
-              />
+              <>
+                <EnvGroupRow
+                  key={id}
+                  envGroup={envGroup}
+                  onRemove={onRemove}
+                />
+                {index !== populatedEnvWithFallback.length - 1 && <Spacer y={.5} />}
+              </>
             );
           })}
+          <Spacer y={1} />
         </>
       )}
+      <Button
+        alt
+        onClick={() => {
+          setShowEnvModal(true);
+        }}
+      >
+        <I className="material-icons">add</I>
+        Sync an env group
+      </Button>
       {showEnvModal ? (
         <EnvGroupModal
           setOpen={setShowEnvModal}
@@ -137,12 +144,22 @@ const EnvGroups: React.FC<Props> = ({
           append={onAdd}
         />
       ) : null}
-    </div>
+    </>
   );
 };
 
 export default EnvGroups;
 
+const I = styled.i`
+  font-size: 20px;
+  cursor: pointer;
+  padding: 5px;
+  color: #aaaabb;
+  :hover {
+    color: white;
+  }
+`;
+
 const AddRowButton = styled.div`
   display: flex;
   align-items: center;

+ 4 - 1
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvSettings.tsx

@@ -5,6 +5,7 @@ import { PopulatedEnvGroup } from "./types";
 import EnvVariables from "./EnvVariables";
 import EnvGroups from "./EnvGroups";
 import { AppRevision } from "lib/revisions/types";
+import Spacer from "components/porter/Spacer";
 
 type Props = {
   appName?: string;
@@ -17,7 +18,9 @@ type Props = {
 const EnvSettings: React.FC<Props> = (props) => {
   return (
     <>
-      <EnvVariables syncedEnvGroups={[]}/>
+      <Spacer y={1} />
+      <EnvVariables syncedEnvGroups={props.attachedEnvGroups}/>
+      <Spacer y={1} />
       <EnvGroups {...props} attachedEnvGroups={props.attachedEnvGroups} />
     </>
   );

+ 118 - 153
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvVarRow.tsx

@@ -2,6 +2,9 @@ import React from "react";
 import { Controller, useFormContext } from "react-hook-form";
 import styled from "styled-components";
 
+import warning from "assets/warning.svg";
+
+import Image from "components/porter/Image";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import Tooltip from "components/porter/Tooltip";
@@ -22,68 +25,46 @@ const EnvVarRow: React.FC<Props> = ({
 }) => {
   const { control: appControl, watch } = useFormContext<PorterAppFormData>();
   const hidden = watch(`app.env.${index}.hidden`);
-  const keys = watch(`app.env.${index}.key`);
-
-  const validKey = (key: string): boolean => /^[A-Za-z]/.test(key);
 
   return (
     <InputWrapper>
       {entry.locked ? (
-        <Tooltip
-          content={
-            "Secrets are immutable. To edit, delete and recreate the key with your new value."
-          }
-          position={"bottom"}
-        >
-          <Input
-            placeholder="ex: key"
-            width="270px"
-            value={entry.key}
-            disabled
-            spellCheck={false}
-            override={isKeyOverriding(entry.key)}
-          />
-        </Tooltip>
+        <Input
+          placeholder="ex: key"
+          width="270px"
+          value={entry.key}
+          disabled
+          spellCheck={false}
+        />
       ) : (
         <Controller
           name={`app.env.${index}.key`}
           control={appControl}
           render={({ field: { value, onChange }, fieldState: { error } }) => (
-            <>
-              <Input
-                placeholder="ex: key"
-                width="270px"
-                value={value}
-                onChange={(e) => {
-                  onChange(e.target.value);
-                }}
-                spellCheck={false}
-                override={isKeyOverriding(value)}
-                style={error ? { borderColor: "#fbc902" } : {}}
-              />
-            </>
+            <Input
+              placeholder="ex: key"
+              width="270px"
+              value={value}
+              onChange={(e) => {
+                onChange(e.target.value);
+              }}
+              spellCheck={false}
+              style={error ? { borderColor: "#fbc902" } : {}}
+            />
           )}
         />
       )}
-      <Spacer x={0.5} inline />
+      <Spacer inline width="10px" />
       {hidden ? (
         entry.locked ? (
-          <Tooltip
-            content={
-              "Secrets are immutable. To edit, delete and recreate the key with your new value."
-            }
-            position={"bottom"}
-          >
-            <Input
-              placeholder="ex: value"
-              width="270px"
-              value={entry.value}
-              disabled
-              type={"password"}
-              spellCheck={false}
-              override={isKeyOverriding(entry.key)}
-            />
-          </Tooltip>
+          <Input
+            placeholder="ex: value"
+            flex
+            value={entry.value}
+            disabled
+            type={"password"}
+            spellCheck={false}
+          />
         ) : (
           <Controller
             name={`app.env.${index}.value`}
@@ -91,14 +72,13 @@ const EnvVarRow: React.FC<Props> = ({
             render={({ field: { value, onChange } }) => (
               <Input
                 placeholder="ex: value"
-                width="270px"
+                flex
                 value={value}
                 onChange={(e) => {
                   onChange(e.target.value);
                 }}
                 type={"password"}
                 spellCheck={false}
-                override={isKeyOverriding(entry.key)}
               />
             )}
           />
@@ -110,18 +90,29 @@ const EnvVarRow: React.FC<Props> = ({
           render={({ field: { value, onChange } }) => (
             <MultiLineInputer
               placeholder="ex: value"
-              width="270px"
               value={value}
               onChange={(e) => {
                 onChange(e.target.value);
               }}
               rows={value?.split("\n").length}
               spellCheck={false}
-              override={isKeyOverriding(entry.key)}
             />
           )}
         />
       )}
+      {isKeyOverriding(entry.key) && (
+        <>
+          <Spacer width="10px" inline />
+          <Tooltip
+            content="This key overrides a value in a synced environment group"
+            position="left"
+            width="220px"
+          >
+            <Image src={warning} size={14} />
+          </Tooltip>
+          <Spacer width="2px" inline />
+        </>
+      )}
       {hidden ? (
         <Controller
           name={`app.env.${index}.hidden`}
@@ -138,25 +129,20 @@ const EnvVarRow: React.FC<Props> = ({
           )}
         />
       ) : (
-        <Tooltip
-          content={"Click to turn this variable into a secret"}
-          position={"bottom"}
-        >
-          <Controller
-            name={`app.env.${index}.hidden`}
-            control={appControl}
-            render={({ field: { value, onChange } }) => (
-              <HideButton
-                onClick={() => {
-                  onChange(!value);
-                }}
-                disabled={entry.locked}
-              >
-                <i className="material-icons">lock</i>
-              </HideButton>
-            )}
-          />
-        </Tooltip>
+        <Controller
+          name={`app.env.${index}.hidden`}
+          control={appControl}
+          render={({ field: { value, onChange } }) => (
+            <HideButton
+              onClick={() => {
+                onChange(!value);
+              }}
+              disabled={entry.locked}
+            >
+              <i className="material-icons">lock</i>
+            </HideButton>
+          )}
+        />
       )}
       <DeleteButton
         onClick={() => {
@@ -165,20 +151,6 @@ const EnvVarRow: React.FC<Props> = ({
       >
         <i className="material-icons">cancel</i>
       </DeleteButton>
-      {!validKey(keys) && (
-        <>
-          <Spacer x={1} inline />
-          <Text color={"#fbc902"}>Key must begin with a letter</Text>
-        </>
-      )}
-      {isKeyOverriding(entry.key) && (
-        <>
-          <Spacer x={1} inline />
-          <Text color={"#6b74d6"}>
-            Key is overriding value in an environment group
-          </Text>
-        </>
-      )}
     </InputWrapper>
   );
 };
@@ -187,7 +159,7 @@ export default EnvVarRow;
 const InputWrapper = styled.div`
   display: flex;
   align-items: center;
-  margin-top: 5px;
+  margin-bottom: 5px;
 `;
 
 type InputProps = {
@@ -196,99 +168,92 @@ type InputProps = {
   override?: boolean;
 };
 
-const Input = styled.input<InputProps>`
+const Input = styled.input<{ flex?: boolean; override?: boolean }>`
   outline: none;
+  display: ${(props) => (props.flex ? "flex" : "block")};
+  ${(props) => (props.flex && 'flex: 1;')}
   border: none;
   margin-bottom: 5px;
   font-size: 13px;
-  background: #ffffff11;
-  border: ${(props) =>
-    props.override ? "2px solid #6b74d6" : " 1px solid #ffffff55"};
-  border-radius: 3px;
-  width: ${(props) => (props.width ? props.width : "270px")};
-  color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
+  background: ${(props) => props.theme.fg};
+  border: ${(props) => (props.override ? '2px solid #f4cb42' : ' 1px solid #494b4f')};
+  border-radius: 5px;
+  width: ${(props) => props.width ? props.width : "270px"};
+  color: ${(props) => props.disabled ? "#ffffff44" : "#fefefe"};
   padding: 5px 10px;
   height: 35px;
 `;
 
-const MultiLineInputer = styled.textarea<InputProps>`
-                    outline: none;
-                    border: none;
-                    margin-bottom: 5px;
-                    font-size: 13px;
-                    background: #ffffff11;
-                    border: ${(props) =>
-                      props.override
-                        ? "2px solid #6b74d6"
-                        : " 1px solid #ffffff55"};
-                    border-radius: 3px;
-                    min-width: ${(props) =>
-                      props.width ? props.width : "270px"};
-                    max-width: ${(props) =>
-                      props.width ? props.width : "270px"};
-                    color: ${(props) =>
-                      props.disabled ? "#ffffff44" : "white"};
-                    padding: 8px 10px 5px 10px;
-                    min-height: 35px;
-                    max-height: 100px;
-                    white-space: nowrap;
+export const MultiLineInputer = styled.textarea<InputProps>`
+  outline: none;
+  border: none;
+  display: flex;
+  flex: 1;
+  margin-bottom: 5px;
+  font-size: 13px;
+  background: ${(props) => props.theme.fg};
+  border: ${(props) => (props.override ? '2px solid #f4cb42' : ' 1px solid #494b4f')};
+  border-radius: 5px;
+  color: ${(props) => (props.disabled ? "#ffffff44" : "#fefefe")};
+  padding: 8px 10px 5px 10px;
+  min-height: 35px;
+  max-height: 100px;
+  white-space: nowrap;
 
-                    ::-webkit-scrollbar {
-                        width: 8px;
-                    :horizontal {
-                        height: 8px;
+  ::-webkit-scrollbar {
+    width: 8px;
+    :horizontal {
+      height: 8px;
     }
   }
 
-                    ::-webkit-scrollbar-corner {
-                        width: 10px;
-                    background: #ffffff11;
-                    color: white;
+  ::-webkit-scrollbar-corner {
+    width: 10px;
+    background: #ffffff11;
+    color: white;
   }
 
-                    ::-webkit-scrollbar-track {
-                        width: 10px;
-                    -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
-                    box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+  ::-webkit-scrollbar-track {
+    width: 10px;
+    -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+    box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
   }
 
-                    ::-webkit-scrollbar-thumb {
-                        background - color: darkgrey;
-                    outline: 1px solid slategrey;
+  ::-webkit-scrollbar-thumb {
+    background-color: darkgrey;
+    outline: 1px solid slategrey;
   }
-                    `;
+`;
 
 const DeleteButton = styled.div`
-                    width: 15px;
-                    height: 15px;
-                    display: flex;
-                    align-items: center;
-                    margin-left: 8px;
-                    margin-top: -3px;
-                    justify-content: center;
+  width: 15px;
+  height: 15px;
+  display: flex;
+  align-items: center;
+  margin-left: 8px;
+  justify-content: center;
 
   > i {
-                        font - size: 17px;
-                    color: #ffffff44;
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
-                    cursor: pointer;
-                    :hover {
-                        color: #ffffff88;
+    font-size: 17px;
+    color: #ffffff44;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    :hover {
+      color: #ffffff88;
     }
   }
                     `;
 
 const HideButton = styled(DeleteButton)`
-                    margin-top: -5px;
   > i {
-                        font - size: 19px;
-                    cursor: ${(props: { disabled: boolean }) =>
-                      props.disabled ? "default" : "pointer"};
-                    :hover {
-                        color: ${(props: { disabled: boolean }) =>
-                          props.disabled ? "#ffffff44" : "#ffffff88"};
+    font-size: 19px;
+    cursor: ${(props: { disabled: boolean }) =>
+      props.disabled ? "default" : "pointer"};
+    :hover {
+      color: ${(props: { disabled: boolean }) =>
+        props.disabled ? "#ffffff44" : "#ffffff88"};
     }
   }
-                    `;
+`;

+ 49 - 87
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvVariables.tsx

@@ -11,7 +11,9 @@ import { type PorterAppFormData } from "lib/porter-apps";
 import { dotenv_parse } from "shared/string_utils";
 import upload from "assets/upload.svg";
 
+import Image from "components/porter/Image";
 import EnvVarRow from "./EnvVarRow";
+import Button from "components/porter/Button";
 
 export type KeyValueType = {
   key: string;
@@ -44,7 +46,7 @@ const EnvVariables = ({ syncedEnvGroups }: PropsType) => {
     if (!syncedEnvGroups) return false;
     return syncedEnvGroups.some(
       (envGroup) =>
-        key in envGroup.variables || key in envGroup?.secret_variables
+        key in envGroup.variables || key in envGroup.secret_variables
     );
   };
 
@@ -69,42 +71,47 @@ const EnvVariables = ({ syncedEnvGroups }: PropsType) => {
 
   return (
     <>
-      <StyledInputArray>
-        {environmentVariables.map((entry, i) => (
-          <EnvVarRow
-            key={entry.id}
-            entry={entry}
-            index={i}
-            remove={() => {
-              remove(i);
-            }}
-            isKeyOverriding={isKeyOverriding}
-          />
-        ))}
-        <InputWrapper>
-          <AddRowButton
-            onClick={() => {
-              append({
-                key: "",
-                value: "",
-                hidden: false,
-                locked: false,
-                deleted: false,
-              });
-            }}
-          >
-            <i className="material-icons">add</i> Add Row
-          </AddRowButton>
-          <Spacer x={0.5} inline />
-          <UploadButton
-            onClick={() => {
-              setShowEditorModal(true);
-            }}
-          >
-            <img src={upload} alt="Upload" /> Copy from File
-          </UploadButton>
-        </InputWrapper>
-      </StyledInputArray>
+      {environmentVariables.map((entry, i) => (
+        <EnvVarRow
+          key={entry.id}
+          entry={entry}
+          index={i}
+          remove={() => {
+            remove(i);
+          }}
+          isKeyOverriding={isKeyOverriding}
+        />
+      ))}
+      {environmentVariables.length > 0 && (
+        <Spacer y={.5} />
+      )} 
+      <InputWrapper>
+        <Button
+          alt
+          onClick={() => {
+            append({
+              key: "",
+              value: "",
+              hidden: false,
+              locked: false,
+              deleted: false,
+            });
+          }}
+        >
+          <I className="material-icons">add</I> Add row
+        </Button>
+        <Spacer x={0.5} inline />
+        <Button
+          alt
+          onClick={() => {
+            setShowEditorModal(true);
+          }}
+        >
+          <Image src={upload} size={16} />
+          <Spacer inline x={.5} />
+          Copy from file
+        </Button>
+      </InputWrapper>
       {showEditorModal && (
         <Modal
           onRequestClose={() => {
@@ -129,58 +136,13 @@ const EnvVariables = ({ syncedEnvGroups }: PropsType) => {
 
 export default EnvVariables;
 
-const AddRowButton = styled.div`
-  display: flex;
-  align-items: center;
-  width: 270px;
-  font-size: 13px;
-  color: #aaaabb;
-  height: 32px;
-  border-radius: 3px;
-  cursor: pointer;
-  background: #ffffff11;
-  :hover {
-    background: #ffffff22;
-  }
-
-  > i {
-    color: #ffffff44;
-    font-size: 16px;
-    margin-left: 8px;
-    margin-right: 10px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-`;
-
-const UploadButton = styled(AddRowButton)`
-  background: none;
-  position: relative;
-  border: 1px solid #ffffff55;
-  > i {
-    color: #ffffff44;
-    font-size: 16px;
-    margin-left: 8px;
-    margin-right: 10px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-  > img {
-    width: 14px;
-    margin-left: 10px;
-    margin-right: 12px;
-  }
+const I = styled.i`
+  font-size: 16px;
+  margin-right: 7px;
 `;
 
 const InputWrapper = styled.div`
   display: flex;
   align-items: center;
-  margin-top: 5px;
-`;
-
-const StyledInputArray = styled.div`
-  margin-bottom: 15px;
-  margin-top: 22px;
-`;
+  margin-bottom: 5px;
+`;

+ 0 - 2
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/ExpandableEnvGroup.tsx

@@ -99,9 +99,7 @@ const fadeIn = keyframes`
 const StyledCard = styled.div`
   border: 1px solid #ffffff44;
   background: #ffffff11;
-  margin-bottom: 5px;
   border-radius: 8px;
-  margin-top: 15px;
   padding: 10px 14px;
   overflow: hidden;
   font-size: 13px;

+ 4 - 3
dashboard/src/main/home/env-dashboard/EnvGroupArray.tsx

@@ -21,7 +21,7 @@ export type KeyValueType = {
 type PropsType = {
   label?: string;
   values: KeyValueType[];
-  setValues: (x: KeyValueType[]) => void;
+  setValues?: (x: KeyValueType[]) => void;
   disabled?: boolean;
   fileUpload?: boolean;
   secretOption?: boolean;
@@ -131,7 +131,6 @@ const EnvGroupArray = ({
                 {entry.hidden ? (
                   <Input
                     placeholder="ex: value"
-                    width="270px"
                     value={entry.value}
                     flex
                     onChange={(e: any) => {
@@ -293,7 +292,8 @@ const InputWrapper = styled.div`
   align-items: center;
   margin-bottom: 5px;
 `;
-const Input = styled.input<{ flex?: boolean }>`
+
+const Input = styled.input<{ flex?: boolean; override?: boolean }>`
   outline: none;
   display: ${(props) => (props.flex ? "flex" : "block")};
   ${(props) => (props.flex && 'flex: 1;')}
@@ -308,6 +308,7 @@ const Input = styled.input<{ flex?: boolean }>`
   padding: 5px 10px;
   height: 35px;
 `;
+
 const Label = styled.div`
   color: #ffffff;
   margin-bottom: 10px;

+ 1 - 1
dashboard/src/main/home/env-dashboard/tabs/SyncedAppsTab.tsx

@@ -71,7 +71,7 @@ const SyncedAppsTab: React.FC<Props> = ({ envGroup }) => {
             key: ra.source.name,
             onSelect: () => {
               history.push(
-                `/apps/${ra.source.name}?target=${ra.app_revision.deployment_target.id}`
+                `/apps/${ra.source.name}/environment?target=${ra.app_revision.deployment_target.id}`
               );
             },
           }))}