Forráskód Böngészése

Moved status to outside from form flow, clean up connect external cluster and improved state restore

jnfrati 4 éve
szülő
commit
6301de9e97

+ 4 - 6
dashboard/src/main/home/onboarding/Onboarding.tsx

@@ -12,7 +12,7 @@ import { Onboarding as OnboardingSaveType } from "./types";
 const Onboarding = () => {
   const context = useContext(Context);
   const [isLoading, setIsLoading] = useState(true);
-  useSteps();
+  useSteps(isLoading);
 
   useEffect(() => {
     let unsub = devtools(OFState, "Onboarding flow state");
@@ -92,8 +92,8 @@ const Onboarding = () => {
       project_id,
       project_name,
       ...(odata || {}),
-      ...(registry_connection_data || {}),
-      ...(provision_connection_data || {}),
+      ...({ registry_connection_data } || {}),
+      ...({ provision_connection_data } || {}),
     };
   };
 
@@ -110,9 +110,7 @@ const Onboarding = () => {
   }, [context.currentProject]);
 
   return (
-    <StyledOnboarding>
-      {isLoading ? <Loading /> : <Routes />}
-    </StyledOnboarding>
+    <StyledOnboarding>{isLoading ? <Loading /> : <Routes />}</StyledOnboarding>
   );
 };
 

+ 4 - 4
dashboard/src/main/home/onboarding/components/ProviderSelector.tsx

