Jelajahi Sumber

POR-1856 show error on app update failure (#3740)

Co-authored-by: d-g-town <66391417+d-g-town@users.noreply.github.com>
ianedwards 2 tahun lalu
induk
melakukan
016fd028c2

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

@@ -36,6 +36,7 @@ import ConfirmRedeployModal from "./ConfirmRedeployModal";
 import ImageSettingsTab from "./tabs/ImageSettingsTab";
 import { useAppAnalytics } from "lib/hooks/useAppAnalytics";
 import { useClusterResourceLimits } from "lib/hooks/useClusterResourceLimits";
+import Error from "components/porter/Error";
 
 // commented out tabs are not yet implemented
 // will be included as support is available based on data from app revisions rather than helm releases
@@ -60,6 +61,9 @@ type AppDataContainerProps = {
   tabParam?: string;
 };
 
+// todo(ianedwards): refactor button to use more predictable state
+export type ButtonStatus = "" | "loading" | JSX.Element | "success";
+
 const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
   const history = useHistory();
   const queryClient = useQueryClient();
@@ -135,7 +139,14 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
   const {
     reset,
     handleSubmit,
-    formState: { isDirty, dirtyFields, isSubmitting },
+    setError,
+    formState: {
+      isDirty,
+      dirtyFields,
+      isSubmitting,
+      errors,
+      isSubmitSuccessful,
+    },
   } = porterAppFormMethods;
 
   // getAllDirtyFields recursively gets all dirty fields from the dirtyFields object
@@ -293,23 +304,34 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
         appName: latestProto.name,
         errorStackTrace: stack,
       });
