Feroze Mohideen 3 лет назад
Родитель
Сommit
99ea5eea62

+ 121 - 19
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -12,6 +12,7 @@ import pr_icon from "assets/pull_request_icon.svg";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import useAuth from "shared/auth/useAuth";
+import Error from "components/porter/Error";
 
 import Loading from "components/Loading";
 import Text from "components/porter/Text";
@@ -27,6 +28,8 @@ import Button from "components/porter/Button";
 import Services from "../new-app-flow/Services";
 import { Service } from "../new-app-flow/serviceTypes";
 import ConfirmOverlay from "components/porter/ConfirmOverlay";
+import { createFinalPorterYaml } from "../new-app-flow/schema";
+import EnvGroupArray, { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
 
 type Props = RouteComponentProps & {};
 
@@ -61,6 +64,11 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const [newestImage, setNewestImage] = useState<string>(null);
   const [showDeleteOverlay, setShowDeleteOverlay] = useState<boolean>(false);
 
+  const [services, setServices] = useState<Service[]>([]);
+  const [envVars, setEnvVars] = useState<KeyValueType[]>([]);
+  const [updating, setUpdating] = useState<boolean>(false);
+  const [updateError, setUpdateError] = useState<string>("");
+
   const getPorterApp = async () => {
     setIsLoading(true);
     const { appName } = props.match.params as any;
@@ -85,14 +93,24 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           revision: 0,
         }
       );
-      setAppData({
+
+      const newAppData = {
         app: resPorterApp?.data,
         chart: resChartData?.data,
-      });
-      console.log(appData);
-      setIsLoading(false);
+      };
+      setAppData(newAppData);
+
+      const helmValues = resChartData?.data?.config;
+      const defaultValues = resChartData?.data?.chart?.values;
+      if ((defaultValues && Object.keys(defaultValues).length > 0) || (helmValues && Object.keys(helmValues).length > 0)) {
+        const svcs = Service.deserialize(helmValues, defaultValues);
+        setServices(svcs);
+        console.log(helmValues);
+      }
+      console.log(newAppData);
     } catch (err) {
       setError(err);
+    } finally {
       setIsLoading(false);
     }
   };
@@ -127,7 +145,51 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     }
   };
 
