Explorar o código

App template updates (#3791)

ianedwards %!s(int64=2) %!d(string=hai) anos
pai
achega
224a6fff3f

+ 13 - 3
dashboard/src/main/home/app-dashboard/app-view/tabs/preview-environments/PreviewEnvironmentSettings.tsx

@@ -10,6 +10,8 @@ import { useGithubWorkflow } from "lib/hooks/useGithubWorkflow";
 import styled from "styled-components";
 import healthy from "assets/status-healthy.png";
 import Icon from "components/porter/Icon";
+import { z } from "zod";
+import { PorterApp } from "@porter-dev/api-contracts";
 
 type Props = {};
 
@@ -20,7 +22,7 @@ const PreviewEnvironmentSettings: React.FC<Props> = ({}) => {
     ["getAppTemplate", projectId, clusterId, porterApp.name],
     async () => {
       try {
-        await api.getAppTemplate(
+        const res = await api.getAppTemplate(
           "<token>",
           {},
           {
@@ -30,9 +32,17 @@ const PreviewEnvironmentSettings: React.FC<Props> = ({}) => {
           }
         );
 
-        return true;
+        const data = await z
+          .object({
+            template_b64_app_proto: z.string(),
+          })
+          .parseAsync(res.data);
+
+        return PorterApp.fromJsonString(atob(data.template_b64_app_proto), {
+          ignoreUnknownFields: true,
+        });
       } catch (err) {
-        return false;
+        return null;
       }
     }
   );

+ 54 - 10
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/AppTemplateForm.tsx

@@ -24,16 +24,22 @@ import api from "shared/api";
 import { z } from "zod";
 import { populatedEnvGroup } from "main/home/app-dashboard/validate-apply/app-settings/types";
 import { useQuery } from "@tanstack/react-query";
-import { Redirect } from "react-router";
+import { Redirect, useHistory } from "react-router";
 import Button from "components/porter/Button";
 import { useAppValidation } from "lib/hooks/useAppValidation";
 import { PorterApp } from "@porter-dev/api-contracts";
 import axios from "axios";
 import GithubActionModal from "main/home/app-dashboard/new-app-flow/GithubActionModal";
+import Error from "components/porter/Error";
+import _ from "lodash";
 import { useClusterResources } from "shared/ClusterResourcesContext";
 
-const AppTemplateForm: React.FC = () => {
-  const [step, setStep] = useState(0);
+type Props = {
+  existingTemplate: PorterApp | null;
+};
+
+const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
+  const history = useHistory();
   const [validatedAppProto, setValidatedAppProto] = useState<PorterApp | null>(
     null
   );
@@ -55,7 +61,6 @@ const AppTemplateForm: React.FC = () => {
     servicesFromYaml,
     clusterId,
     projectId,
-    deploymentTarget,
   } = useLatestRevision();
 
   const { data: baseEnvGroups = [] } = useQuery(
@@ -104,14 +109,14 @@ const AppTemplateForm: React.FC = () => {
   const withPreviewOverrides = useMemo(() => {
     return applyPreviewOverrides({
       app: clientAppFromProto({
-        proto: latestProto,
+        proto: existingTemplate ? existingTemplate : latestProto,
         overrides: servicesFromYaml,
         variables: appEnv?.variables,
         secrets: appEnv?.secret_variables,
       }),
       overrides: servicesFromYaml?.previews,
     });
-  }, [latestProto, appEnv, servicesFromYaml]);
+  }, [latestProto, existingTemplate, appEnv, servicesFromYaml]);
 
   const porterAppFormMethods = useForm<PorterAppFormData>({
     reValidateMode: "onSubmit",
@@ -127,7 +132,31 @@ const AppTemplateForm: React.FC = () => {
     },
   });
 
-  const { reset, handleSubmit } = porterAppFormMethods;
+  const {
+    reset,
+    handleSubmit,
+    formState: { errors, isSubmitting, isSubmitSuccessful },
+  } = porterAppFormMethods;
+
+  const errorMessagesDeep = useMemo(() => {
+    return Object.values(_.mapValues(errors, (error) => error?.message));
+  }, [errors]);
+
+  const buttonStatus = useMemo(() => {
+    if (isSubmitting) {
+      return "loading";
+    }
+
+    if (errorMessagesDeep.length > 0) {
+      return <Error message={`App update failed. ${errorMessagesDeep[0]}`} />;
+    }
+
+    if (isSubmitSuccessful) {
+      return "success";
+    }
+
+    return "";
+  }, [isSubmitting, errorMessagesDeep]);
 
   const onSubmit = handleSubmit(async (data) => {
     try {
@@ -153,7 +182,17 @@ const AppTemplateForm: React.FC = () => {
         }, {});
       setFinalizedAppEnv({ variables, secrets });
 
-      setShowGHAModal(true);
+      if (!existingTemplate) {
+        setShowGHAModal(true);
+        return;
+      }
+
+      await createTemplateAndWorkflow({
+        app: proto,
+        variables,
+        secrets,
+      });
+      history.push(`/apps/${proto.name}/settings`);
     } catch (err) {
       if (axios.isAxiosError(err) && err.response?.data?.error) {
         setCreateError(err.response?.data?.error);
@@ -278,8 +317,13 @@ const AppTemplateForm: React.FC = () => {
                 maxRAM={currentClusterResources.maxRAM}
               />
             </>,
-            <Button type="submit" loadingText={"Deploying..."} width={"150px"}>
-              Enable Previews
+            <Button
+              type="submit"
+              loadingText={"Saving..."}
+              width={"150px"}
+              status={buttonStatus}
+            >
+              {existingTemplate ? "Update Previews" : "Enable Previews"}
             </Button>,
           ].filter((x) => x)}
         />

+ 57 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/SetupApp.tsx

@@ -9,10 +9,18 @@ import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader";
 import Spacer from "components/porter/Spacer";
 import AppTemplateForm from "./AppTemplateForm";
 import { LatestRevisionProvider } from "main/home/app-dashboard/app-view/LatestRevisionContext";
+import { useQuery } from "@tanstack/react-query";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { match } from "ts-pattern";
+import { z } from "zod";
+import { PorterApp } from "@porter-dev/api-contracts";
+import Loading from "components/Loading";
 
 type Props = RouteComponentProps & {};
 
 const SetupApp: React.FC<Props> = ({ location }) => {
+  const { currentCluster, currentProject } = useContext(Context);
   const params = useMemo(() => {
     const queryParams = new URLSearchParams(location.search);
     const appName = queryParams.get("app_name");
@@ -24,6 +32,49 @@ const SetupApp: React.FC<Props> = ({ location }) => {
 
   const appName = params.appName;
 
+  const templateRes = useQuery(
+    ["getAppTemplate", currentProject?.id, currentCluster?.id, appName],
+    async () => {
+      if (
+        !currentProject ||
+        !currentCluster ||
+        currentCluster.id === -1 ||
+        currentProject.id === -1 ||
+        !appName
+      ) {
+        return null;
+      }
+
+      try {
+        const res = await api.getAppTemplate(
+          "<token>",
+          {},
+          {
+            project_id: currentProject?.id,
+            cluster_id: currentCluster?.id,
+            porter_app_name: appName,
+          }
+        );
+
+        const data = await z
+          .object({
+            template_b64_app_proto: z.string(),
+          })
+          .parseAsync(res.data);
+
+        return PorterApp.fromJsonString(atob(data.template_b64_app_proto), {
+          ignoreUnknownFields: true,
+        });
+      } catch (err) {
+        return null;
+      }
+    },
+    {
+      enabled: !!currentProject && !!currentCluster && !!appName,
+      refetchOnWindowFocus: false,
+    }
+  );
+
   if (!appName) {
     return null;
   }
@@ -42,7 +93,12 @@ const SetupApp: React.FC<Props> = ({ location }) => {
               disableLineBreak
             />
             <DarkMatter />
-            <AppTemplateForm />
+            {match(templateRes)
+              .with({ status: "loading" }, () => <Loading />)
+              .with({ status: "success" }, ({ data }) => {
+                return <AppTemplateForm existingTemplate={data} />;
+              })
+              .otherwise(() => null)}
             <Spacer y={3} />
           </StyledConfigureTemplate>
         </Div>