Browse Source

checkpoint

Ian Edwards 2 years ago
parent
commit
9d224b6191

+ 3 - 0
api/server/handlers/porter_app/validate.go

@@ -2,6 +2,7 @@ package porter_app
 
 import (
 	"encoding/base64"
+	"fmt"
 	"net/http"
 
 	"connectrpc.com/connect"
@@ -137,6 +138,8 @@ func (c *ValidatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	fmt.Printf("predeploy: %v\n", ccpResp.Msg.App.Predeploy)
+
 	encoded, err := helpers.MarshalContractObject(ctx, ccpResp.Msg.App)
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error marshalling app proto back to json")

+ 7 - 7
dashboard/src/lib/hooks/usePorterYaml.ts

@@ -19,7 +19,7 @@ type DetectedServices = {
  * added to an app by default with read-only values.
  *
  */
-export const usePorterYaml = (source: SourceOptions) => {
+export const usePorterYaml = (source: SourceOptions | null) => {
   const { currentProject, currentCluster } = useContext(Context);
   const [
     detectedServices,
@@ -30,11 +30,11 @@ export const usePorterYaml = (source: SourceOptions) => {
     [
       "getPorterYamlContents",
       currentProject?.id,
-      source.git_branch,
-      source.git_repo_name,
+      source?.git_branch,
+      source?.git_repo_name,
     ],
     async () => {
-      if (!currentProject) {
+      if (!currentProject || !source) {
         return;
       }
       if (source.type !== "github") {
@@ -59,9 +59,9 @@ export const usePorterYaml = (source: SourceOptions) => {
     },
     {
       enabled:
-        source.type === "github" &&
-        Boolean(source.git_repo_name) &&
-        Boolean(source.git_branch),
+        source?.type === "github" &&
+        Boolean(source?.git_repo_name) &&
+        Boolean(source?.git_branch),
     }
   );
 

+ 32 - 3
dashboard/src/lib/porter-apps/index.ts

@@ -219,10 +219,38 @@ const clientBuildFromProto = (proto?: Build): BuildOptions | undefined => {
     .exhaustive();
 };
 
-export function clientAppFromProto(proto: PorterApp): ClientPorterApp {
+export function clientAppFromProto(
+  proto: PorterApp,
+  overrides: {
+    services: ClientService[];
+    predeploy?: ClientService;
+  } | null
+): ClientPorterApp {
   const services = Object.entries(proto.services)
     .map(([name, service]) => serializedServiceFromProto({ name, service }))
-    .map((svc) => deserializeService(svc));
+    .map((svc) => {
+      console.log("checking for override", svc.name, overrides?.services);
+      const override = overrides?.services.find(
+        (s) => s.name.value === svc.name
+      );
+      if (override) {
+        console.log(
+          "override found for service",
+          svc.name,
+          serializeService(override)
+        );
+        console.log(
+          "deserializeService(svc, serializeService(override))",
+          deserializeService(svc, serializeService(override))
+        );
+        return deserializeService(svc, serializeService(override));
+      }
+      return deserializeService(svc);
+    });
+
+  const predeployOverrides = overrides?.predeploy
+    ? serializeService(overrides.predeploy)
+    : undefined;
 
   const predeploy = proto.predeploy
     ? deserializeService(
@@ -230,7 +258,8 @@ export function clientAppFromProto(proto: PorterApp): ClientPorterApp {
           name: "pre-deploy",
           service: proto.predeploy,
           isPredeploy: true,
-        })
+        }),
+        predeployOverrides
       )
     : undefined;
 

+ 5 - 1
dashboard/src/main/home/Home.tsx

@@ -423,7 +423,11 @@ const Home: React.FC<Props> = (props) => {
               )}
             </Route>
             <Route path="/apps/:appName/:tab">
-              <ExpandedApp />
+              {currentProject?.validate_apply_v2 ? (
+                <AppView />
+              ) : (
+                <ExpandedApp />
+              )}
             </Route>
             <Route path="/apps/:appName">
               {currentProject?.validate_apply_v2 ? (

+ 84 - 37
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -1,7 +1,7 @@
-import React, { useContext, useMemo } from "react";
+import React, { useContext, useMemo, useState } from "react";
 import { AppRevision } from "lib/revisions/types";
 import { PorterApp } from "@porter-dev/api-contracts";
-import { useForm } from "react-hook-form";
+import { FormProvider, useForm } from "react-hook-form";
 import {
   PorterAppFormData,
   SourceOptions,
@@ -13,49 +13,80 @@ import { PorterAppRecord } from "./AppView";
 import RevisionsList from "./RevisionsList";
 import { Context } from "shared/Context";
 import { useDefaultDeploymentTarget } from "lib/hooks/useDeploymentTarget";
+import Spacer from "components/porter/Spacer";
+import { usePorterYaml } from "lib/hooks/usePorterYaml";
+import TabSelector from "components/TabSelector";
+import { RouteComponentProps, useParams, withRouter } from "react-router";
+import { match } from "ts-pattern";
+import Overview from "./Overview";
+import { ClientService } from "lib/porter-apps/services";
+
+// commented out tabs are not yet implemented
+// will be included as support is available based on data from app revisions rather than helm releases
+const validTabs = [
+  // "activity",
+  // "events",
+  "overview",
+  // "logs",
+  // "metrics",
+  // "debug",
+  "environment",
+  "build-settings",
+  "settings",
+  // "helm-values",
+  // "job-history",
+] as const;
+const DEFAULT_TAB = "overview";
+type ValidTab = typeof validTabs[number];
 
 type AppDataContainerProps = {
-  latestRevision: AppRevision;
+  latestSource: SourceOptions;
+  latestRevisionNumber: number;
+  latestProto: PorterApp;
   porterApp: PorterAppRecord;
+  overrides: {
+    services: ClientService[];
+    predeploy?: ClientService;
+  } | null;
+} & RouteComponentProps;
+
+type Params = {
+  eventId?: string;
+  tab?: ValidTab;
 };
 
 const AppDataContainer: React.FC<AppDataContainerProps> = ({
-  latestRevision,
+  history,
+  latestProto,
+  latestRevisionNumber,
+  latestSource,
   porterApp,
+  overrides,
 }) => {
   const { currentProject, currentCluster } = useContext(Context);
-  const deploymentTarget = useDefaultDeploymentTarget();
+  const { tab } = useParams<Params>();
 
-  const latestProto = useMemo(
-    () => PorterApp.fromJsonString(atob(latestRevision.b64_app_proto)),
-    [latestRevision]
-  );
-  const latestSource: SourceOptions = useMemo(() => {
-    if (porterApp.image_repo_uri) {
-      const [repository, tag] = porterApp.image_repo_uri.split(":");
-      return {
-        type: "docker-registry",
-        image: {
-          repository,
-          tag,
-        },
-      };
+  const currentTab = useMemo(() => {
+    if (tab && validTabs.includes(tab)) {
+      return tab as ValidTab;
     }
 
-    return {
-      type: "github",
-      git_repo_id: porterApp.git_repo_id ?? 0,
-      git_repo_name: porterApp.repo_name ?? "",
-      git_branch: porterApp.git_branch ?? "",
-      porter_yaml_path: porterApp.porter_yaml_path ?? "./porter.yaml",
-    };
-  }, [porterApp]);
+    return DEFAULT_TAB;
+  }, [tab]);
+
+  const deploymentTarget = useDefaultDeploymentTarget();
+
+  console.log('overrides', overrides)
+  console.log(
+    "clientAppFromProto(latestProto, overrides)",
+    clientAppFromProto(latestProto, overrides)
+  );
 
   const porterAppFormMethods = useForm<PorterAppFormData>({
     reValidateMode: "onSubmit",
     resolver: zodResolver(porterAppFormValidator),
     defaultValues: {
-      app: clientAppFromProto(latestProto),
+      app: clientAppFromProto(latestProto, overrides),
       source: latestSource,
     },
   });
@@ -69,15 +100,31 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({
   }
 
   return (
-    <RevisionsList
-      latestRevisionNumber={latestRevision.revision_number}
-      deploymentTargetId={deploymentTarget?.deployment_target_id}
-      projectId={currentProject.id}
-      clusterId={currentCluster.id}
-      appName={porterApp.name}
-      sourceType={latestSource.type}
-    />
+    <FormProvider {...porterAppFormMethods}>
+      <RevisionsList
+        latestRevisionNumber={latestRevisionNumber}
+        deploymentTargetId={deploymentTarget?.deployment_target_id}
+        projectId={currentProject.id}
+        clusterId={currentCluster.id}
+        appName={porterApp.name}
+        sourceType={latestSource.type}
+      />
+      <Spacer y={1} />
+      <TabSelector
+        noBuffer
+        options={[{ label: "Overview", value: "overview" }]}
+        currentTab={currentTab}
+        setCurrentTab={() => {
+          history.push(`/apps/${porterApp.name}/${currentTab}`);
+        }}
+      />
+      <Spacer y={1} />
+      {match(currentTab)
+        .with("overview", () => <Overview />)
+        .otherwise(() => null)}
+      <Spacer y={2} />
+    </FormProvider>
   );
 };
 
-export default AppDataContainer;
+export default withRouter(AppDataContainer);

+ 34 - 20
dashboard/src/main/home/app-dashboard/app-view/AppView.tsx

@@ -25,6 +25,8 @@ import box from "assets/box.png";
 import github from "assets/github-white.png";
 import pr_icon from "assets/pull_request_icon.svg";
 import notFound from "assets/not-found.png";
+import { usePorterYaml } from "lib/hooks/usePorterYaml";
+import { SourceOptions } from "lib/porter-apps";
 
 export const porterAppValidator = z.object({
   name: z.string(),
@@ -49,24 +51,6 @@ const icons = [
   web,
 ];
 
-// commented out tabs are not yet implemented
-// will be included as support is available based on data from app revisions rather than helm releases
-const validTabs = [
-  // "activity",
-  // "events",
-  "overview",
-  // "logs",
-  // "metrics",
-  // "debug",
-  "environment",
-  "build-settings",
-  "settings",
-  // "helm-values",
-  // "job-history",
-] as const;
-const DEFAULT_TAB = "activity";
-type ValidTab = typeof validTabs[number];
-
 type Props = RouteComponentProps & {};
 
 const AppView: React.FC<Props> = ({ match }) => {
@@ -173,6 +157,30 @@ const AppView: React.FC<Props> = ({ match }) => {
         : null,
     [revision]
   );
+  const latestSource: SourceOptions | null = useMemo(() => {
+    if (!appData) {
+      return null;
+    }
+    if (appData.image_repo_uri) {
+      const [repository, tag] = appData.image_repo_uri.split(":");
+      return {
+        type: "docker-registry",
+        image: {
+          repository,
+          tag,
+        },
+      };
+    }
+
+    return {
+      type: "github",
+      git_repo_id: appData.git_repo_id ?? 0,
+      git_repo_name: appData.repo_name ?? "",
+      git_branch: appData.git_branch ?? "",
+      porter_yaml_path: appData.porter_yaml_path ?? "./porter.yaml",
+    };
+  }, [appData]);
+  const overrides = usePorterYaml(latestSource);
 
   const getIconSvg = (build: PorterApp["build"]) => {
     if (!build) {
@@ -264,8 +272,14 @@ const AppView: React.FC<Props> = ({ match }) => {
         )}
       </Container>
       <Spacer y={0.5} />
-      {appData && revision && (
-        <AppDataContainer porterApp={appData} latestRevision={revision} />
+      {appData && appProto && revision && latestSource && (
+        <AppDataContainer
+          porterApp={appData}
+          latestProto={appProto}
+          latestRevisionNumber={revision.revision_number}
+          overrides={overrides}
+          latestSource={latestSource}
+        />
       )}
     </StyledExpandedApp>
   );

+ 62 - 0
dashboard/src/main/home/app-dashboard/app-view/Overview.tsx

@@ -0,0 +1,62 @@
+import { PorterApp } from "@porter-dev/api-contracts";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import { PorterAppFormData } from "lib/porter-apps";
+import React, { useMemo } from "react";
+import { useFormContext, useFormState } from "react-hook-form";
+import ServiceList from "../validate-apply/services-settings/ServiceList";
+import {
+  defaultSerialized,
+  deserializeService,
+} from "lib/porter-apps/services";
+import Error from "components/porter/Error";
+import Button from "components/porter/Button";
+
+const Overview: React.FC = () => {
+  const { formState } = useFormContext<PorterAppFormData>();
+
+  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 (
+    <>
+      <Text size={16}>Pre-deploy job</Text>
+      <Spacer y={0.5} />
+      <ServiceList
+        limitOne={true}
+        addNewText={"Add a new pre-deploy job"}
+        prePopulateService={deserializeService(
+          defaultSerialized({
+            name: "pre-deploy",
+            type: "predeploy",
+          })
+        )}
+        isPredeploy
+      />
+      <Spacer y={0.5} />
+      <Text size={16}>Application services</Text>
+      <Spacer y={0.5} />
+      <ServiceList addNewText={"Add a new service"} defaultExpanded />
+      <Spacer y={0.75} />
+      <Button
+        type="submit"
+        status={buttonStatus}
+        loadingText={"Updating..."}
+        disabled={formState.isSubmitting || !formState.isDirty}
+      >
+        Update app
+      </Button>
+    </>
+  );
+};
+
+export default Overview;

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

@@ -139,7 +139,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
         return;
       }
 
-      await createAndApply({ app: validatedAppProto, source });
+      // await createAndApply({ app: validatedAppProto, source });
     } catch (err) {
       if (axios.isAxiosError(err) && err.response?.data?.error) {
         setDeployError(err.response?.data?.error);