Quellcode durchsuchen

remove extra error for env var keys (#3972)

ianedwards vor 2 Jahren
Ursprung
Commit
fb4617d1b8

+ 34 - 31
dashboard/src/lib/porter-apps/index.ts

@@ -1,25 +1,27 @@
-import { BUILDPACK_TO_NAME } from "main/home/app-dashboard/types/buildpack";
+import {
+  EFS,
+  HelmOverrides,
+  PorterApp,
+  Service,
+  type Build,
+} from "@porter-dev/api-contracts";
+import { match } from "ts-pattern";
 import { z } from "zod";
+
+import { BUILDPACK_TO_NAME } from "main/home/app-dashboard/types/buildpack";
+import { type KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
+
+import { buildValidator, type BuildOptions } from "./build";
 import {
-  DetectedServices,
   defaultSerialized,
   deserializeService,
-  serializeService,
   serializedServiceFromProto,
+  serializeService,
   serviceProto,
   serviceValidator,
   uniqueServices,
+  type DetectedServices,
 } from "./services";
-import {
-  Build,
-  HelmOverrides,
-  PorterApp,
-  Service,
-  EFS,
-} from "@porter-dev/api-contracts";
-import { match } from "ts-pattern";
-import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
-import { BuildOptions, buildValidator } from "./build";
 
 // sourceValidator is used to validate inputs for source setting fields
 export const sourceValidator = z.discriminatedUnion("type", [
@@ -145,6 +147,15 @@ export const porterAppFormValidator = z
       message: "app must have at least one service",
       path: ["app", "services"],
     }
+  )
+  .refine(
+    ({ app: { env } }) => {
+      return env.every((e) => e.key.length > 0 && /^[A-Za-z]/.test(e.key));
+    },
+    {
+      message: "All environment variables keys must start with a letter",
+      path: ["app", "env"],
+    }
   );
 export type PorterAppFormData = z.infer<typeof porterAppFormValidator>;
 
@@ -285,12 +296,10 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
               app.helmOverrides != null
                 ? new HelmOverrides({ b64Values: btoa(app.helmOverrides) })
                 : undefined,
-
           }),
-          efsStorage:
-            new EFS({
-              enabled: app.efsStorage.enabled,
-            })
+          efsStorage: new EFS({
+            enabled: app.efsStorage.enabled,
+          }),
         })
     )
     .with(
@@ -311,11 +320,9 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
             app.helmOverrides != null
               ? new HelmOverrides({ b64Values: btoa(app.helmOverrides) })
               : undefined,
-          efsStorage:
-            new EFS({
-              enabled: app.efsStorage.enabled,
-            })
-
+          efsStorage: new EFS({
+            enabled: app.efsStorage.enabled,
+          }),
         })
     )
     .exhaustive();
@@ -459,11 +466,10 @@ export function clientAppFromProto({
         buildpacks: [],
         builder: "",
       },
-      helmOverrides: helmOverrides,
+      helmOverrides,
       efsStorage: new EFS({
         enabled: proto.efsStorage?.enabled ?? false,
-      })
-
+      }),
     };
   }
 
@@ -501,11 +507,8 @@ export function clientAppFromProto({
       buildpacks: [],
       builder: "",
     },
-    helmOverrides: helmOverrides,
-    efsStorage:
-      { enabled: proto.efsStorage?.enabled ?? false }
-
-    ,
+    helmOverrides,
+    efsStorage: { enabled: proto.efsStorage?.enabled ?? false },
   };
 }
 

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

@@ -416,6 +416,14 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
             errorMessage = `${errorMessage} - ${serviceErrorMessage}`;
           }
           errorMessage = `${errorMessage}. To undo all changes, refresh the page.`;