@@ -15,17 +15,17 @@ export type ProviderSelectorProps = {
 const baseOptions = [
   {
     value: "aws",
-    icon: integrationList["aws"].icon,
+    icon: integrationList["aws"]?.icon,
     label: "Amazon Elastic Container Registry (ECR)",
   },
   {
     value: "gcp",
-    icon: integrationList["gcp"].icon,
+    icon: integrationList["gcp"]?.icon,
     label: "Google Cloud Registry (GCR)",
   },
   {
     value: "do",
-    icon: integrationList["do"].icon,
+    icon: integrationList["do"]?.icon,
     label: "DigitalOcean Container Registry (DOCR)",
   },
 ];
@@ -38,7 +38,7 @@ const skipOption = {
 
 const externalOption = {
   value: "external",
-  icon: integrationList["kubernetes"].icon,
+  icon: integrationList["kubernetes"]?.icon,
   label: "Link to an existing cluster",
 };
 

+ 50 - 21
dashboard/src/main/home/onboarding/state/StepHandler.ts

@@ -139,7 +139,7 @@ const flow: FlowType = {
         settings: {
           url: "/onboarding/provision/settings",
           on: {
-            continue: "clean_up",
+            continue: "provision_resources.status",
             go_back: "provision_resources.credentials",
           },
           execute: {
@@ -148,6 +148,13 @@ const flow: FlowType = {
             },
           },
         },
+        status: {
+          url: "/onboarding/provision/status",
+          on: {
+            continue: "clean_up",
+            go_back: "provision_resources.credentials",
+          },
+        },
       },
     },
     clean_up: {
@@ -162,12 +169,13 @@ type StepHandlerType = {
   currentStepName: string;
   currentStep: Step;
   canGoBack?: boolean;
+  isSubFlow?: boolean;
   actions: {
     nextStep: (action?: Action) => void;
     clearState: () => void;
     restoreState: (prevState: Partial<StepHandlerType>) => void;
-    getStep: (nextStepName: string) => Step;
     goTo: (step: string) => void;
+    setNewCurrentStep: (stepName: string) => { hasError: boolean };
   };
 };
 
@@ -175,6 +183,7 @@ export const StepHandler: StepHandlerType = proxy({
   flow,
   currentStepName: flow.initial,
   currentStep: flow.steps[flow.initial],
+  isSubFlow: false,
   actions: {
     nextStep: (action: Action = "continue") => {
       const cs = StepHandler.currentStep;
@@ -190,10 +199,7 @@ export const StepHandler: StepHandlerType = proxy({
           "No next step name found, fix the action triggering nextStep"
         );
       }
-      const newStep = StepHandler.actions.getStep(nextStepName);
-      StepHandler.currentStepName = nextStepName;
-      StepHandler.currentStep = newStep;
-      StepHandler.canGoBack = !!newStep?.on?.go_back;
+      StepHandler.actions.setNewCurrentStep(nextStepName);
       return;
     },
     getStep: (nextStepName: string) => {
@@ -206,23 +212,18 @@ export const StepHandler: StepHandlerType = proxy({
       if (substep) {
         nextStep = step.substeps[substep];
       }
-      return nextStep;
+      return { step: nextStep, isChild: !!substep };
     },
     goTo: (step: string) => {
-      const newStep = StepHandler.actions.getStep(step);
-      if (!newStep) {
+      const status = StepHandler.actions.setNewCurrentStep(step);
+      if (status.hasError) {
         throw new Error(
           "No next step name found, fix the action triggering nextStep"
         );
       }
-      StepHandler.currentStepName = step;
-      StepHandler.currentStep = newStep;
-      StepHandler.canGoBack = !!newStep?.on?.go_back;
-      return;
     },
     clearState: () => {
-      StepHandler.currentStepName = flow.initial;
-      StepHandler.currentStep = flow.steps[flow.initial];
+      StepHandler.actions.setNewCurrentStep(flow.initial);
     },
     restoreState: (prevState) => {
       if (
@@ -231,22 +232,50 @@ export const StepHandler: StepHandlerType = proxy({
       ) {
         return;
       }
-      StepHandler.currentStepName = prevState.currentStepName;
-      StepHandler.currentStep = StepHandler.actions.getStep(
-        prevState.currentStepName
-      );
+      const stepName = prevState.currentStepName;
+
+      StepHandler.actions.setNewCurrentStep(stepName);
+    },
+    setNewCurrentStep: (newStepName: string) => {
+      const [stepName, substep] = newStepName?.split(".");
+
+      const isChild = !!substep;
+      const step = flow.steps[stepName as Steps];
+
+      let nextStep: Step = step;
+
+      if (isChild) {
+        nextStep = step.substeps[substep];
+      }
+
+      if (!nextStep) {
+        return {
+          hasError: true,
+        };
+      }
+
+      StepHandler.currentStepName = newStepName;
+      StepHandler.currentStep = nextStep;
+      StepHandler.canGoBack = !!nextStep?.on?.go_back;
+      StepHandler.isSubFlow = isChild;
+      return {
+        hasError: false,
+      };
     },
   },
 });
 
-export const useSteps = () => {
+export const useSteps = (isParentLoading?: boolean) => {
   const snap = useSnapshot(StepHandler);
   const location = useLocation();
   const { pushFiltered } = useRouting();
   useEffect(() => {
+    if (isParentLoading) {
+      return;
+    }
     if (snap.currentStepName === "clean_up") {
       StepHandler.actions.clearState();
     }
     pushFiltered(snap.currentStep.url, ["tab"]);
-  }, [location.pathname, snap.currentStep?.url]);
+  }, [location.pathname, snap.currentStep?.url, isParentLoading]);
 };

+ 4 - 4
dashboard/src/main/home/onboarding/state/index.ts

@@ -105,17 +105,17 @@ const decompressState = (prev_state: any) => {
     skip: state.skip_registry_connection,
     provider: state.registry_connection_provider,
     credentials: {
-      id: state.registry_connection_credential_id,
+      id: state?.registry_connection_data?.id,
     },
     settings: {
-      registry_name: state.registry_connection_settings_name,
+      registry_name: state?.registry_connection_data?.name,
     },
   };
 
   if (registry.provider === "gcp") {
-    registry.settings.gcr_url = state.registry_connection_settings_url;
+    registry.settings.gcr_url = state.registry_connection_data?.url;
   } else if (registry.provider === "do") {
-    registry.settings.registry_url = state.registry_connection_settings_url;
+    registry.settings.registry_url = state.registry_connection_data?.url;
   }
 
   let provision: any = {

+ 3 - 3
dashboard/src/main/home/onboarding/steps/ConnectRegistry/ConnectRegistryWrapper.tsx

@@ -9,9 +9,9 @@ const ConnectRegistryWrapper = () => {
     <ConnectRegistry
       provider={snap.StateHandler.connected_registry?.provider}
       project={snap.StateHandler.project}
-      onSelectProvider={(provider) =>
-        provider !== "skip" && OFState.actions.nextStep("continue", provider)
-      }
+      onSelectProvider={(provider) => {
+        provider !== "skip" && OFState.actions.nextStep("continue", provider);
+      }}
       onSaveCredentials={(data) => OFState.actions.nextStep("continue", data)}
       onSaveSettings={(data) => OFState.actions.nextStep("continue", data)}
       onSuccess={() => OFState.actions.nextStep("continue")}

+ 48 - 24
dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx

@@ -10,6 +10,7 @@ import FormFlowWrapper from "./forms/FormFlow";
 import ConnectExternalCluster from "./forms/_ConnectExternalCluster";
 import { SupportedProviders } from "../../types";
 import backArrow from "assets/back_arrow.png";
+import { SharedStatus } from "./forms/Status";
 
 type Props = {
   provider: SupportedProviders | "external";
@@ -41,6 +42,52 @@ const ProvisionResources: React.FC<Props> = ({
 }) => {
   const { step } = useParams<{ step: any }>();
 
+  const Content = () => {
+    switch (step) {
+      case "credentials":
+      case "settings":
+        return (
+          <>
+            <FormFlowWrapper
+              provider={provider}
+              currentStep={step}
+              onSaveCredentials={onSaveCredentials}
+              onSaveSettings={onSaveSettings}
+              project={project}
+            />
+          </>
+        );
+      case "status":
+        return (
+          <>
+            <SharedStatus
+              project={project}
+              filter={[]}
+              nextFormStep={console.log}
+            />
+          </>
+        );
+      case "connect_own_cluster":
+        return (
+          <>
+            <ConnectExternalCluster nextStep={onSuccess} project={project} />
+          </>
+        );
+      default:
+        return (
+          <>
+            <ProviderSelector
+              selectProvider={(provider) => {
+                onSelectProvider(provider);
+              }}
+              enableSkip={false}
+              enableExternal={!shouldProvisionRegistry}
+            />
+          </>
+        );
+    }
+  };
+
   return (
     <div>
       {enable_go_back && (
@@ -58,30 +105,7 @@ const ProvisionResources: React.FC<Props> = ({
         Porter automatically creates a cluster and registry in your cloud to run
         applications.
       </Helper>
-
-      {provider ? (
-        provider !== "external" ? (
-          <FormFlowWrapper
-            provider={provider}
-            currentStep={step}
-            onSaveCredentials={onSaveCredentials}
-            onSaveSettings={onSaveSettings}
-            project={project}
-          />
-        ) : (
-          <ConnectExternalCluster nextStep={onSuccess} project={project} />
-        )
-      ) : (
-        <>
-          <ProviderSelector
-            selectProvider={(provider) => {
-              onSelectProvider(provider);
-            }}
-            enableSkip={false}
-            enableExternal={!shouldProvisionRegistry}
-          />
-        </>
-      )}
+      {Content()}
     </div>
   );
 };

+ 7 - 3
dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResourcesWrapper.tsx

@@ -10,9 +10,13 @@ const ProvisionResourcesWrapper = () => {
       shouldProvisionRegistry={snap.StateHandler.connected_registry?.skip}
       provider={snap.StateHandler.provision_resources?.provider}
       project={snap.StateHandler.project}
-      onSelectProvider={(provider: string) =>
-        provider !== "dummy" && OFState.actions.nextStep("continue", provider)
-      }
+      onSelectProvider={(provider: string) => {
+        if (provider !== "external") {
+          OFState.actions.nextStep("continue", provider);
+          return;
+        }
+        OFState.actions.nextStep("skip");
+      }}
       onSaveCredentials={(data) => OFState.actions.nextStep("continue", data)}
       onSaveSettings={(data) => OFState.actions.nextStep("continue", data)}
       onSuccess={() => OFState.actions.nextStep("continue")}

+ 3 - 9
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/FormFlow.tsx

@@ -3,43 +3,37 @@ import {
   SkipProvisionConfig,
   SupportedProviders,
 } from "main/home/onboarding/types";
-import React, { useContext, useMemo } from "react";
+import React, { useMemo } from "react";
 import styled from "styled-components";
 import Breadcrumb from "components/Breadcrumb";
 import { integrationList } from "shared/common";
 import {
   CredentialsForm as AWSCredentialsForm,
   SettingsForm as AWSSettingsForm,
-  Status as AWSProvisionerStatus,
 } from "./_AWSProvisionerForm";
 
 import {
   CredentialsForm as DOCredentialsForm,
   SettingsForm as DOSettingsForm,
-  Status as DOProvisionerStatus,
 } from "./_DOProvisionerForm";
 
 import {
   CredentialsForm as GCPCredentialsForm,
   SettingsForm as GCPSettingsForm,
-  Status as GCPProvisionerStatus,
 } from "./_GCPProvisionerForm";
 
 const Forms = {
   aws: {
     credentials: AWSCredentialsForm,
     settings: AWSSettingsForm,
-    status: AWSProvisionerStatus,
   },
   gcp: {
     credentials: GCPCredentialsForm,
     settings: GCPSettingsForm,
-    status: GCPProvisionerStatus,
   },
   do: {
     credentials: DOCredentialsForm,
     settings: DOSettingsForm,
-    status: DOProvisionerStatus,
   },
 };
 
@@ -59,14 +53,14 @@ const FormTitle = {
   external: {
     label: "Connect an existing cluster",
     icon: integrationList["kubernetes"],
-  }
+  },
 };
 
 type Props = {
   onSaveCredentials: (credentials: any) => void;
   onSaveSettings: (settings: any) => void;
   provider: SupportedProviders | "external";
-  currentStep: "credentials" | "settings" | "status";
+  currentStep: "credentials" | "settings";
   project: { id: number; name: string };
 };
 

+ 128 - 109
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/Status.tsx

@@ -1,118 +1,137 @@
-import ProvisionerStatus, { TFModule, TFResource } from "components/ProvisionerStatus";
+import ProvisionerStatus, {
+  TFModule,
+  TFResource,
+} from "components/ProvisionerStatus";
 import React, { useEffect, useState } from "react";
 import api from "shared/api";
 import { useWebsockets } from "shared/hooks/useWebsockets";
 
 export const SharedStatus: React.FC<{
-    nextFormStep: () => void;
-    project: any;
-    filter: string[];
-  }> = ({ nextFormStep, project, filter }) => {
-    const {
-      newWebsocket,
-      openWebsocket,
-      closeWebsocket,
-      closeAllWebsockets,
-    } = useWebsockets();
-  
-    const [tfModules, setTFModules] = useState<TFModule[]>([]);
-  
-    const setupInfraWebsocket = (
-      websocketID: string,
-      module: TFModule
-    ) => {
-      let apiPath = `/api/projects/${project?.id}/infras/${module.id}/logs`;
-  
-      const wsConfig = {
-        onopen: () => {
-          console.log(`connected to websocket: ${websocketID}`);
-        },
-        onmessage: (evt: MessageEvent) => {
-          console.log("EVENT IS", evt)
-        },
-  
-        onclose: () => {
-          console.log(`closing websocket: ${websocketID}`);
-        },
-  
-        onerror: (err: ErrorEvent) => {
-          console.log(err);
-          closeWebsocket(websocketID);
-        },
-      };
-  
-      newWebsocket(websocketID, apiPath, wsConfig);
-      openWebsocket(websocketID);
+  nextFormStep: () => void;
+  project: any;
+  filter: string[];
+}> = ({ nextFormStep, project, filter }) => {
+  const {
+    newWebsocket,
+    openWebsocket,
+    closeWebsocket,
+    closeAllWebsockets,
+  } = useWebsockets();
+
+  const [tfModules, setTFModules] = useState<TFModule[]>([]);
+
+  const setupInfraWebsocket = (websocketID: string, module: TFModule) => {
+    let apiPath = `/api/projects/${project?.id}/infras/${module.id}/logs`;
+
+    const wsConfig = {
+      onopen: () => {
+        console.log(`connected to websocket: ${websocketID}`);
+      },
+      onmessage: (evt: MessageEvent) => {
+        console.log("EVENT IS", evt);
+      },
+
+      onclose: () => {
+        console.log(`closing websocket: ${websocketID}`);
+      },
+
+      onerror: (err: ErrorEvent) => {
+        console.log(err);
+        closeWebsocket(websocketID);
+      },
     };
-  
-    useEffect(() => {  
-      api.getInfra("<token>", {}, { project_id: project?.id }).then((res) => {
-        var matchedInfras : Map<string, any> = new Map()
-  
-        res.data.forEach((infra : any) => {
-          if (filter.includes(infra.kind) && matchedInfras.get(infra.Kind)?.id || 0 < infra.id) {
-            matchedInfras.set(infra.kind, infra)
-          }
-        })
-  
-        var modules : TFModule[] = []
-  
-        // query for desired and current state, and convert to tf module
-        matchedInfras.forEach((infra : any) => {
-          api.getInfraDesired("<token>", {}, { project_id: project?.id, infra_id: infra?.id }).then((resDesired) => {
-            api.getInfraCurrent("<token>", {}, { project_id: project?.id, infra_id: infra?.id }).then((resCurrent) => {
-              var desired = resDesired.data
-              var current = resCurrent.data
-  
-              // convert current state to a lookup table
-              var currentMap : Map<string, string> = new Map()
-  
-              current?.resources?.forEach((val : any) => {
-                currentMap.set(val?.type + "." + val?.name, "")
-              })
-  
-              // map desired state to list of resources
-              var resources : TFResource[] = desired?.map((val : any) => {
-                return {
-                  addr: val?.addr,
-                  provisioned: currentMap.has(val?.addr),
-                  // TODO: add error types
-                  error: "",
-                }
+
+    newWebsocket(websocketID, apiPath, wsConfig);
+    openWebsocket(websocketID);
+  };
+
+  useEffect(() => {
+    api.getInfra("<token>", {}, { project_id: project?.id }).then((res) => {
+      var matchedInfras: Map<string, any> = new Map();
+
+      res.data.forEach((infra: any) => {
+        if (
+          (filter.includes(infra.kind) && matchedInfras.get(infra.Kind)?.id) ||
+          0 < infra.id
+        ) {
+          matchedInfras.set(infra.kind, infra);
+        }
+      });
+
+      var modules: TFModule[] = [];
+
+      // query for desired and current state, and convert to tf module
+      matchedInfras.forEach((infra: any) => {
+        api
+          .getInfraDesired(
+            "<token>",
+            {},
+            { project_id: project?.id, infra_id: infra?.id }
+          )
+          .then((resDesired) => {
+            api
+              .getInfraCurrent(
+                "<token>",
+                {},
+                { project_id: project?.id, infra_id: infra?.id }
+              )
+              .then((resCurrent) => {
+                var desired = resDesired.data;
+                var current = resCurrent.data;
+
+                // convert current state to a lookup table
+                var currentMap: Map<string, string> = new Map();
+
+                current?.resources?.forEach((val: any) => {
+                  currentMap.set(val?.type + "." + val?.name, "");
+                });
+
+                // map desired state to list of resources
+                var resources: TFResource[] = desired?.map((val: any) => {
+                  return {
+                    addr: val?.addr,
+                    provisioned: currentMap.has(val?.addr),
+                    // TODO: add error types
+                    error: "",
+                  };
+                });
+
+                var module: TFModule = {
+                  id: infra.id,
+                  kind: infra.kind,
+                  resources: resources,
+                };
+
+                modules.push(module);
               })
-  
-              var module : TFModule = {
-                id: infra.id,
-                kind: infra.kind,
-                resources: resources,
-              }
-  
-              modules.push(module)
-            })
+              .catch(catchError);
           })
-        });
-  
-        setTFModules(modules)
-      })
-    }, [])
-  
-    useEffect(() => {
+          .catch(catchError);
+      });
+
+      setTFModules(modules);
+    });
+  }, []);
+
+  useEffect(() => {
+    tfModules.forEach((val) => {
+      setupInfraWebsocket(val.id + "", val);
+    });
+
+    return () => {
       tfModules.forEach((val) => {
-        setupInfraWebsocket(val.id + "", val);
-      })
-  
-      return () => {
-        tfModules.forEach((val) => {
-          closeWebsocket(val.id + "");        
-        })
-      };
-    }, [tfModules]);
-  
-    return (
-      <>
-        <ProvisionerStatus 
-          modules={tfModules}
-        />
-      </>
-    );
-  };
+        closeWebsocket(val.id + "");
+      });
+    };
+  }, [tfModules]);
+
+  const catchError = (error: any) => {
+    console.error(error);
+  };
+
+  return (
+    <>
+      <ProvisionerStatus modules={tfModules} />
+    </>
+  );
+};

+ 1 - 5
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_ConnectExternalCluster.tsx

@@ -131,7 +131,7 @@ const PageCount = styled.div`
 `;
 
 const PageSection = styled.div`
-  position: absolute;
+  position: relative;
   bottom: 22px;
   right: 20px;
   display: flex;
@@ -237,11 +237,7 @@ const CloseButtonImg = styled.img`
 
 const StyledClusterInstructionsModal = styled.div`
   width: 100%;
-  position: absolute;
-  left: 0;
-  top: 0;
   height: 100%;
-  padding: 25px 32px;
   overflow: hidden;
   border-radius: 6px;
   background: #202227;