Răsfoiți Sursa

add usePorterYaml and app to proto conversion (#3409)

ianedwards 2 ani în urmă
părinte
comite
13609cb496

+ 127 - 0
dashboard/src/lib/hooks/usePorterYaml.ts

@@ -0,0 +1,127 @@
+import { PorterApp } from "@porter-dev/api-contracts";
+import { useQuery } from "@tanstack/react-query";
+import { SourceOptions, defaultServicesWithOverrides } from "lib/porter-apps";
+import { ClientService } from "lib/porter-apps/services";
+import { useCallback, useContext, useEffect, useState } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { z } from "zod";
+
+type DetectedServices = {
+  services: ClientService[];
+  predeploy?: ClientService;
+};
+
+/*
+ *
+ * usePorterYaml is a hook that will fetch the porter.yaml file from the
+ * specified source and parse it to determine the services that should be
+ * added to an app by default with read-only values.
+ *
+ */
+export const usePorterYaml = (source: SourceOptions) => {
+  const { currentProject, currentCluster } = useContext(Context);
+  const [
+    detectedServices,
+    setDetectedServices,
+  ] = useState<DetectedServices | null>(null);
+
+  const { data } = useQuery(
+    [
+      "getPorterYamlContents",
+      currentProject?.id,
+      source.git_branch,
+      source.git_repo_name,
+    ],
+    async () => {
+      if (!currentProject) {
+        return;
+      }
+      if (source.type !== "github") {
+        return;
+      }
+      const res = await api.getPorterYamlContents(
+        "<token>",
+        {
+          path: source.porter_yaml_path,
+        },
+        {
+          project_id: currentProject.id,
+          git_repo_id: source.git_repo_id,
+          kind: "github",
+          owner: source.git_repo_name.split("/")[0],
+          name: source.git_repo_name.split("/")[1],
+          branch: source.git_branch,
+        }
+      );
+
+      return z.string().parseAsync(res.data);
+    },
+    {
+      enabled:
+        source.type === "github" &&
+        Boolean(source.git_repo_name) &&
+        Boolean(source.git_branch),
+    }
+  );
+
+  const detectServices = useCallback(
+    async ({
+      b64Yaml,
+      projectId,
+      clusterId,
+    }: {
+      b64Yaml: string;
+      projectId: number;
+      clusterId: number;
+    }) => {
+      try {
+        const res = await api.parsePorterYaml(
+          "<token>",
+          { b64_yaml: b64Yaml },
+          {
+            project_id: projectId,
+            cluster_id: clusterId,
+          }
+        );
+
+        const data = await z
+          .object({
+            b64_app_proto: z.string(),
+          })
+          .parseAsync(res.data);
+        const proto = PorterApp.fromJsonString(atob(data.b64_app_proto));
+
+        const { services, predeploy } = defaultServicesWithOverrides({
+          overrides: proto,
+        });
+
+        if (services.length || predeploy) {
+          setDetectedServices({
+            services,
+            predeploy,
+          });
+        }
+      } catch (err) {
+        // silent failure for now
+      }
+    },
+    []
+  );
+
+  useEffect(() => {
+    if (!currentProject || !currentCluster) {
+      return;
+    }
+
+    if (data) {
+      detectServices({
+        b64Yaml: data,
+        projectId: currentProject.id,
+        clusterId: currentCluster.id,
+      });
+    }
+  }, [data]);
+
+  return detectedServices;
+};

+ 39 - 1
dashboard/src/lib/porter-apps/index.ts

@@ -4,10 +4,13 @@ import {
   ClientService,
   defaultSerialized,
   deserializeService,
+  isPredeployService,
+  serializeService,
   serializedServiceFromProto,
+  serviceProto,
   serviceValidator,
 } from "./services";
-import { PorterApp } from "@porter-dev/api-contracts";
+import { PorterApp, Service } from "@porter-dev/api-contracts";
 
 // buildValidator is used to validate inputs for build setting fields
 export const buildValidator = z.object({
@@ -102,3 +105,38 @@ export function defaultServicesWithOverrides({
     predeploy,
   };
 }
+
+export function clientAppToProto(app: ClientPorterApp): PorterApp {
+  const services = app.services
+    .filter((s) => !isPredeployService(s))
+    .reduce((acc: Record<string, Service>, svc) => {
+      acc[svc.name.value] = serviceProto(serializeService(svc));
+      return acc;
+    }, {});
+
+  const predeploy = app.services.find((s) => isPredeployService(s));
+
+  const proto = new PorterApp({
+    name: app.name,
+    services,
+    env: app.env,
+    build: {
+      context: app.build.context,
+      method: app.build.method,
+      buildpacks: app.build.buildpacks.map((b) => b.buildpack),
+      builder: app.build.builder,
+      dockerfile: app.build.dockerfile,
+    },
+    ...(app.image && {
+      image: {
+        repository: app.image.repository,
+        tag: app.image.tag,
+      },
+    }),
+    ...(predeploy && {
+      predeploy: serviceProto(serializeService(predeploy)),
+    }),
+  });
+
+  return proto;
+}

+ 14 - 108
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback, useContext, useEffect, useMemo } from "react";
+import React, { useContext, useEffect } from "react";
 import { RouteComponentProps, withRouter } from "react-router";
 import web from "assets/web.png";
 import AnimateHeight from "react-animate-height";
@@ -11,13 +11,9 @@ import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import { ControlledInput } from "components/porter/ControlledInput";
 import Link from "components/porter/Link";
-import EnvGroupArrayStacks from "main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks";
 
 import { Context } from "shared/Context";
-import {
-  PorterAppFormData,
-  defaultServicesWithOverrides,
-} from "lib/porter-apps";
+import { PorterAppFormData } from "lib/porter-apps";
 import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader";
 import SourceSelector from "../new-app-flow/SourceSelector";
 import Button from "components/porter/Button";
@@ -25,20 +21,19 @@ import RepoSettings from "./RepoSettings";
 import ImageSettings from "./ImageSettings";
 import Container from "components/porter/Container";
 import ServiceList from "../validate-apply/services-settings/ServiceList";
-import { useQuery } from "@tanstack/react-query";
-import api from "shared/api";
-import { z } from "zod";
-import { PorterApp } from "@porter-dev/api-contracts";
 import {
+  ClientService,
   defaultSerialized,
   deserializeService,
 } from "lib/porter-apps/services";
 import EnvVariables from "../validate-apply/app-settings/EnvVariables";
+import { usePorterYaml } from "lib/hooks/usePorterYaml";
+import { valueExists } from "shared/util";
 
 type CreateAppProps = {} & RouteComponentProps;
 
 const CreateApp: React.FC<CreateAppProps> = ({}) => {
-  const { currentProject, currentCluster } = useContext(Context);
+  const { currentProject } = useContext(Context);
   const [step, setStep] = React.useState(0);
   const [detectedServices, setDetectedServices] = React.useState<{
     detected: boolean;
@@ -77,93 +72,7 @@ const CreateApp: React.FC<CreateAppProps> = ({}) => {
   const source = watch("source");
   const build = watch("app.build");
   const image = watch("app.image");
-
-  const { data } = useQuery(
-    [
-      "getPorterYamlContents",
-      currentProject?.id,
-      source.git_branch,
-      source.git_repo_name,
-    ],
-    async () => {
-      if (!currentProject) {
-        return;
-      }
-      if (source.type !== "github") {
-        return;
-      }
-      const res = await api.getPorterYamlContents(
-        "<token>",
-        {
-          path: source.porter_yaml_path,
-        },
-        {
-          project_id: currentProject.id,
-          git_repo_id: source.git_repo_id,
-          kind: "github",
-          owner: source.git_repo_name.split("/")[0],
-          name: source.git_repo_name.split("/")[1],
-          branch: source.git_branch,
-        }
-      );
-
-      return z.string().parseAsync(res.data);
-    },
-    {
-      enabled:
-        source.type === "github" &&
-        Boolean(source.git_repo_name) &&
-        Boolean(source.git_branch),
-    }
-  );
-
-  const detectServices = useCallback(
-    async ({
-      b64Yaml,
-      projectId,
-      clusterId,
-    }: {
-      b64Yaml: string;
-      projectId: number;
-      clusterId: number;
-    }) => {
-      try {
-        const res = await api.parsePorterYaml(
-          "<token>",
-          { b64_yaml: b64Yaml },
-          {
-            project_id: projectId,
-            cluster_id: clusterId,
-          }
-        );
-
-        const data = await z
-          .object({
-            b64_app_proto: z.string(),
-          })
-          .parseAsync(res.data);
-        const proto = PorterApp.fromJsonString(atob(data.b64_app_proto));
-
-        const { services, predeploy } = defaultServicesWithOverrides({
-          overrides: proto,
-        });
-
-        if (services.length) {
-          const defaultServices = predeploy
-            ? [...services, predeploy]
-            : services;
-          setValue("app.services", defaultServices);
-          setDetectedServices({
-            detected: true,
-            count: services.length,
-          });
-        }
-      } catch (err) {
-        // silent failure for now
-      }
-    },
-    []
-  );
+  const servicesFromYaml = usePorterYaml(source);
 
   useEffect(() => {
     // set step to 1 if name is filled out
@@ -202,18 +111,15 @@ const CreateApp: React.FC<CreateAppProps> = ({}) => {
   }, [source?.type, source?.git_repo_name, source?.git_branch, image?.tag]);
 
   useEffect(() => {
-    if (!currentProject || !currentCluster) {
-      return;
-    }
-
-    if (data) {
-      detectServices({
-        b64Yaml: data,
-        projectId: currentProject.id,
-        clusterId: currentCluster.id,
+    if (servicesFromYaml && !detectedServices.detected) {
+      const { services, predeploy } = servicesFromYaml;
+      setValue("app.services", [...services, predeploy].filter(valueExists));
+      setDetectedServices({
+        detected: true,
+        count: services.length,
       });
     }
-  }, [data]);
+  }, [servicesFromYaml, detectedServices.detected]);
 
   if (!currentProject) {
     return null;

+ 4 - 0
dashboard/src/shared/util.ts

@@ -6,3 +6,7 @@ export const isJSON = (value: string): boolean => {
     return false;
   }
 };
+
+export function valueExists<T>(value: T | null | undefined): value is T {
+  return !!value;
+}