+        } else if (appErrors.includes("env")) {
+          errorMessage = "Environment variables are not properly configured";
+          if (errors.app?.env?.root?.message ?? errors.app?.env?.message) {
+            const envErrorMessage =
+              errors.app?.env?.root?.message ?? errors.app?.env?.message;
+            errorMessage = `${errorMessage} - ${envErrorMessage}`;
+          }
+          errorMessage = `${errorMessage}. To undo all changes, refresh the page.`;
         } else if (appErrors.includes("message")) {
           // this is the high level error message coming from the apply
           errorMessage = errors.app?.message ?? errorMessage;

+ 202 - 200
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvVarRow.tsx

@@ -1,219 +1,215 @@
 import React from "react";
-import { type KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
-import Tooltip from "components/porter/Tooltip";
 import { Controller, useFormContext } from "react-hook-form";
-import { type PorterAppFormData } from "lib/porter-apps";
+import styled from "styled-components";
+
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
-import styled from "styled-components";
+import Tooltip from "components/porter/Tooltip";
+import { type KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
+import { type PorterAppFormData } from "lib/porter-apps";
 
 type Props = {
-    entry: KeyValueType;
-    index: number;
-    remove: () => void;
-    isKeyOverriding: (key: string) => boolean;
-    invalidKey: (key: string) => boolean;
-}
+  entry: KeyValueType;
+  index: number;
+  remove: () => void;
+  isKeyOverriding: (key: string) => boolean;
+};
 const EnvVarRow: React.FC<Props> = ({
-    entry,
-    index,
-    remove,
-    isKeyOverriding,
-    invalidKey,
+  entry,
+  index,
+  remove,
+  isKeyOverriding,
 }) => {
-    const { control: appControl, watch, setError, clearErrors, formState: { errors } } = useFormContext<PorterAppFormData>();
-    const hidden = watch(`app.env.${index}.hidden`);
-    const keys = watch(`app.env.${index}.key`);
+  const { control: appControl, watch } = useFormContext<PorterAppFormData>();
+  const hidden = watch(`app.env.${index}.hidden`);
+  const keys = watch(`app.env.${index}.key`);
 
-    const validateKey = (key: string): boolean => {
-        const isValid = /^[A-Za-z]/.test(key);
-        if (!isValid) {
-            setError(`app.env.${index}.key`, {
-                type: "manual",
-                message: "Key must begin with a letter",
-            });
-        } else {
-            clearErrors(`app.env.${index}.key`);
-        }
-        return isValid;
-    };
+  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>
-            ) : (
-                <Controller
-                    name={`app.env.${index}.key`}
-                    control={appControl}
-                    render={({ field: { value, onChange }, fieldState: { error } }) => (
-                        <>
-                            <Input
-                                placeholder="ex: key"
-                                width="270px"
-                                value={value}
-                                onChange={(e) => {
-                                    validateKey(e.target.value);
-                                    onChange(e.target.value);
-                                }}
-                                spellCheck={false}
-                                override={isKeyOverriding(value)}
-                                style={error ? { borderColor: '#fbc902' } : {}}
-                            />
-                        </>
-                    )}
-                />
-            )}
-            <Spacer x={0.5} inline />
-            {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>
-                ) : (
-                    <Controller
-                        name={`app.env.${index}.value`}
-                        control={appControl}
-                        render={({ field: { value, onChange } }) => (
-                            <Input
-                                placeholder="ex: value"
-                                width="270px"
-                                value={value}
-                                onChange={(e) => { onChange(e.target.value); }}
-                                type={"password"}
-                                spellCheck={false}
-                                override={isKeyOverriding(entry.key)}
-                            />
-                        )}
-                    />
-                )
-            ) : (
-                <Controller
-                    name={`app.env.${index}.value`}
-                    control={appControl}
-                    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)}
-                        />
-                    )}
-                />
-            )}
-            {hidden ? (
-                <Controller
-                    name={`app.env.${index}.hidden`}
-                    control={appControl}
-                    render={({ field: { value, onChange } }) => (
-                        <HideButton
-                            onClick={() => {
-                                onChange(!value)
-                            }}
-                            disabled={entry.locked}
-                        >
-                            <i className="material-icons">lock_open</i>
-                        </HideButton>
-                    )}
-                />
-            ) : (
-                <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>
+  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>
+      ) : (
+        <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" } : {}}
+              />
+            </>
+          )}
+        />
+      )}
+      <Spacer x={0.5} inline />
+      {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>
+        ) : (
+          <Controller
+            name={`app.env.${index}.value`}
+            control={appControl}
+            render={({ field: { value, onChange } }) => (
+              <Input
+                placeholder="ex: value"
+                width="270px"
+                value={value}
+                onChange={(e) => {
+                  onChange(e.target.value);
+                }}
+                type={"password"}
+                spellCheck={false}
+                override={isKeyOverriding(entry.key)}
+              />
             )}
-            <DeleteButton
+          />
+        )
+      ) : (
+        <Controller
+          name={`app.env.${index}.value`}
+          control={appControl}
+          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)}
+            />
+          )}
+        />
+      )}
+      {hidden ? (
+        <Controller
+          name={`app.env.${index}.hidden`}
+          control={appControl}
+          render={({ field: { value, onChange } }) => (
+            <HideButton
+              onClick={() => {
+                onChange(!value);
+              }}
+              disabled={entry.locked}
+            >
+              <i className="material-icons">lock_open</i>
+            </HideButton>
+          )}
+        />
+      ) : (
+        <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={() => {
-                    remove()
+                  onChange(!value);
                 }}
-            >
-                <i className="material-icons">cancel</i>
-            </DeleteButton>
-            {!invalidKey(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>
-                </>
+                disabled={entry.locked}
+              >
+                <i className="material-icons">lock</i>
+              </HideButton>
             )}
-        </InputWrapper>
-    );
+          />
+        </Tooltip>
+      )}
+      <DeleteButton
+        onClick={() => {
+          remove();
+        }}
+      >
+        <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>
+  );
 };
 export default EnvVarRow;
 