+
+      if (err.response?.data?.message) {
+        setError("app", {
+          message: `App update failed: ${err.response.data.message}`,
+        });
+      }
     }
   });
 
   const cancelRedeploy = useCallback(() => {
-    const resetProto = previewRevision ? PorterApp.fromJsonString(atob(previewRevision.b64_app_proto), {
-      ignoreUnknownFields: true,
-    }) : latestProto;
+    const resetProto = previewRevision
+      ? PorterApp.fromJsonString(atob(previewRevision.b64_app_proto), {
+          ignoreUnknownFields: true,
+        })
+      : latestProto;
 
     // we don't store versions of build settings because they are stored in the db, so we just have to use the latest version
     // however, for image settings, we can pull image repo and tag from the proto
-    const resetSource = porterAppRecord.image_repo_uri && resetProto.image ? {
-      type: "docker-registry" as const,
-      image: {
-        repository: resetProto.image.repository,
-        tag: resetProto.image.tag
-      }
-    } : latestSource;
+    const resetSource =
+      porterAppRecord.image_repo_uri && resetProto.image
+        ? {
+            type: "docker-registry" as const,
+            image: {
+              repository: resetProto.image.repository,
+              tag: resetProto.image.tag,
+            },
+          }
+        : latestSource;
 
     reset({
       app: clientAppFromProto({
@@ -333,22 +355,45 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
     onSubmit();
   }, [onSubmit, setConfirmDeployModalOpen]);
 
+  const buttonStatus = useMemo(() => {
+    if (isSubmitting) {
+      return "loading";
+    }
+
+    if (Object.keys(errors).length > 0) {
+      return errors.app?.message ? (
+        <Error message={errors.app.message} />
+      ) : (
+        <Error message="App update failed. If the error persists, please contact support." />
+      );
+    }
+
+    if (isSubmitSuccessful) {
+      return "success";
+    }
+
+    return "";
+  }, [isSubmitting, errors]);
+
   useEffect(() => {
     const newProto = previewRevision
       ? PorterApp.fromJsonString(atob(previewRevision.b64_app_proto), {
-        ignoreUnknownFields: true,
-      })
+          ignoreUnknownFields: true,
+        })
       : latestProto;
 
     // we don't store versions of build settings because they are stored in the db, so we just have to use the latest version
     // however, for image settings, we can pull image repo and tag from the proto
-    const newSource = porterAppRecord.image_repo_uri && newProto.image ? {
-      type: "docker-registry" as const,
-      image: {
-        repository: newProto.image.repository,
-        tag: newProto.image.tag
-      }
-    } : latestSource;
+    const newSource =
+      porterAppRecord.image_repo_uri && newProto.image
+        ? {
+            type: "docker-registry" as const,
+            image: {
+              repository: newProto.image.repository,
+              tag: newProto.image.tag,
+            },
+          }
+        : latestSource;
 
     reset({
       app: clientAppFromProto({
@@ -426,17 +471,17 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
             { label: "Environment", value: "environment" },
             ...(latestProto.build
               ? [
-                {
-                  label: "Build Settings",
-                  value: "build-settings",
-                },
-              ]
+                  {
+                    label: "Build Settings",
+                    value: "build-settings",
+                  },
+                ]
               : [
-                {
-                  label: "Image Settings",
-                  value: "image-settings",
-                },
-              ]),
+                  {
+                    label: "Image Settings",
+                    value: "image-settings",
+                  },
+                ]),
             { label: "Settings", value: "settings" },
           ]}
           currentTab={currentTab}
@@ -453,11 +498,24 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
         <Spacer y={1} />
         {match(currentTab)
           .with("activity", () => <Activity />)
-          .with("overview", () => <Overview maxCPU={maxCPU} maxRAM={maxRAM} />)
-          .with("build-settings", () => <BuildSettingsTab />)
-          .with("image-settings", () => <ImageSettingsTab />)
+          .with("overview", () => (
+            <Overview
+              maxCPU={maxCPU}
+              maxRAM={maxRAM}
+              buttonStatus={buttonStatus}
+            />
+          ))
+          .with("build-settings", () => (
+            <BuildSettingsTab buttonStatus={buttonStatus} />
+          ))
+          .with("image-settings", () => (
+            <ImageSettingsTab buttonStatus={buttonStatus} />
+          ))
           .with("environment", () => (
-            <Environment latestSource={latestSource} />
+            <Environment
+              latestSource={latestSource}
+              buttonStatus={buttonStatus}
+            />
           ))
           .with("settings", () => <Settings />)
           .with("logs", () => <LogsTab />)

+ 8 - 18
dashboard/src/main/home/app-dashboard/app-view/tabs/BuildSettingsTab.tsx

@@ -7,8 +7,13 @@ import Spacer from "components/porter/Spacer";
 import Button from "components/porter/Button";
 import Error from "components/porter/Error";
 import { match } from "ts-pattern";
+import { ButtonStatus } from "../AppDataContainer";
 
-const BuildSettingsTab: React.FC = () => {
+type Props = {
+  buttonStatus: ButtonStatus;
+};
+
+const BuildSettingsTab: React.FC<Props> = ({ buttonStatus }) => {
   const {
     watch,
     formState: { isSubmitting, errors },
@@ -18,20 +23,6 @@ const BuildSettingsTab: React.FC = () => {
   const build = watch("app.build");
   const source = watch("source");
 
-  const buttonStatus = useMemo(() => {
-    if (isSubmitting) {
-      return "loading";
-    }
-
-    if (Object.keys(errors).length > 0) {
-      // TODO: remove console.log once rollout is stable
-      console.log(errors);
-      return <Error message="Unable to update app" />;
-    }
-
-    return "";
-  }, [isSubmitting, errors]);
-
   return (
     <>
       {match(source)
@@ -58,10 +49,9 @@ const BuildSettingsTab: React.FC = () => {
             </Button>
           </>
         ))
-        .otherwise(() => null)
-      }
+        .otherwise(() => null)}
     </>
   );
 };
 
-export default BuildSettingsTab;
+export default BuildSettingsTab;

+ 4 - 16
dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx

@@ -11,23 +11,24 @@ import api from "shared/api";
 import { z } from "zod";
 import { populatedEnvGroup } from "../../validate-apply/app-settings/types";
 import EnvSettings from "../../validate-apply/app-settings/EnvSettings";
+import { ButtonStatus } from "../AppDataContainer";
 
 type Props = {
   latestSource: SourceOptions;
+  buttonStatus: ButtonStatus;
 };
 
-const Environment: React.FC<Props> = ({ latestSource }) => {
+const Environment: React.FC<Props> = ({ latestSource, buttonStatus }) => {
   const {
     latestRevision,
     latestProto,
     clusterId,
     projectId,
     previewRevision,
-    servicesFromYaml,
     attachedEnvGroups,
   } = useLatestRevision();
   const {
-    formState: { isSubmitting, errors },
+    formState: { isSubmitting },
   } = useFormContext<PorterAppFormData>();
 
   const { data: baseEnvGroups = [] } = useQuery(
@@ -52,18 +53,6 @@ const Environment: React.FC<Props> = ({ latestSource }) => {
     }
   );
 
-  const buttonStatus = useMemo(() => {
-    if (isSubmitting) {
-      return "loading";
-    }
-
-    if (Object.keys(errors).length > 0) {
-      return <Error message="Unable to update app" />;
-    }
-
-    return "";
-  }, [isSubmitting, errors]);
-
   return (
     <>
       <Text size={16}>Environment variables</Text>
@@ -74,7 +63,6 @@ const Environment: React.FC<Props> = ({ latestSource }) => {
         revision={previewRevision ? previewRevision : latestRevision} // get versions of env groups attached to preview revision if set
         baseEnvGroups={baseEnvGroups}
         latestSource={latestSource}
-        servicesFromYaml={servicesFromYaml}
         attachedEnvGroups={attachedEnvGroups}
       />
       <Spacer y={0.5} />

+ 7 - 14
dashboard/src/main/home/app-dashboard/app-view/tabs/ImageSettingsTab.tsx

@@ -12,29 +12,22 @@ import Link from "components/porter/Link";
 import Text from "components/porter/Text";
 import ImageSettings from "../../image-settings/ImageSettings";
 import { match } from "ts-pattern";
+import { ButtonStatus } from "../AppDataContainer";
 
-const ImageSettingsTab: React.FC = () => {
+type Props = {
+    buttonStatus: ButtonStatus;
+}
+
+const ImageSettingsTab: React.FC<Props> = ({ buttonStatus }) => {
     const {
         watch,
-        formState: { isSubmitting, errors },
+        formState: { isSubmitting },
         setValue,
     } = useFormContext<PorterAppFormData>();
     const { projectId, latestRevision, latestProto } = useLatestRevision();
 
     const source = watch("source");
 
-    const buttonStatus = useMemo(() => {
-        if (isSubmitting) {
-            return "loading";
-        }
-
-        if (Object.keys(errors).length > 0) {
-            return <Error message="Unable to update app" />;
-        }
-
-        return "";
-    }, [isSubmitting, errors]);
-
     return match(source)
         .with({ type: "docker-registry" }, (source) => (
             <>

+ 5 - 14
dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx

@@ -13,12 +13,15 @@ import Error from "components/porter/Error";
 import Button from "components/porter/Button";
 import { useLatestRevision } from "../LatestRevisionContext";
 import { useAppStatus } from "lib/hooks/useAppStatus";
+import { ButtonStatus } from "../AppDataContainer";
 
 type Props = {
   maxCPU: number;
   maxRAM: number;
-}
-const Overview: React.FC<Props> = ({ maxCPU, maxRAM }) => {
+  buttonStatus: ButtonStatus;
+};
+
+const Overview: React.FC<Props> = ({ maxCPU, maxRAM, buttonStatus }) => {
   const { formState } = useFormContext<PorterAppFormData>();
   const {
     porterApp,
@@ -37,18 +40,6 @@ const Overview: React.FC<Props> = ({ maxCPU, maxRAM }) => {
     appName: latestProto.name,
   });
 
-  const buttonStatus = useMemo(() => {
-    if (formState.isSubmitting) {
-      return "loading";
-    }
-
-    if (Object.keys(formState.errors).length > 0) {
-      return <Error message="Unable to update app" />;
-    }
-
-    return "";
-  }, [formState.isSubmitting, formState.errors]);
-
   return (
     <>
       {porterApp.git_repo_id && (