-  const renderIcon = (b: string, size?: string) => {
+  const updatePorterApp = async () => {
+    try {
+      setUpdating(true);
+      if (
+        appData != null
+        && currentCluster != null
+        && currentProject != null
+        && appData.app != null
+      ) {
+        const finalPorterYaml = createFinalPorterYaml(
+          services,
+          [],
+          undefined,
+          appData.app.name,
+          currentProject.id,
+          currentCluster.id,
+        )
+        const yamlString = yaml.dump(finalPorterYaml);
+        const base64Encoded = btoa(yamlString);
+        await api.updatePorterStack(
+          "<token>",
+          {
+            stack_name: appData.app.name,
+            porter_yaml: base64Encoded,
+          },
+          {
+            cluster_id: currentCluster.id,
+            project_id: currentProject.id,
+            stack_name: appData.app.name,
+          }
+        )
+      } else {
+        setUpdateError("Unable to update app, please try again later.");
+      }
+    } catch (err) {
+      // TODO: better error handling
+      console.log(err);
+      const errMessage = err?.response?.data?.error ?? err?.toString() ?? 'An error occurred while deploying your app. Please try again.'
+      setUpdateError(errMessage);
+    } finally {
+      setUpdating(false)
+    }
+  }
+
+  const renderIcon = (b?: string, size?: string) => {
     var src = box;
     if (b) {
       const bp = b.split(",")[0]?.split("/")[1];
@@ -293,18 +355,28 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const renderTabContents = () => {
     switch (tab) {
       case "overview":
-        const helmValues = appData?.chart?.config;
-        const defaultValues = appData?.chart?.chart?.values;
-        if ((defaultValues && Object.keys(defaultValues).length > 0) || (helmValues && Object.keys(helmValues).length > 0)) {
-          const svcs = Service.deserialize(helmValues, defaultValues);
-          return <Services
-            setServices={(services: any[]) => {
-            }}
-            services={svcs}
-          />;
-        } else {
-          return <Text>No services found for this application yet.</Text>
-        }
+        return (
+          <>
+            <Services
+              setServices={setServices}
+              services={services}
+            />
+            <Spacer y={0.5} />
+            <Button
+              onClick={() => {
+                updatePorterApp();
+              }}
+              status={updating ? "loading" : updateError ? (
+                <Error message={updateError} />
+              ) : undefined}
+              loadingText={"Updating..."}
+              width={"150px"}
+            >
+              Update app
+            </Button>
+            <Spacer y={0.5} />
+          </>
+        )
       case "build-settings":
         return (
           <BuildSettingsTabStack appData={appData} setAppData={setAppData} />
@@ -328,6 +400,34 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             </Button>
           </>
         );
+      case "environment-variables":
+        return (
+          <>
+            <Text size={16}>Environment variables</Text>
+            <Spacer y={0.5} />
+            <Text color="helper">
+              Shared among all services.
+            </Text>
+            <EnvGroupArray
+              values={envVars}
+              setValues={setEnvVars}
+              fileUpload={true}
+            />
+            <Spacer y={0.5} />
+            <Button
+              onClick={() => {
+                updatePorterApp();
+              }}
+              status={updating ? "loading" : updateError ? (
+                <Error message={updateError} />
+              ) : undefined}
+              loadingText={"Updating..."}
+              width={"150px"}
+            >
+              Update app
+            </Button>
+            <Spacer y={0.5} />
+          </>);
       default:
         return <div>dream on</div>;
     }
@@ -353,7 +453,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         <StyledExpandedApp>
           <Back to="/apps" />
           <Container row>
-            {renderIcon(appData.app.build_packs)}
+            {renderIcon(appData.app?.build_packs)}
             <Text size={21}>{appData.app.name}</Text>
             {appData.app.repo_name && (
               <>
@@ -417,12 +517,13 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           <Spacer y={1} />
           <TabSelector
             options={
-              appData.app.build_packs
+              appData.app?.build_packs
                 ? [
                   { label: "Events", value: "events" },
                   { label: "Logs", value: "logs" },
                   { label: "Metrics", value: "metrics" },
                   { label: "Overview", value: "overview" },
+                  { label: "Environment variables", value: "environment-variables" },
                   { label: "Build settings", value: "build-settings" },
                   { label: "Settings", value: "settings" },
                 ]
@@ -431,6 +532,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                   { label: "Logs", value: "logs" },
                   { label: "Metrics", value: "metrics" },
                   { label: "Overview", value: "overview" },
+                  { label: "Environment variables", value: "environment-variables" },
                   { label: "Settings", value: "settings" },
                 ]
             }

+ 11 - 8
dashboard/src/main/home/app-dashboard/new-app-flow/JobTabs.tsx

@@ -21,8 +21,6 @@ const JobTabs: React.FC<Props> = ({
   const [currentTab, setCurrentTab] = React.useState<string>('main');
 
   const renderMain = () => {
-    setHeight(244);
-
     return (
       <>
         <Spacer y={1} />
@@ -47,8 +45,6 @@ const JobTabs: React.FC<Props> = ({
   };
 
   const renderResources = () => {
-    setHeight(244);
-
     return (
       <>
         <Spacer y={1} />
@@ -61,7 +57,7 @@ const JobTabs: React.FC<Props> = ({
         />
         <Spacer y={1} />
         <Input
-          label="RAM (GB)"
+          label="RAM (MB)"
           placeholder="ex: 1"
           value={service.ram}
           width="300px"
@@ -72,8 +68,6 @@ const JobTabs: React.FC<Props> = ({
   };
 
   const renderAdvanced = () => {
-    setHeight(118.5);
-
     return (
       <>
         <Spacer y={1} />
@@ -96,7 +90,16 @@ const JobTabs: React.FC<Props> = ({
           { label: 'Advanced', value: 'advanced' },
         ]}
         currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
+        setCurrentTab={(value: string) => {
+          if (value === 'main') {
+            setHeight(244);
+          } else if (value === 'resources') {
+            setHeight(244);
+          } else if (value === 'advanced') {
+            setHeight(118.5);
+          }
+          setCurrentTab(value);
+        }}
       />
       {currentTab === 'main' && renderMain()}
       {currentTab === 'resources' && renderResources()}

+ 10 - 66
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -36,9 +36,8 @@ import {
 } from "shared/types";
 import Error from "components/porter/Error";
 import { z } from "zod";
-import { AppsSchema, EnvSchema, PorterYamlSchema } from "./schema";
+import { PorterYamlSchema, createFinalPorterYaml } from "./schema";
 import { Service } from "./serviceTypes";
-import { overrideObjectValues } from "./utils";
 
 type Props = RouteComponentProps & {};
 
@@ -205,7 +204,14 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       }
 
       // validate form data
-      const finalPorterYaml = createFinalPorterYaml();
+      const finalPorterYaml = createFinalPorterYaml(
+        formState.serviceList,
+        formState.envVariables,
+        porterJson,
+        formState.applicationName,
+        currentProject.id,
+        currentCluster.id
+      );
       const yamlString = yaml.dump(finalPorterYaml);
       const base64Encoded = btoa(yamlString);
       const imageInfo = imageUrl
@@ -237,7 +243,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         }
       );
 
-      await api.updatePorterStack(
+      await api.createPorterStack(
         "<token>",
         {
           stack_name: formState.applicationName,
@@ -265,68 +271,6 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     }
   };
 
-  const combineEnv = (
-    dashboardSetVariables: KeyValueType[],
-    porterYamlSetVariables: Record<string, string> | undefined
-  ): z.infer<typeof EnvSchema> => {
-    const env: z.infer<typeof EnvSchema> = {};
-    for (const { key, value } of dashboardSetVariables) {
-      env[key] = value;
-    }
-    if (porterYamlSetVariables != null) {
-      for (const [key, value] of Object.entries(porterYamlSetVariables)) {
-        env[key] = value;
-      }
-    }
-    return env;
-  };
-
-  const createApps = (serviceList: Service[]): z.infer<typeof AppsSchema> => {
-    const apps: z.infer<typeof AppsSchema> = {};
-    for (const service of serviceList) {
-      let config = Service.serialize(service);
-      // TODO: get rid of this block when we handle ingress on the backend
-      if (Service.isWeb(service)) {
-        const ingress = Service.handleWebIngress(
-          service,
-          formState.applicationName,
-          currentCluster?.id,
-          currentProject?.id
-        );
-        config = {
-          ...config,
-          ...ingress,
-        };
-      }
-      if (
-        porterJson != null &&
-        porterJson.apps[service.name] != null &&
-        porterJson.apps[service.name].config != null
-      ) {
-        config = overrideObjectValues(
-          config,
-          porterJson.apps[service.name].config
-        );
-      }
-      // required because of https://github.com/helm/helm/issues/9214
-      apps[Service.toHelmName(service)] = {
-        type: service.type,
-        run: service.startCommand.value,
-        config,
-      };
-    }
-
-    return apps;
-  };
-
-  const createFinalPorterYaml = (): z.infer<typeof PorterYamlSchema> => {
-    return {
-      version: "v1stack",
-      env: combineEnv(formState.envVariables, porterJson?.env),
-      apps: createApps(formState.serviceList),
-    };
-  };
-
   return (
     <CenterWrapper>
       <Div>

+ 11 - 8
dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx

@@ -21,8 +21,6 @@ const WebTabs: React.FC<Props> = ({
   const [currentTab, setCurrentTab] = React.useState<string>('main');
 
   const renderMain = () => {
-    setHeight(300);
-
     return (
       <>
         <Spacer y={1} />
@@ -54,8 +52,6 @@ const WebTabs: React.FC<Props> = ({
   };
 
   const renderResources = () => {
-    setHeight(713.5);
-
     return (
       <>
         <Spacer y={1} />
@@ -68,7 +64,7 @@ const WebTabs: React.FC<Props> = ({
         />
         <Spacer y={1} />
         <Input
-          label="RAM (GB)"
+          label="RAM (MB)"
           placeholder="ex: 1"
           value={service.ram}
           width="300px"
@@ -126,8 +122,6 @@ const WebTabs: React.FC<Props> = ({
   };
 
   const renderAdvanced = () => {
-    setHeight(159);
-
     return (
       <>
         <Spacer y={1} />
@@ -151,7 +145,16 @@ const WebTabs: React.FC<Props> = ({
           { label: 'Advanced', value: 'advanced' },
         ]}
         currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
+        setCurrentTab={(value: string) => {
+          if (value === 'main') {
+            setHeight(300);
+          } else if (value === 'resources') {
+            setHeight(713.5);
+          } else if (value === 'advanced') {
+            setHeight(159);
+          }
+          setCurrentTab(value);
+        }}
       />
       {currentTab === 'main' && renderMain()}
       {currentTab === 'resources' && renderResources()}

+ 9 - 6
dashboard/src/main/home/app-dashboard/new-app-flow/WorkerTabs.tsx

@@ -21,8 +21,6 @@ const WorkerTabs: React.FC<Props> = ({
   const [currentTab, setCurrentTab] = React.useState<string>('main');
 
   const renderMain = () => {
-    setHeight(159);
-
     return (
       <>
         <Spacer y={1} />
@@ -39,8 +37,6 @@ const WorkerTabs: React.FC<Props> = ({
   };
 
   const renderResources = () => {
-    setHeight(713.5);
-
     return (
       <>
         <Spacer y={1} />
@@ -53,7 +49,7 @@ const WorkerTabs: React.FC<Props> = ({
         />
         <Spacer y={1} />
         <Input
-          label="RAM (GB)"
+          label="RAM (MB)"
           placeholder="ex: 1"
           value={service.ram}
           width="300px"
@@ -126,7 +122,14 @@ const WorkerTabs: React.FC<Props> = ({
           // { label: 'Advanced', value: 'advanced' },
         ]}
         currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
+        setCurrentTab={(value: string) => {
+          if (value === 'main') {
+            setHeight(159);
+          } else if (value === 'resources') {
+            setHeight(713.5);
+          }
+          setCurrentTab(value);
+        }}
       />
       {currentTab === 'main' && renderMain()}
       {currentTab === 'resources' && renderResources()}

+ 78 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/schema.tsx

@@ -1,4 +1,7 @@
+import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
 import * as z from "zod";
+import { Service } from "./serviceTypes";
+import { overrideObjectValues } from "./utils";
 
 const appConfigSchema = z.object({
     run: z.string().min(1),
@@ -39,3 +42,78 @@ export const PorterYamlSchema = z.object({
     apps: AppsSchema,
     release: z.string().optional(),
 });
+
+export const createFinalPorterYaml = (
+    services: Service[],
+    dashboardSetEnvVariables: KeyValueType[],
+    porterJson: z.infer<typeof PorterYamlSchema> | undefined,
+    stackName: string,
+    projectId: number,
+    clusterId: number,
+): z.infer<typeof PorterYamlSchema> => {
+    return {
+        version: "v1stack",
+        env: combineEnv(dashboardSetEnvVariables, porterJson?.env),
+        apps: createApps(services, porterJson, stackName, projectId, clusterId),
+    };
+};
+
+const combineEnv = (
+    dashboardSetVariables: KeyValueType[],
+    porterYamlSetVariables: Record<string, string> | undefined
+): z.infer<typeof EnvSchema> => {
+    const env: z.infer<typeof EnvSchema> = {};
+    for (const { key, value } of dashboardSetVariables) {
+        env[key] = value;
+    }
+    if (porterYamlSetVariables != null) {
+        for (const [key, value] of Object.entries(porterYamlSetVariables)) {
+            env[key] = value;
+        }
+    }
+    return env;
+};
+
+const createApps = (
+    serviceList: Service[],
+    porterJson: z.infer<typeof PorterYamlSchema> | undefined,
+    stackName: string,
+    projectId: number,
+    clusterId: number,
+): z.infer<typeof AppsSchema> => {
+    const apps: z.infer<typeof AppsSchema> = {};
+    for (const service of serviceList) {
+        let config = Service.serialize(service);
+        // TODO: get rid of this block when we handle ingress on the backend
+        if (Service.isWeb(service)) {
+            const ingress = Service.handleWebIngress(
+                service,
+                stackName,
+                clusterId,
+                projectId
+            );
+            config = {
+                ...config,
+                ...ingress,
+            };
+        }
+        if (
+            porterJson != null &&
+            porterJson.apps[service.name] != null &&
+            porterJson.apps[service.name].config != null
+        ) {
+            config = overrideObjectValues(
+                config,
+                porterJson.apps[service.name].config
+            );
+        }
+        // required because of https://github.com/helm/helm/issues/9214
+        apps[Service.toHelmName(service)] = {
+            type: service.type,
+            run: service.startCommand.value,
+            config,
+        };
+    }
+
+    return apps;
+};

+ 28 - 8
dashboard/src/shared/api.tsx

@@ -165,14 +165,14 @@ const createEmailVerification = baseApi<{}, {}>("POST", (pathParams) => {
 });
 
 const getPorterApps = baseApi<
-{},
-{
-  project_id: number;
-  cluster_id: number;
-}
+  {},
+  {
+    project_id: number;
+    cluster_id: number;
+  }
 >("GET", (pathParams) => {
-let { project_id, cluster_id } = pathParams;
-return `/api/projects/${project_id}/clusters/${cluster_id}/stacks`;
+  let { project_id, cluster_id } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks`;
 });
 
 const getPorterApp = baseApi<
@@ -240,7 +240,7 @@ const deletePorterApp = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${name}`;
 });
 
-const updatePorterStack = baseApi<
+const createPorterStack = baseApi<
   {
     stack_name: string;
     porter_yaml: string;
@@ -258,6 +258,25 @@ const updatePorterStack = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/stacks`;
 });
 
+const updatePorterStack = baseApi<
+  {
+    stack_name: string;
+    porter_yaml: string;
+    image_info?: {
+      repository: string;
+      tag: string;
+    }
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    stack_name: string;
+  }
+>("PATCH", (pathParams) => {
+  let { project_id, cluster_id, stack_name } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}`;
+});
+
 const createEnvironment = baseApi<
   {
     name: string;
@@ -2579,6 +2598,7 @@ export default {
   createPorterApp,
   updatePorterApp,
   deletePorterApp,
+  createPorterStack,
   updatePorterStack,
   createConfigMap,
   deleteCluster,