-
 const InputWrapper = styled.div`
-            display: flex;
-            align-items: center;
-            margin-top: 5px;
-
-            `;
+  display: flex;
+  align-items: center;
+  margin-top: 5px;
+`;
 
 type InputProps = {
-    disabled?: boolean;
-    width: string;
-    override?: boolean;
+  disabled?: boolean;
+  width: string;
+  override?: boolean;
 };
 
 const Input = styled.input<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;
-                width: ${(props) => props.width ? props.width : "270px"};
-                color: ${(props) => props.disabled ? "#ffffff44" : "white"};
-                padding: 5px 10px;
-                height: 35px;
-                `;
+  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;
+  width: ${(props) => (props.width ? props.width : "270px")};
+  color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
+  padding: 5px 10px;
+  height: 35px;
+`;
 
 const MultiLineInputer = styled.textarea<InputProps>`
                     outline: none;
@@ -221,11 +217,17 @@ const MultiLineInputer = styled.textarea<InputProps>`
                     margin-bottom: 5px;
                     font-size: 13px;
                     background: #ffffff11;
-                    border: ${(props) => (props.override ? '2px solid #6b74d6' : ' 1px solid #ffffff55')};
+                    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")};
+                    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;
@@ -283,10 +285,10 @@ const HideButton = styled(DeleteButton)`
   > i {
                         font - size: 19px;
                     cursor: ${(props: { disabled: boolean }) =>
-        props.disabled ? "default" : "pointer"};
+                      props.disabled ? "default" : "pointer"};
                     :hover {
                         color: ${(props: { disabled: boolean }) =>
-        props.disabled ? "#ffffff44" : "#ffffff88"};
+                          props.disabled ? "#ffffff44" : "#ffffff88"};
     }
   }
-                    `;
+                    `;

+ 35 - 30
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvVariables.tsx

@@ -1,14 +1,16 @@
 import React, { useState } from "react";
+import { useFieldArray, useFormContext } from "react-hook-form";
 import styled from "styled-components";
-import Modal from "main/home/modals/Modal";
-import EnvEditorModal from "main/home/modals/EnvEditorModal";
 
-import upload from "assets/upload.svg";
-import { dotenv_parse } from "shared/string_utils";
 import { type NewPopulatedEnvGroup } from "components/porter-form/types";
 import Spacer from "components/porter/Spacer";
