Procházet zdrojové kódy

Merge branch 'nico/new-onboarding-flow' of https://github.com/porter-dev/porter into nico/new-onboarding-flow

jusrhee před 4 roky
rodič
revize
0e57630a1f

+ 39 - 41
dashboard/src/components/ProvisionerStatus.tsx

@@ -11,24 +11,24 @@ type Props = {
 };
 };
 
 
 export interface TFModule {
 export interface TFModule {
-  id: number
-  kind: string
-  status: string
-  created_at: string
-  global_errors?: TFResourceError[]
+  id: number;
+  kind: string;
+  status: string;
+  created_at: string;
+  global_errors?: TFResourceError[];
   // optional resources, if not created
   // optional resources, if not created
-  resources?: TFResource[]
+  resources?: TFResource[];
 }
 }
 
 
 export interface TFResourceError {
 export interface TFResourceError {
-  errored_out: boolean
-  error_context?: string
+  errored_out: boolean;
+  error_context?: string;
 }
 }
 
 
 export interface TFResource {
 export interface TFResource {
-  addr: string,
-  provisioned: boolean,
-  errored: TFResourceError,
+  addr: string;
+  provisioned: boolean;
+  errored: TFResourceError;
 }
 }
 
 
 const nameMap: { [key: string]: string } = {
 const nameMap: { [key: string]: string } = {
@@ -74,8 +74,8 @@ const ProvisionerStatus: React.FC<Props> = ({ modules }) => {
   };
   };
 
 
   const renderModules = () => {
   const renderModules = () => {
-    return modules.map(val => {
-      console.log(val)
+    return modules.map((val) => {
+      console.log(val);
       const totalResources = val.resources?.length;
       const totalResources = val.resources?.length;
       const provisionedResources = val.resources?.filter((resource) => {
       const provisionedResources = val.resources?.filter((resource) => {
         return resource.provisioned;
         return resource.provisioned;
@@ -84,39 +84,43 @@ const ProvisionerStatus: React.FC<Props> = ({ modules }) => {
       let errors: string[] = [];
       let errors: string[] = [];
 
 
       if (val.status == "destroyed") {
       if (val.status == "destroyed") {
-        errors.push("Note: this infrastructure was automatically destroyed.")
+        errors.push("Note: this infrastructure was automatically destroyed.");
       }
       }
 
 
-      let hasError = val.resources?.filter((resource) => {
-        if (resource.errored?.errored_out) {
-          errors.push(resource.errored?.error_context)
-        }
+      let hasError =
+        val.resources?.filter((resource) => {
+          if (resource.errored?.errored_out) {
+            errors.push(resource.errored?.error_context);
+          }
 
 
-        return resource.errored?.errored_out
-      }).length > 0
+          return resource.errored?.errored_out;
+        }).length > 0;
 
 
       if (val.global_errors) {
       if (val.global_errors) {
         for (let globalErr of val.global_errors) {
         for (let globalErr of val.global_errors) {
-          errors.push("Global error: " + globalErr.error_context)
-          hasError = true
+          errors.push("Global error: " + globalErr.error_context);
+          hasError = true;
         }
         }
       }
       }
 
 
-      const width = 100 * (provisionedResources / (totalResources * 1.0)) || 0
+      const width =
+        val.status == "created"
+          ? 100
+          : 100 * (provisionedResources / (totalResources * 1.0)) || 0;
 
 
       let error = null;
       let error = null;
 
 
       if (hasError) {
       if (hasError) {
         error = errors.map((error, index) => {
         error = errors.map((error, index) => {
-          return <ExpandedError key={index}>{error}</ExpandedError>
-        })
-      } 
+          return <ExpandedError key={index}>{error}</ExpandedError>;
+        });
+      }
       let loadingFill;
       let loadingFill;
       let status;
       let status;
 
 
       if (hasError || val.status == "destroyed") {
       if (hasError || val.status == "destroyed") {
-        loadingFill = <LoadingFill status="error" width={width + "%"} />
-        status = renderStatus("error")
+        loadingFill = <LoadingFill status="error" width={width + "%"} />;
+        status = renderStatus("error");
       } else if (width == 100) {
       } else if (width == 100) {
         loadingFill = <LoadingFill status="successful" width={width + "%"} />;
         loadingFill = <LoadingFill status="successful" width={width + "%"} />;
         status = renderStatus("successful");
         status = renderStatus("successful");
@@ -129,28 +133,22 @@ const ProvisionerStatus: React.FC<Props> = ({ modules }) => {
         <InfraObject key={val.id}>
         <InfraObject key={val.id}>
           <InfraHeader>
           <InfraHeader>
             <Flex>
             <Flex>
-            {status}
-            {
-              integrationList[val.kind] && <Icon src={integrationList[val.kind].icon} />
-            }
-            {nameMap[val.kind]}
+              {status}
+              {integrationList[val.kind] && (
+                <Icon src={integrationList[val.kind].icon} />
+              )}
+              {nameMap[val.kind]}
             </Flex>
             </Flex>
             <Timestamp>Started {readableDate(val.created_at)}</Timestamp>
             <Timestamp>Started {readableDate(val.created_at)}</Timestamp>
           </InfraHeader>
           </InfraHeader>
           <LoadingBar>{loadingFill}</LoadingBar>
           <LoadingBar>{loadingFill}</LoadingBar>
-          <ErrorWrapper>
-            {error}
-          </ErrorWrapper>
+          <ErrorWrapper>{error}</ErrorWrapper>
         </InfraObject>
         </InfraObject>
       );
       );
     });
     });
   };
   };
 
 
-  return (
-    <StyledProvisionerStatus>
-      {renderModules()}
-    </StyledProvisionerStatus>
-  );
+  return <StyledProvisionerStatus>{renderModules()}</StyledProvisionerStatus>;
 };
 };
 
 
 export default ProvisionerStatus;
 export default ProvisionerStatus;

+ 7 - 5
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -106,11 +106,13 @@ class Dashboard extends Component<PropsType, StateType> {
 
 
   renderTabContents = () => {
   renderTabContents = () => {
     if (this.currentTab() === "provisioner") {
     if (this.currentTab() === "provisioner") {
-      return <SharedStatus 
-        filter={[]} 
-        project_id={this.props.projectId} 
-        nextFormStep={() => null} 
-      />
+      return (
+        <SharedStatus
+          filter={[]}
+          project_id={this.props.projectId}
+          setInfraStatus={(val: string) => null}
+        />
+      );
     } else if (this.currentTab() === "create-cluster") {
     } else if (this.currentTab() === "create-cluster") {
       let helperText = "Create a cluster to link to this project";
       let helperText = "Create a cluster to link to this project";
       let helperIcon = "info";
       let helperIcon = "info";

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

@@ -51,6 +51,7 @@ export const StateHandler = proxy({
   connected_source: null,
   connected_source: null,
   connected_registry: null,
   connected_registry: null,
   provision_resources: null,
   provision_resources: null,
+  current_error: null,
   actions: {
   actions: {
     restoreState: (prevState: any) => {
     restoreState: (prevState: any) => {
       StateHandler.project = prevState.project;
       StateHandler.project = prevState.project;
@@ -123,5 +124,8 @@ export const StateHandler = proxy({
     clearResourceProvisioningProvider: () => {
     clearResourceProvisioningProvider: () => {
       StateHandler.provision_resources.provider = "";
       StateHandler.provision_resources.provider = "";
     },
     },
+    saveCurrentError: (data: any) => {
+      StateHandler.current_error = data;
+    },
   },
   },
 });
 });

+ 5 - 0
dashboard/src/main/home/onboarding/state/StepHandler.ts

@@ -165,6 +165,11 @@ const flow: FlowType = {
             continue: "clean_up",
             continue: "clean_up",
             go_back: "provision_resources.credentials",
             go_back: "provision_resources.credentials",
           },
           },
+          execute: {
+            on: {
+              go_back: "saveCurrentError",
+            },
+          },
         },
         },
       },
       },
     },
     },

+ 74 - 21
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_DORegistryForm.tsx

@@ -1,5 +1,6 @@
 import Helper from "components/form-components/Helper";
 import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import InputRow from "components/form-components/InputRow";
+import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import RegistryImageList from "main/home/onboarding/components/RegistryImageList";
 import RegistryImageList from "main/home/onboarding/components/RegistryImageList";
 import { OFState } from "main/home/onboarding/state";
 import { OFState } from "main/home/onboarding/state";
@@ -11,6 +12,16 @@ import api from "shared/api";
 import styled from "styled-components";
 import styled from "styled-components";
 import { useSnapshot } from "valtio";
 import { useSnapshot } from "valtio";
 
 
+const readableDate = (s: string) => {
+  const ts = new Date(s);
+  const date = ts.toLocaleDateString();
+  const time = ts.toLocaleTimeString([], {
+    hour: "numeric",
+    minute: "2-digit",
+  });
+  return `${time} on ${date}`;
+};
+
 /**
 /**
  * This will redirect to DO, and we should pass the redirection URI to be /onboarding/registry?provider=do
  * This will redirect to DO, and we should pass the redirection URI to be /onboarding/registry?provider=do
  *
  *
@@ -23,36 +34,78 @@ export const CredentialsForm: React.FC<{
   nextFormStep: (data: Partial<DORegistryConfig>) => void;
   nextFormStep: (data: Partial<DORegistryConfig>) => void;
   project: any;
   project: any;
 }> = ({ nextFormStep, project }) => {
 }> = ({ nextFormStep, project }) => {
-  const location = useLocation();
+  const snap = useSnapshot(OFState);
+
+  const [isLoading, setIsLoading] = useState(true);
+  const [connectedAccount, setConnectedAccount] = useState(null);
+
   useEffect(() => {
   useEffect(() => {
-    api
-      .getOAuthIds("<token>", {}, { project_id: project?.id })
-      .then((res) => {
-        let tgtIntegration = res.data.find((integration: any) => {
-          return integration.client === "do";
-        });
+    api.getOAuthIds("<token>", {}, { project_id: project?.id }).then((res) => {
+      let integrations = res.data.filter((integration: any) => {
+        return integration.client === "do";
+      });
 
 
-        if (tgtIntegration) {
-          nextFormStep({
-            credentials: {
-              id: tgtIntegration.id,
-            },
-          });
+      if (Array.isArray(integrations) && integrations.length) {
+        // Sort decendant
+        integrations.sort((a, b) => b.id - a.id);
+        let lastUsed = integrations.find((i) => {
+          i.id === snap.StateHandler?.provision_resources?.credentials?.id;
+        });
+        if (!lastUsed) {
+          lastUsed = integrations[0];
         }
         }
-      })
-      .catch(console.log);
+        setConnectedAccount(lastUsed);
+      }
+      setIsLoading(false);
+    });
   }, []);
   }, []);
 
 
+  const submit = (integrationId: number) => {
+    nextFormStep({
+      credentials: {
+        id: integrationId,
+      },
+    });
+  };
+
   const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
   const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
 
 
   const encoded_redirect_uri = encodeURIComponent(url);
   const encoded_redirect_uri = encodeURIComponent(url);
+
+  if (isLoading) {
+    return <Loading />;
+  }
+
   return (
   return (
-    <ConnectDigitalOceanButton
-      target={"_blank"}
-      href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
-    >
-      Sign in to Digital Ocean
-    </ConnectDigitalOceanButton>
+    <>
+      {connectedAccount !== null && (
+        <div>
+          <div>Connected account: {connectedAccount.client}</div>
+          <div>Connected at: {readableDate(connectedAccount.created_at)}</div>
+        </div>
+      )}
+      <ConnectDigitalOceanButton
+        target={"_blank"}
+        href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
+      >
+        {connectedAccount !== null
+          ? "Connect another account"
+          : "Sign In to Digital Ocean"}
+      </ConnectDigitalOceanButton>
+
+      <Br />
+      {connectedAccount !== null && (
+        <SaveButton
+          text="Continue with connected account"
+          disabled={false}
+          onClick={() => submit(connectedAccount.id)}
+          makeFlush={true}
+          clearPosition={true}
+          status={""}
+          statusPosition={"right"}
+        />
+      )}
+    </>
   );
   );
 };
 };
 
 

+ 59 - 19
dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx

@@ -1,7 +1,7 @@
 import Helper from "components/form-components/Helper";
 import Helper from "components/form-components/Helper";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
-import React from "react";
+import React, { useState } from "react";
 import { useParams } from "react-router";
 import { useParams } from "react-router";
 import styled from "styled-components";
 import styled from "styled-components";
 import ProviderSelector, {
 import ProviderSelector, {
@@ -28,7 +28,7 @@ type Props = {
   onSaveSettings: (settings: any) => void;
   onSaveSettings: (settings: any) => void;
   onSuccess: () => void;
   onSuccess: () => void;
   onSkip: () => void;
   onSkip: () => void;
-  goBack: () => void;
+  goBack: (data?: any) => void;
 };
 };
 
 
 const ProvisionResources: React.FC<Props> = ({
 const ProvisionResources: React.FC<Props> = ({
@@ -44,6 +44,58 @@ const ProvisionResources: React.FC<Props> = ({
   goBack,
   goBack,
 }) => {
 }) => {
   const { step } = useParams<{ step: any }>();
   const { step } = useParams<{ step: any }>();
+  const [infraStatus, setInfraStatus] = useState<{
+    hasError: boolean;
+    description?: string;
+  }>(null);
+
+  const renderSaveButton = () => {
+    if (infraStatus && !infraStatus.hasError) {
+      return (
+        <>
+          <Br height="15px" />
+          <SaveButton
+            text="Continue"
+            disabled={false}
+            onClick={onSuccess}
+            makeFlush={true}
+            clearPosition={true}
+            statusPosition="right"
+            saveText=""
+          />
+        </>
+      );
+    } else if (infraStatus) {
+      return (
+        <>
+          <Br height="15px" />
+          <SaveButton
+            text="Resolve Errors"
+            status="Encountered errors while provisioning."
+            disabled={false}
+            onClick={() => goBack(infraStatus.description)}
+            makeFlush={true}
+            clearPosition={true}
+            statusPosition="right"
+            saveText=""
+          />
+        </>
+      );
+    }
+  };
+
+  const getFilterOpts = (): string[] => {
+    switch (provider) {
+      case "aws":
+        return ["eks", "ecr"];
+      case "gcp":
+        return ["gke", "gcr"];
+      case "do":
+        return ["doks", "docr"];
+    }
+
+    return [];
+  };
 
 
   const Content = () => {
   const Content = () => {
     switch (step) {
     switch (step) {
@@ -64,24 +116,12 @@ const ProvisionResources: React.FC<Props> = ({
           <>
           <>
             <SharedStatus
             <SharedStatus
               project_id={project?.id}
               project_id={project?.id}
-              filter={[]}
-              nextFormStep={onSuccess}
-              goBack={goBack}
+              filter={getFilterOpts()}
+              setInfraStatus={setInfraStatus}
             />
             />
             <Br />
             <Br />
-            <Helper>
-              Note: Provisioning can take up to 15 minutes.
-            </Helper>
-            <Br height="15px" />
-            <SaveButton
-              text="Continue"
-              disabled={false}
-              onClick={() => alert("continue")}
-              makeFlush={true}
-              clearPosition={true}
-              statusPosition="right"
-              saveText=""
-            />
+            <Helper>Note: Provisioning can take up to 15 minutes.</Helper>
+            {renderSaveButton()}
           </>
           </>
         );
         );
       case "connect_own_cluster":
       case "connect_own_cluster":
@@ -149,7 +189,7 @@ export default ProvisionResources;
 
 
 const Br = styled.div<{ height?: string }>`
 const Br = styled.div<{ height?: string }>`
   width: 100%;
   width: 100%;
-  height: ${props => props.height || "1px"};
+  height: ${(props) => props.height || "1px"};
   margin-top: -3px;
   margin-top: -3px;
 `;
 `;
 
 

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

@@ -22,7 +22,7 @@ const ProvisionResourcesWrapper = () => {
       onSuccess={() => OFState.actions.nextStep("continue")}
       onSuccess={() => OFState.actions.nextStep("continue")}
       onSkip={() => OFState.actions.nextStep("skip")}
       onSkip={() => OFState.actions.nextStep("skip")}
       enable_go_back={snap.StepHandler.canGoBack && !snap.StepHandler.isSubFlow}
       enable_go_back={snap.StepHandler.canGoBack && !snap.StepHandler.isSubFlow}
-      goBack={() => OFState.actions.nextStep("go_back")}
+      goBack={(data: any) => OFState.actions.nextStep("go_back", data)}
     />
     />
   );
   );
 };
 };

+ 150 - 68
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/SharedStatus.tsx

@@ -8,11 +8,10 @@ import api from "shared/api";
 import { useWebsockets } from "shared/hooks/useWebsockets";
 import { useWebsockets } from "shared/hooks/useWebsockets";
 
 
 export const SharedStatus: React.FC<{
 export const SharedStatus: React.FC<{
-  nextFormStep: () => void;
+  setInfraStatus: (status: { hasError: boolean; description?: string }) => void;
   project_id: number;
   project_id: number;
   filter: string[];
   filter: string[];
-  goBack?: any;
-}> = ({ nextFormStep, project_id, filter, goBack }) => {
+}> = ({ setInfraStatus, project_id, filter }) => {
   const {
   const {
     newWebsocket,
     newWebsocket,
     openWebsocket,
     openWebsocket,
@@ -48,8 +47,8 @@ export const SharedStatus: React.FC<{
     for (let addedResource of addedResources) {
     for (let addedResource of addedResources) {
       // if exists, update state to provisioned
       // if exists, update state to provisioned
       if (resourceAddrMap.has(addedResource.addr)) {
       if (resourceAddrMap.has(addedResource.addr)) {
-        let currResource = resources[resourceAddrMap.get(addedResource.addr)]
-        addedResource.errored = currResource.errored
+        let currResource = resources[resourceAddrMap.get(addedResource.addr)];
+        addedResource.errored = currResource.errored;
         resources[resourceAddrMap.get(addedResource.addr)] = addedResource;
         resources[resourceAddrMap.get(addedResource.addr)] = addedResource;
       } else {
       } else {
         resources.push(addedResource);
         resources.push(addedResource);
@@ -81,6 +80,73 @@ export const SharedStatus: React.FC<{
     setTFModules([...tfModules]);
     setTFModules([...tfModules]);
   };
   };
 
 
+  useEffect(() => {
+    // recompute tf module state each time, to see if infra is ready
+    if (tfModules.length > 0) {
+      // see if all tf modules are in a "created" state
+      if (
+        tfModules.filter((val) => val.status == "created").length ==
+        tfModules.length
+      ) {
+        setInfraStatus({
+          hasError: false,
+        });
+        return;
+      }
+
+      if (
+        tfModules.filter((val) => val.status == "error").length ==
+        tfModules.length
+      ) {
+        setInfraStatus({
+          hasError: true,
+        });
+        return;
+      }
+
+      // otherwise, check that all resources in each module are provisioned. Each module
+      // must have more than one resource
+      let numModulesSuccessful = 0;
+      let numModulesErrored = 0;
+
+      for (let tfModule of tfModules) {
+        if (tfModule.status == "created") {
+          numModulesSuccessful++;
+        } else if (tfModule.status == "error") {
+          numModulesErrored++;
+        } else {
+          let resLength = tfModule.resources?.length;
+          if (resLength > 0) {
+            numModulesSuccessful +=
+              tfModule.resources.filter((resource) => resource.provisioned)
+                .length == resLength
+                ? 1
+                : 0;
+
+            numModulesErrored +=
+              tfModule.resources.filter(
+                (resource) => resource.errored?.errored_out
+              ).length > 0
+                ? 1
+                : 0;
+          }
+        }
+      }
+
+      if (numModulesSuccessful == tfModules.length) {
+        setInfraStatus({
+          hasError: false,
+        });
+      } else if (numModulesErrored + numModulesSuccessful == tfModules.length) {
+        // otherwise, if all modules are either in an error state or successful,
+        // set the status to error
+        setInfraStatus({
+          hasError: true,
+        });
+      }
+    }
+  }, [tfModules]);
+
   const setupInfraWebsocket = (
   const setupInfraWebsocket = (
     websocketID: string,
     websocketID: string,
     module: TFModule,
     module: TFModule,
@@ -132,6 +198,10 @@ export const SharedStatus: React.FC<{
                   });
                   });
                 }
                 }
               }
               }
+            case "change_summary":
+              if (streamValData.changes.add != 0) {
+                updateDesiredState(index, module);
+              }
             default:
             default:
           }
           }
         }
         }
@@ -153,26 +223,79 @@ export const SharedStatus: React.FC<{
     openWebsocket(websocketID);
     openWebsocket(websocketID);
   };
   };
 
 
+  const mergeCurrentAndDesired = (
+    index: number,
+    desired: any,
+    currentMap: Map<string, string>
+  ) => {
+    // map desired state to list of resources
+    var addedResources: TFResource[] = desired?.map((val: any) => {
+      return {
+        addr: val?.addr,
+        provisioned: currentMap.has(val?.addr),
+        errored: {
+          errored_out: val?.errored?.errored_out,
+          error_context: val?.errored?.error_context,
+        },
+      };
+    });
+
+    updateTFModules(index, addedResources, [], []);
+  };
+
+  const updateDesiredState = (index: number, val: TFModule) => {
+    api
+      .getInfraDesired(
+        "<token>",
+        {},
+        { project_id: project_id, infra_id: val?.id }
+      )
+      .then((resDesired) => {
+        api
+          .getInfraCurrent(
+            "<token>",
+            {},
+            { project_id: project_id, infra_id: val?.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, "");
+            });
+
+            mergeCurrentAndDesired(index, desired, currentMap);
+          })
+          .catch((err) => {
+            var desired = resDesired.data;
+            var currentMap: Map<string, string> = new Map();
+
+            // merge with empty current map
+            mergeCurrentAndDesired(index, desired, currentMap);
+          });
+      })
+      .catch((err) => console.log(err));
+  };
+
   useEffect(() => {
   useEffect(() => {
     api.getInfra("<token>", {}, { project_id: project_id }).then((res) => {
     api.getInfra("<token>", {}, { project_id: project_id }).then((res) => {
-      var matchedInfras : Map<string, any> = new Map()
-        var numCreated = 0
-  
-        res.data.forEach((infra : any) => {
-          // if filter list is empty, add infra automatically
-          if (filter.length == 0) {
-            matchedInfras.set(infra.kind + "-" + infra.id, infra)
-          } else if (filter.includes(infra.kind) && matchedInfras.get(infra.Kind)?.id || 0 < infra.id) {
-            matchedInfras.set(infra.kind, infra)
-          }
-
-          numCreated += infra?.status == "created" ? 1 : 0
-        })
-          
-        // if all created, call next form step
-        if (numCreated == res.data.length) {
-          nextFormStep()
+      var matchedInfras: Map<string, any> = new Map();
+
+      res.data.forEach((infra: any) => {
+        // if filter list is empty, add infra automatically
+        if (filter.length == 0) {
+          matchedInfras.set(infra.kind + "-" + infra.id, infra);
+        } else if (
+          (filter.includes(infra.kind) && matchedInfras.get(infra.Kind)?.id) ||
+          0 < infra.id
+        ) {
+          matchedInfras.set(infra.kind, infra);
         }
         }
+      });
 
 
       // query for desired and current state, and convert to tf module
       // query for desired and current state, and convert to tf module
       matchedInfras.forEach((infra: any) => {
       matchedInfras.forEach((infra: any) => {
@@ -190,59 +313,18 @@ export const SharedStatus: React.FC<{
 
 
       tfModules.forEach((val, index) => {
       tfModules.forEach((val, index) => {
         if (val?.status != "created" && val?.status != "destroyed") {
         if (val?.status != "created" && val?.status != "destroyed") {
-          api
-            .getInfraDesired(
-              "<token>",
-              {},
-              { project_id: project_id, infra_id: val?.id }
-            )
-            .then((resDesired) => {
-              api
-                .getInfraCurrent(
-                  "<token>",
-                  {},
-                  { project_id: project_id, infra_id: val?.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 addedResources: TFResource[] = desired?.map((val: any) => {
-                    return {
-                      addr: val?.addr,
-                      provisioned: currentMap.has(val?.addr),
-                      errored: {
-                        errored_out: val?.errored?.errored_out,
-                        error_context: val?.errored?.error_context,
-                      },
-                    };
-                  });
-
-                  updateTFModules(index, addedResources, [], [])
-                })
-                .catch((err) => console.log(err));
-            })
-            .catch((err) => console.log(err));
+          updateDesiredState(index, val);
+          setupInfraWebsocket(val.id + "", val, index);
         }
         }
-      })
-
-      tfModules.forEach((val, index) => {
-        setupInfraWebsocket(val.id + "", val, index);
       });
       });
     });
     });
 
 
     return closeAllWebsockets;
     return closeAllWebsockets;
   }, []);
   }, []);
 
 
-  let sortedModules = tfModules.sort((a, b) => b.id < a.id ? -1 : b.id > a.id ? 1 : 0)
+  let sortedModules = tfModules.sort((a, b) =>
+    b.id < a.id ? -1 : b.id > a.id ? 1 : 0
+  );
 
 
   return (
   return (
     <>
     <>

+ 35 - 15
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_AWSProvisionerForm.tsx

@@ -177,6 +177,22 @@ export const SettingsForm: React.FC<{
     console.error(error);
     console.error(error);
   };
   };
 
 
+  const hasRegistryProvisioned = (
+    infras: { kind: string; status: string }[]
+  ) => {
+    return !!infras.find(
+      (i) => ["docr", "gcr", "ecr"].includes(i.kind) && i.status === "created"
+    );
+  };
+
+  const hasClusterProvisioned = (
+    infras: { kind: string; status: string }[]
+  ) => {
+    return !!infras.find(
+      (i) => ["doks", "gks", "eks"].includes(i.kind) && i.status === "created"
+    );
+  };
+
   const provisionECR = async (awsIntegrationId: number) => {
   const provisionECR = async (awsIntegrationId: number) => {
     console.log("Started provision ECR");
     console.log("Started provision ECR");
 
 
@@ -222,16 +238,33 @@ export const SettingsForm: React.FC<{
       setButtonStatus(validation.error);
       setButtonStatus(validation.error);
       return;
       return;
     }
     }
+
+    let infras = [];
+
+    try {
+      infras = await api
+        .getInfra("<token>", {}, { project_id: project?.id })
+        .then((res) => res?.data);
+    } catch (error) {
+      setButtonStatus("Something went wrong, try again later");
+      return;
+    }
+
     const integrationId = snap.StateHandler.provision_resources.credentials.id;
     const integrationId = snap.StateHandler.provision_resources.credentials.id;
+
     let registryProvisionResponse = null;
     let registryProvisionResponse = null;
     let clusterProvisionResponse = null;
     let clusterProvisionResponse = null;
 
 
     const shouldProvisionECR = snap.StateHandler.connected_registry.skip;
     const shouldProvisionECR = snap.StateHandler.connected_registry.skip;
 
 
     if (shouldProvisionECR) {
     if (shouldProvisionECR) {
-      registryProvisionResponse = await provisionECR(integrationId);
+      if (!hasRegistryProvisioned(infras)) {
+        registryProvisionResponse = await provisionECR(integrationId);
+      }
+    }
+    if (!hasClusterProvisioned(infras)) {
+      clusterProvisionResponse = await provisionEKS(integrationId);
     }
     }
-    clusterProvisionResponse = await provisionEKS(integrationId);
 
 
     nextFormStep({
     nextFormStep({
       settings: {
       settings: {
@@ -279,19 +312,6 @@ export const SettingsForm: React.FC<{
   );
   );
 };
 };
 
 
-export const Status: React.FC<{
-  nextFormStep: () => void;
-  project: any;
-}> = ({ nextFormStep, project }) => {
-  return (
-    <SharedStatus
-      nextFormStep={nextFormStep}
-      project_id={project?.id}
-      filter={["eks", "ecr"]}
-    />
-  );
-};
-
 const Br = styled.div`
 const Br = styled.div`
   width: 100%;
   width: 100%;
   height: 15px;
   height: 15px;

+ 99 - 26
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_DOProvisionerForm.tsx

@@ -14,6 +14,7 @@ import styled from "styled-components";
 import { useSnapshot } from "valtio";
 import { useSnapshot } from "valtio";
 import { useWebsockets } from "shared/hooks/useWebsockets";
 import { useWebsockets } from "shared/hooks/useWebsockets";
 import { SharedStatus } from "./SharedStatus";
 import { SharedStatus } from "./SharedStatus";
+import Loading from "components/Loading";
 
 
 const tierOptions = [
 const tierOptions = [
   { value: "basic", label: "Basic" },
   { value: "basic", label: "Basic" },
@@ -33,6 +34,16 @@ const regionOptions = [
   { value: "tor1", label: "Toronto 1" },
   { value: "tor1", label: "Toronto 1" },
 ];
 ];
 
 
+const readableDate = (s: string) => {
+  const ts = new Date(s);
+  const date = ts.toLocaleDateString();
+  const time = ts.toLocaleTimeString([], {
+    hour: "numeric",
+    minute: "2-digit",
+  });
+  return `${time} on ${date}`;
+};
+
 /**
 /**
  * This will redirect to DO, and we should pass the redirection URI to be /onboarding/provision?provider=do
  * This will redirect to DO, and we should pass the redirection URI to be /onboarding/provision?provider=do
  *
  *
@@ -45,34 +56,77 @@ export const CredentialsForm: React.FC<{
   nextFormStep: (data: Partial<DOProvisionerConfig>) => void;
   nextFormStep: (data: Partial<DOProvisionerConfig>) => void;
   project: any;
   project: any;
 }> = ({ nextFormStep, project }) => {
 }> = ({ nextFormStep, project }) => {
+  const snap = useSnapshot(OFState);
+
+  const [isLoading, setIsLoading] = useState(true);
+  const [connectedAccount, setConnectedAccount] = useState(null);
+
   useEffect(() => {
   useEffect(() => {
     api.getOAuthIds("<token>", {}, { project_id: project?.id }).then((res) => {
     api.getOAuthIds("<token>", {}, { project_id: project?.id }).then((res) => {
-      let tgtIntegration = res.data.find((integration: any) => {
+      let integrations = res.data.filter((integration: any) => {
         return integration.client === "do";
         return integration.client === "do";
       });
       });
 
 
-      if (tgtIntegration) {
-        nextFormStep({
-          credentials: {
-            id: tgtIntegration.id,
-          },
+      if (Array.isArray(integrations) && integrations.length) {
+        // Sort decendant
+        integrations.sort((a, b) => b.id - a.id);
+        let lastUsed = integrations.find((i) => {
+          i.id === snap.StateHandler?.provision_resources?.credentials?.id;
         });
         });
+        if (!lastUsed) {
+          lastUsed = integrations[0];
+        }
+        setConnectedAccount(lastUsed);
       }
       }
+      setIsLoading(false);
     });
     });
   }, []);
   }, []);
 
 
+  const submit = (integrationId: number) => {
+    nextFormStep({
+      credentials: {
+        id: integrationId,
+      },
+    });
+  };
+
   const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
   const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
 
 
   const encoded_redirect_uri = encodeURIComponent(url);
   const encoded_redirect_uri = encodeURIComponent(url);
 
 
+  if (isLoading) {
+    return <Loading />;
+  }
+
   return (
   return (
     <>
     <>
+      {connectedAccount !== null && (
+        <div>
+          <div>Connected account: {connectedAccount.client}</div>
+          <div>Connected at: {readableDate(connectedAccount.created_at)}</div>
+        </div>
+      )}
       <ConnectDigitalOceanButton
       <ConnectDigitalOceanButton
         target={"_blank"}
         target={"_blank"}
         href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
         href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
       >
       >
-        Sign In to Digital Ocean
+        {connectedAccount !== null
+          ? "Connect another account"
+          : "Sign In to Digital Ocean"}
       </ConnectDigitalOceanButton>
       </ConnectDigitalOceanButton>
+
+      <Br />
+      {connectedAccount !== null && (
+        <SaveButton
+          text="Continue with connected account"
+          disabled={false}
+          onClick={() => submit(connectedAccount.id)}
+          makeFlush={true}
+          clearPosition={true}
+          status={""}
+          statusPosition={"right"}
+        />
+      )}
     </>
     </>
   );
   );
 };
 };
@@ -110,6 +164,22 @@ export const SettingsForm: React.FC<{
     console.error(error);
     console.error(error);
   };
   };
 
 
+  const hasRegistryProvisioned = (
+    infras: { kind: string; status: string }[]
+  ) => {
+    return !!infras.find(
+      (i) => ["docr", "gcr", "ecr"].includes(i.kind) && i.status === "created"
+    );
+  };
+
+  const hasClusterProvisioned = (
+    infras: { kind: string; status: string }[]
+  ) => {
+    return !!infras.find(
+      (i) => ["doks", "gks", "eks"].includes(i.kind) && i.status === "created"
+    );
+  };
+
   const provisionDOCR = async (integrationId: number, tier: string) => {
   const provisionDOCR = async (integrationId: number, tier: string) => {
     console.log("Provisioning DOCR...");
     console.log("Provisioning DOCR...");
     try {
     try {
@@ -165,18 +235,34 @@ export const SettingsForm: React.FC<{
       setButtonStatus(validation.error);
       setButtonStatus(validation.error);
       return;
       return;
     }
     }
+
+    let infras = [];
+    try {
+      infras = await api
+        .getInfra("<token>", {}, { project_id: project?.id })
+        .then((res) => res?.data);
+    } catch (error) {
+      setButtonStatus("Something went wrong, try again later");
+      return;
+    }
+
     const integrationId = snap.StateHandler.provision_resources.credentials.id;
     const integrationId = snap.StateHandler.provision_resources.credentials.id;
     let registryProvisionResponse = null;
     let registryProvisionResponse = null;
     let clusterProvisionResponse = null;
     let clusterProvisionResponse = null;
 
 
     if (snap.StateHandler.connected_registry.skip) {
     if (snap.StateHandler.connected_registry.skip) {
-      registryProvisionResponse = await provisionDOCR(integrationId, tier);
+      if (!hasRegistryProvisioned(infras)) {
+        registryProvisionResponse = await provisionDOCR(integrationId, tier);
+      }
+    }
+
+    if (!hasClusterProvisioned(infras)) {
+      clusterProvisionResponse = await provisionDOKS(
+        integrationId,
+        region,
+        clusterName
+      );
     }
     }
-    clusterProvisionResponse = await provisionDOKS(
-      integrationId,
-      region,
-      clusterName
-    );
 
 
     nextFormStep({
     nextFormStep({
       settings: {
       settings: {
@@ -235,19 +321,6 @@ export const SettingsForm: React.FC<{
   );
   );
 };
 };
 
 
-export const Status: React.FC<{
-  nextFormStep: () => void;
-  project: any;
-}> = ({ nextFormStep, project }) => {
-  return (
-    <SharedStatus
-      nextFormStep={nextFormStep}
-      project_id={project?.id}
-      filter={["doks", "docr"]}
-    />
-  );
-};
-
 const Br = styled.div`
 const Br = styled.div`
   width: 100%;
   width: 100%;
   height: 15px;
   height: 15px;

+ 51 - 28
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx

@@ -161,6 +161,22 @@ export const SettingsForm: React.FC<{
     return { hasError: false, error: "" };
     return { hasError: false, error: "" };
   };
   };
 
 
+  const hasRegistryProvisioned = (
+    infras: { kind: string; status: string }[]
+  ) => {
+    return !!infras.find(
+      (i) => ["docr", "gcr", "ecr"].includes(i.kind) && i.status === "created"
+    );
+  };
+
+  const hasClusterProvisioned = (
+    infras: { kind: string; status: string }[]
+  ) => {
+    return !!infras.find(
+      (i) => ["doks", "gks", "eks"].includes(i.kind) && i.status === "created"
+    );
+  };
+
   const catchError = (error: any) => {
   const catchError = (error: any) => {
     console.error(error);
     console.error(error);
   };
   };
@@ -168,20 +184,36 @@ export const SettingsForm: React.FC<{
   const submit = async () => {
   const submit = async () => {
     const validation = validate();
     const validation = validate();
 
 
+    setButtonStatus("loading");
+
     if (validation.hasError) {
     if (validation.hasError) {
       setButtonStatus(validation.error);
       setButtonStatus(validation.error);
       return;
       return;
     }
     }
 
 
-    setButtonStatus("loading");
+    let infras = [];
+
+    try {
+      infras = await api
+        .getInfra("<token>", {}, { project_id: project?.id })
+        .then((res) => res?.data);
+    } catch (error) {
+      setButtonStatus("Something went wrong, try again later");
+      return;
+    }
+
     const integrationId = snap.StateHandler.provision_resources.credentials.id;
     const integrationId = snap.StateHandler.provision_resources.credentials.id;
 
 
     let registryProvisionResponse = null;
     let registryProvisionResponse = null;
     let clusterProvisionResponse = null;
     let clusterProvisionResponse = null;
     if (snap.StateHandler.connected_registry.skip) {
     if (snap.StateHandler.connected_registry.skip) {
-      registryProvisionResponse = await provisionGCR(integrationId);
+      if (!hasRegistryProvisioned(infras)) {
+        registryProvisionResponse = await provisionGCR(integrationId);
+      }
+    }
+    if (!hasClusterProvisioned(infras)) {
+      clusterProvisionResponse = await provisionGKE(integrationId);
     }
     }
-    clusterProvisionResponse = await provisionGKE(integrationId);
 
 
     nextFormStep({
     nextFormStep({
       settings: {
       settings: {
@@ -192,27 +224,29 @@ export const SettingsForm: React.FC<{
     });
     });
   };
   };
 
 
-  const provisionGCR = (id: number) => {
+  const provisionGCR = async (id: number) => {
     console.log("Provisioning GCR");
     console.log("Provisioning GCR");
 
 
-    return api
-      .createGCR(
+    try {
+      const res = await api.createGCR(
         "<token>",
         "<token>",
         {
         {
           gcp_integration_id: id,
           gcp_integration_id: id,
           issuer_email: snap.StateHandler.user_email,
           issuer_email: snap.StateHandler.user_email,
         },
         },
         { project_id: project.id }
         { project_id: project.id }
-      )
-      .then((res) => res?.data)
-      .catch(catchError);
+      );
+      return res?.data;
+    } catch (error) {
+      return catchError(error);
+    }
   };
   };
 
 
-  const provisionGKE = (id: number) => {
+  const provisionGKE = async (id: number) => {
     console.log("Provisioning GKE");
     console.log("Provisioning GKE");
 
 
-    return api
-      .createGKE(
+    try {
+      const res = await api.createGKE(
         "<token>",
         "<token>",
         {
         {
           gke_name: clusterName,
           gke_name: clusterName,
@@ -220,9 +254,11 @@ export const SettingsForm: React.FC<{
           issuer_email: snap.StateHandler.user_email,
           issuer_email: snap.StateHandler.user_email,
         },
         },
         { project_id: project.id }
         { project_id: project.id }
-      )
-      .then((res) => res?.data)
-      .catch(catchError);
+      );
+      return res?.data;
+    } catch (error) {
+      return catchError(error);
+    }
   };
   };
 
 
   return (
   return (
@@ -252,19 +288,6 @@ export const SettingsForm: React.FC<{
   );
   );
 };
 };
 
 
-export const Status: React.FC<{
-  nextFormStep: () => void;
-  project: any;
-}> = ({ nextFormStep, project }) => {
-  return (
-    <SharedStatus
-      nextFormStep={nextFormStep}
-      project_id={project?.id}
-      filter={["gke", "gcr"]}
-    />
-  );
-};
-
 const Br = styled.div`
 const Br = styled.div`
   width: 100%;
   width: 100%;
   height: 15px;
   height: 15px;

+ 3 - 3
dashboard/src/main/home/onboarding/types.ts

@@ -13,7 +13,7 @@ export type AWSRegistryConfig = {
   skip: false;
   skip: false;
   provider: "aws";
   provider: "aws";
   credentials: {
   credentials: {
-    id: string;
+    id: number;
   };
   };
   settings: {
   settings: {
     registry_connection_id: number;
     registry_connection_id: number;
@@ -25,7 +25,7 @@ export type GCPRegistryConfig = {
   skip: false;
   skip: false;
   provider: "gcp";
   provider: "gcp";
   credentials: {
   credentials: {
-    id: string;
+    id: number;
   };
   };
   settings: {
   settings: {
     registry_connection_id: number;
     registry_connection_id: number;
@@ -38,7 +38,7 @@ export type DORegistryConfig = {
   skip: false;
   skip: false;
   provider: "do";
   provider: "do";
   credentials: {
   credentials: {
-    id: string;
+    id: number;
   };
   };
   settings: {
   settings: {
     registry_connection_id: number;
     registry_connection_id: number;

+ 4 - 0
internal/kubernetes/provisioner/global_stream.go

@@ -106,9 +106,13 @@ func GlobalStreamListener(
 
 
 		// parse messages from the global stream
 		// parse messages from the global stream
 		for _, msg := range xstreams[0].Messages {
 		for _, msg := range xstreams[0].Messages {
+			fmt.Println("GOT REDIS GLOBAL MSG", msg, msg.Values["id"], msg.Values["status"])
+
 			// parse the id to identify the infra
 			// parse the id to identify the infra
 			kind, projID, infraID, err := models.ParseUniqueName(fmt.Sprintf("%v", msg.Values["id"]))
 			kind, projID, infraID, err := models.ParseUniqueName(fmt.Sprintf("%v", msg.Values["id"]))
 
 
+			fmt.Println("PARSED DATA IS", kind, projID, infraID, err)
+
 			if fmt.Sprintf("%v", msg.Values["status"]) == "created" {
 			if fmt.Sprintf("%v", msg.Values["status"]) == "created" {
 				infra, err := repo.Infra().ReadInfra(projID, infraID)
 				infra, err := repo.Infra().ReadInfra(projID, infraID)