+import EnvEditorModal from "main/home/modals/EnvEditorModal";
+import Modal from "main/home/modals/Modal";
 import { type PorterAppFormData } from "lib/porter-apps";
-import { useFormContext, useFieldArray } from "react-hook-form";
+
+import { dotenv_parse } from "shared/string_utils";
+import upload from "assets/upload.svg";
+
 import EnvVarRow from "./EnvVarRow";
 
 export type KeyValueType = {
@@ -23,36 +25,33 @@ type PropsType = {
   syncedEnvGroups?: NewPopulatedEnvGroup[];
 };
 
-const EnvVariables = ({
-  syncedEnvGroups
-}: PropsType) => {
+const EnvVariables = ({ syncedEnvGroups }: PropsType) => {
   const [showEditorModal, setShowEditorModal] = useState(false);
 
   const { control: appControl } = useFormContext<PorterAppFormData>();
 
-  const { append, remove, update, fields: environmentVariables } = useFieldArray({
+  const {
+    append,
+    remove,
+    update,
+    fields: environmentVariables,
+  } = useFieldArray({
     control: appControl,
     name: "app.env",
   });
 
   const isKeyOverriding = (key: string): boolean => {
     if (!syncedEnvGroups) return false;
-    return syncedEnvGroups.some(envGroup =>
-      key in envGroup.variables || key in envGroup?.secret_variables
+    return syncedEnvGroups.some(
+      (envGroup) =>
+        key in envGroup.variables || key in envGroup?.secret_variables
     );
   };
 
-  const invalidKey = (key: string): boolean => {
-    const isValid = /^[A-Za-z]/.test(key);
-
-    return isValid;
-  };
-
-
-  const readFile = (env: string) => {
+  const readFile = (env: string): void => {
     const envObj = dotenv_parse(env);
     for (const key in envObj) {
-      const match = environmentVariables.find(envVar => envVar.key === key);
+      const match = environmentVariables.find((envVar) => envVar.key === key);
       if (match && !match.deleted) {
         const index = environmentVariables.indexOf(match);
         update(index, { ...match, value: envObj[key] });
@@ -63,7 +62,7 @@ const EnvVariables = ({
           hidden: false,
           locked: false,
           deleted: false,
-        })
+        });
       }
     }
   };
@@ -71,16 +70,17 @@ const EnvVariables = ({
   return (
     <>
       <StyledInputArray>
-        {environmentVariables.map((entry, i) =>
+        {environmentVariables.map((entry, i) => (
           <EnvVarRow
             key={entry.id}
             entry={entry}
             index={i}
-            remove={() => { remove(i); }}
+            remove={() => {
+              remove(i);
+            }}
             isKeyOverriding={isKeyOverriding}
-            invalidKey={invalidKey}
           />
-        )}
+        ))}
         <InputWrapper>
           <AddRowButton
             onClick={() => {
@@ -90,7 +90,7 @@ const EnvVariables = ({
                 hidden: false,
                 locked: false,
                 deleted: false,
-              })
+              });
             }}
           >
             <i className="material-icons">add</i> Add Row
@@ -107,13 +107,19 @@ const EnvVariables = ({
       </StyledInputArray>
       {showEditorModal && (
         <Modal
-          onRequestClose={() => { setShowEditorModal(false); }}
+          onRequestClose={() => {
+            setShowEditorModal(false);
+          }}
           width="60%"
           height="650px"
         >
           <EnvEditorModal
-            closeModal={() => { setShowEditorModal(false); }}
-            setEnvVariables={(envFile: string) => { readFile(envFile); }}
+            closeModal={() => {
+              setShowEditorModal(false);
+            }}
+            setEnvVariables={(envFile: string) => {
+              readFile(envFile);
+            }}
           />
         </Modal>
       )}
@@ -123,7 +129,6 @@ const EnvVariables = ({
 
 export default EnvVariables;
 
-
 const AddRowButton = styled.div`
   display: flex;
   align-items: center;