Просмотр исходного кода

Merge pull request #1412 from porter-dev/nico/por-173-reorganize-api-calls-for-sharedstatus

[POR-173] Reorganize api calls for SharedStatus
Nicolas Frati 4 лет назад
Родитель
Сommit
b57629022d

+ 13 - 13
dashboard/src/components/ProvisionerStatus.tsx

@@ -4,7 +4,7 @@ import { integrationList } from "shared/common";
 
 
 import loading from "assets/loading.gif";
 import loading from "assets/loading.gif";
 
 
-import styled from "styled-components";
+import styled, { keyframes } from "styled-components";
 
 
 type Props = {
 type Props = {
   modules: TFModule[];
   modules: TFModule[];
@@ -22,7 +22,7 @@ export interface TFModule {
 }
 }
 
 
 export interface TFResourceError {
 export interface TFResourceError {
-  errored_out: boolean;
+  errored_out?: boolean;
   error_context?: string;
   error_context?: string;
 }
 }
 
 
@@ -199,6 +199,16 @@ const ExpandedError = styled.div`
   padding-bottom: 17px;
   padding-bottom: 17px;
 `;
 `;
 
 
+const movingGradient = keyframes`
+  0% {
+      background-position: left bottom;
+  }
+
+  100% {
+      background-position: right bottom;
+  }
+`;
+
 const LoadingFill = styled.div<{ width: string; status: string }>`
 const LoadingFill = styled.div<{ width: string; status: string }>`
   width: ${(props) => props.width};
   width: ${(props) => props.width};
   background: ${(props) =>
   background: ${(props) =>
@@ -209,19 +219,9 @@ const LoadingFill = styled.div<{ width: string; status: string }>`
       : "linear-gradient(to right, #8ce1ff, #616FEE)"};
       : "linear-gradient(to right, #8ce1ff, #616FEE)"};
   height: 100%;
   height: 100%;
   background-size: 250% 100%;
   background-size: 250% 100%;
-  animation: moving-gradient 2s infinite;
+  animation: ${movingGradient} 2s infinite;
   animation-timing-function: ease-in-out;
   animation-timing-function: ease-in-out;
   animation-direction: alternate;
   animation-direction: alternate;
-
-  @keyframes moving-gradient {
-    0% {
-        background-position: left bottom;
-    }
-
-    100% {
-        background-position: right bottom;
-    }
-  }​
 `;
 `;
 
 
 const StatusIcon = styled.div<{ successful?: boolean }>`
 const StatusIcon = styled.div<{ successful?: boolean }>`

+ 3 - 3
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -16,7 +16,7 @@ import TitleSection from "components/TitleSection";
 
 
 import { pushFiltered, pushQueryParams } from "shared/routing";
 import { pushFiltered, pushQueryParams } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
-import { SharedStatus } from "../onboarding/steps/ProvisionResources/forms/SharedStatus";
+import { StatusPage } from "../onboarding/steps/ProvisionResources/forms/StatusPage";
 
 
 type PropsType = RouteComponentProps &
 type PropsType = RouteComponentProps &
   WithAuthProps & {
   WithAuthProps & {
@@ -107,10 +107,10 @@ class Dashboard extends Component<PropsType, StateType> {
   renderTabContents = () => {
   renderTabContents = () => {
     if (this.currentTab() === "provisioner") {
     if (this.currentTab() === "provisioner") {
       return (
       return (
-        <SharedStatus
+        <StatusPage
           filter={[]}
           filter={[]}
           project_id={this.props.projectId}
           project_id={this.props.projectId}
-          setInfraStatus={(val: string) => null}
+          setInfraStatus={() => null}
         />
         />
       );
       );
     } else if (this.currentTab() === "create-cluster") {
     } else if (this.currentTab() === "create-cluster") {

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

@@ -12,7 +12,7 @@ import ProviderSelector, {
 import FormFlowWrapper from "./forms/FormFlow";
 import FormFlowWrapper from "./forms/FormFlow";
 import ConnectExternalCluster from "./forms/_ConnectExternalCluster";
 import ConnectExternalCluster from "./forms/_ConnectExternalCluster";
 import backArrow from "assets/back_arrow.png";
 import backArrow from "assets/back_arrow.png";
-import { SharedStatus } from "./forms/SharedStatus";
+import { StatusPage } from "./forms/StatusPage";
 import { useSnapshot } from "valtio";
 import { useSnapshot } from "valtio";
 import { OFState } from "../../state";
 import { OFState } from "../../state";
 
 
@@ -108,7 +108,7 @@ const ProvisionResources: React.FC<Props> = () => {
       case "status":
       case "status":
         return (
         return (
           <>
           <>
-            <SharedStatus
+            <StatusPage
               project_id={project?.id}
               project_id={project?.id}
               filter={getFilterOpts()}
               filter={getFilterOpts()}
               setInfraStatus={setInfraStatus}
               setInfraStatus={setInfraStatus}

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

@@ -125,7 +125,7 @@ const FormFlowWrapper: React.FC<Props> = ({ currentStep }) => {
           {FormTitle[provider] && <img src={FormTitle[provider].icon} />}
           {FormTitle[provider] && <img src={FormTitle[provider].icon} />}
           {FormTitle[provider] && FormTitle[provider].label}
           {FormTitle[provider] && FormTitle[provider].label}
         </FormHeader>
         </FormHeader>
-        <GuideButton href={FormTitle[provider].doc} target="_blank">
+        <GuideButton href={FormTitle[provider]?.doc} target="_blank">
           <i className="material-icons-outlined">help</i>
           <i className="material-icons-outlined">help</i>
           Guide
           Guide
         </GuideButton>
         </GuideButton>

+ 0 - 365
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/SharedStatus.tsx

@@ -1,365 +0,0 @@
-import ProvisionerStatus, {
-  TFModule,
-  TFResource,
-  TFResourceError,
-} 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<{
-  setInfraStatus: (status: { hasError: boolean; description?: string }) => void;
-  project_id: number;
-  filter: string[];
-}> = ({ setInfraStatus, project_id, filter }) => {
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeWebsocket,
-    closeAllWebsockets,
-  } = useWebsockets();
-
-  const [tfModules, setTFModules] = useState<TFModule[]>([]);
-  const [isLoadingState, setIsLoadingState] = useState(true);
-
-  const updateTFModules = (
-    index: number,
-    addedResources: TFResource[],
-    erroredResources: TFResource[],
-    globalErrors: TFResourceError[],
-    gotDesired?: boolean
-  ) => {
-    if (!tfModules[index]?.resources) {
-      tfModules[index].resources = [];
-    }
-
-    if (!tfModules[index]?.global_errors) {
-      tfModules[index].global_errors = [];
-    }
-
-    if (gotDesired) {
-      tfModules[index].got_desired = true;
-    }
-
-    let resources = tfModules[index].resources;
-
-    // construct map of tf resources addresses to indices
-    let resourceAddrMap = new Map<string, number>();
-
-    tfModules[index].resources.forEach((resource, index) => {
-      resourceAddrMap.set(resource.addr, index);
-    });
-
-    for (let addedResource of addedResources) {
-      // if exists, update state to provisioned
-      if (resourceAddrMap.has(addedResource.addr)) {
-        let currResource = resources[resourceAddrMap.get(addedResource.addr)];
-        addedResource.errored = currResource.errored;
-        resources[resourceAddrMap.get(addedResource.addr)] = addedResource;
-      } else {
-        resources.push(addedResource);
-        resourceAddrMap.set(addedResource.addr, resources.length - 1);
-
-        // if the resource is being added but there's not a desired state, re-query for the
-        // desired state
-        if (!tfModules[index].got_desired) {
-          updateDesiredState(index, tfModules[index]);
-        }
-      }
-    }
-
-    for (let erroredResource of erroredResources) {
-      // if exists, update state to provisioned
-      if (resourceAddrMap.has(erroredResource.addr)) {
-        resources[resourceAddrMap.get(erroredResource.addr)] = erroredResource;
-      } else {
-        resources.push(erroredResource);
-        resourceAddrMap.set(erroredResource.addr, resources.length - 1);
-      }
-    }
-
-    tfModules[index].global_errors = [
-      ...tfModules[index].global_errors,
-      ...globalErrors,
-    ];
-
-    setTFModules([...tfModules]);
-  };
-
-  useEffect(() => {
-    if (isLoadingState) {
-      return;
-    }
-    // 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,
-          description: "Encountered error while provisioning",
-        });
-        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;
-
-            // if there's a global error, or the number of resources that errored_out is
-            // greater than 0, this resource is in an error state
-            numModulesErrored +=
-              tfModule.global_errors?.length > 0 ||
-              tfModule.resources.filter(
-                (resource) => resource.errored?.errored_out
-              ).length > 0
-                ? 1
-                : 0;
-          } else if (tfModule.global_errors?.length > 0) {
-            numModulesErrored += 1;
-          }
-        }
-      }
-
-      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,
-        });
-      }
-    } else {
-      setInfraStatus(null);
-    }
-  }, [tfModules, isLoadingState]);
-
-  const setupInfraWebsocket = (
-    websocketID: string,
-    module: TFModule,
-    index: number
-  ) => {
-    let apiPath = `/api/projects/${project_id}/infras/${module.id}/logs`;
-
-    const wsConfig = {
-      onopen: () => {
-        console.log(`connected to websocket: ${websocketID}`);
-      },
-      onmessage: (evt: MessageEvent) => {
-        // parse the data
-        let parsedData = JSON.parse(evt.data);
-
-        let addedResources: TFResource[] = [];
-        let erroredResources: TFResource[] = [];
-        let globalErrors: TFResourceError[] = [];
-
-        for (let streamVal of parsedData) {
-          let streamValData = JSON.parse(streamVal?.Values?.data);
-
-          switch (streamValData?.type) {
-            case "apply_complete":
-              addedResources.push({
-                addr: streamValData?.hook?.resource?.addr,
-                provisioned: true,
-                errored: {
-                  errored_out: false,
-                },
-              });
-
-              break;
-            case "diagnostic":
-              if (streamValData["@level"] == "error") {
-                if (streamValData?.hook?.resource?.addr != "") {
-                  erroredResources.push({
-                    addr: streamValData?.hook?.resource?.addr,
-                    provisioned: false,
-                    errored: {
-                      errored_out: true,
-                      error_context: streamValData["@message"],
-                    },
-                  });
-                } else {
-                  globalErrors.push({
-                    errored_out: true,
-                    error_context: streamValData["@message"],
-                  });
-                }
-              }
-            case "change_summary":
-              if (streamValData.changes.add != 0) {
-                updateDesiredState(index, module);
-              }
-            default:
-          }
-        }
-
-        updateTFModules(index, addedResources, erroredResources, globalErrors);
-      },
-
-      onclose: () => {
-        console.log(`closing websocket: ${websocketID}`);
-      },
-
-      onerror: (err: ErrorEvent) => {
-        console.log(err);
-        closeWebsocket(websocketID);
-      },
-    };
-
-    newWebsocket(websocketID, apiPath, wsConfig);
-    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, [], [], true);
-  };
-
-  const updateDesiredState = (index: number, val: TFModule) => {
-    setIsLoadingState(true);
-    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);
-          })
-          .finally(() => {
-            setIsLoadingState(true);
-          });
-      })
-      .catch((err) => {
-        console.log(err);
-        setIsLoadingState(true);
-      });
-  };
-
-  useEffect(() => {
-    api.getInfra("<token>", {}, { project_id: project_id }).then((res) => {
-      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
-      matchedInfras.forEach((infra: any) => {
-        var module: TFModule = {
-          id: infra.id,
-          kind: infra.kind,
-          status: infra.status,
-          got_desired: false,
-          created_at: infra.created_at,
-        };
-
-        tfModules.push(module);
-      });
-
-      if (tfModules.every((m) => m.status === "created")) {
-        setInfraStatus({
-          hasError: false,
-        });
-      }
-
-      setTFModules([...tfModules]);
-
-      tfModules.forEach((val, index) => {
-        if (val?.status != "created") {
-          updateDesiredState(index, val);
-          setupInfraWebsocket(val.id + "", val, index);
-        }
-      });
-    });
-
-    return closeAllWebsockets;
-  }, []);
-
-  let sortedModules = tfModules.sort((a, b) =>
-    b.id < a.id ? -1 : b.id > a.id ? 1 : 0
-  );
-
-  return (
-    <>
-      <ProvisionerStatus modules={sortedModules} />
-    </>
-  );
-};

+ 473 - 0
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/StatusPage.tsx

@@ -0,0 +1,473 @@
+import ProvisionerStatus, {
+  TFModule,
+  TFResource,
+  TFResourceError,
+} from "components/ProvisionerStatus";
+import React, { useEffect, useRef, useState } from "react";
+import api from "shared/api";
+import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
+
+type Props = {
+  setInfraStatus: (status: { hasError: boolean; description?: string }) => void;
+  project_id: number;
+  filter: string[];
+};
+
+type Infra = {
+  id: number;
+  created_at: string;
+  updated_at: string;
+  project_id: number;
+  kind: string;
+  status: string;
+  last_applied: any;
+};
+
+type Desired = {
+  addr: string;
+  errored:
+    | { errored_out: false }
+    | { errored_out: true; error_context: string };
+  implied_provider: string;
+  resource: string;
+  resource_name: string;
+  resource_type: string;
+};
+
+type InfraCurrentResponse = {
+  version: number;
+  terraform_version: string;
+  serial: number;
+  lineage: string;
+  outputs: any;
+  resources: {
+    instances: any[];
+    mode: string;
+    name: string;
+    provider: string;
+    type: string;
+  }[];
+};
+
+export const StatusPage = ({
+  filter: selectedFilters,
+  project_id,
+  setInfraStatus,
+}: Props) => {
+  const {
+    newWebsocket,
+    openWebsocket,
+    closeWebsocket,
+    closeAllWebsockets,
+  } = useWebsockets();
+
+  const {
+    tfModules,
+    initModule,
+    updateDesired,
+    updateModuleResources,
+    updateGlobalErrorsForModule,
+  } = useTFModules();
+
+  const filterBySelectedInfras = (currentInfra: Infra) => {
+    if (!Array.isArray(selectedFilters) || !selectedFilters?.length) {
+      return true;
+    }
+
+    if (selectedFilters.includes(currentInfra.kind)) {
+      return true;
+    }
+    return false;
+  };
+
+  const getLatestInfras = (infras: Infra[]) => {
+    // Create a map with the relation infra.kind => infra
+    // This will allow us to keep only one infra per kind.
+    const infraMap = new Map<string, Infra>();
+
+    infras.forEach((infra) => {
+      // Get last infra from that kind, kind being gke, ecr, etc.
+      const latestSavedInfra = infraMap.get(infra.kind);
+
+      // If infra doesn't exists, it means its the first one appearing so we save it
+      if (!latestSavedInfra) {
+        infraMap.set(infra.kind, infra);
+        return;
+      }
+
+      // Check if the latest saved infra was recent than the one we're currently iterating
+      // If the current one iterating is newer, then we update the map!
+      if (
+        new Date(infra.created_at).getTime() >
+        new Date(latestSavedInfra.created_at).getTime()
+      ) {
+        infraMap.set(infra.kind, infra);
+        return;
+      }
+    });
+
+    // Get the array from the values of the array.
+    return Array.from(infraMap.values());
+  };
+
+  const getInfras = async () => {
+    try {
+      const res = await api.getInfra<Infra[]>(
+        "<token>",
+        {},
+        { project_id: project_id }
+      );
+      // Filter infras based on what we care only, usually on the onboarding we'll want only the ones
+      // currently being provisioned
+      const matchedInfras = res.data.filter(filterBySelectedInfras);
+
+      // Get latest infras for each kind of infra on the array.
+      const latestMatchedInfras = getLatestInfras(matchedInfras);
+
+      // Check if all infras are created then enable continue button
+      if (latestMatchedInfras.every((infra) => infra.status === "created")) {
+        setInfraStatus({
+          hasError: false,
+        });
+      }
+
+      // Init tf modules based on matched infras
+      latestMatchedInfras.forEach((infra) => {
+        // Init the module for the hook
+        initModule(infra);
+
+        // Update all the resources needed for the current infra
+        getDesiredState(infra.id);
+      });
+    } catch (error) {}
+  };
+
+  const getDesiredState = async (infra_id: number) => {
+    try {
+      const desired = await api
+        .getInfraDesired("<token>", {}, { project_id, infra_id })
+        .then((res) => res?.data);
+
+      updateDesired(infra_id, desired);
+      // Check if we have some modules already provisioned
+      await getProvisionedModules(infra_id);
+
+      // Connect to websocket that will provide live info of the provisioning for this infra
+      connectToLiveUpdateModule(infra_id);
+    } catch (error) {
+      console.error(error);
+      setTimeout(() => {
+        getDesiredState(infra_id);
+      }, 500);
+    }
+  };
+
+  const getProvisionedModules = async (infra_id: number) => {
+    try {
+      const current = await api
+        .getInfraCurrent<InfraCurrentResponse>(
+          "<token>",
+          {},
+          { project_id, infra_id }
+        )
+        .then((res) => res?.data);
+
+      const provisionedResources: TFResource[] = current?.resources?.map(
+        (resource: any) => {
+          return {
+            addr: `${resource?.type}.${resource?.name}`,
+            provisioned: true,
+            errored: {
+              errored_out: false,
+            },
+          } as TFResource;
+        }
+      );
+
+      updateModuleResources(infra_id, provisionedResources);
+    } catch (error) {
+      console.error(error);
+    }
+  };
+
+  const connectToLiveUpdateModule = (infra_id: number) => {
+    const websocketId = `${infra_id}`;
+    const apiPath = `/api/projects/${project_id}/infras/${infra_id}/logs`;
+
+    const wsConfig: NewWebsocketOptions = {
+      onopen: () => {
+        console.log(`connected to websocket for infra_id: ${websocketId}`);
+      },
+      onmessage: (evt: MessageEvent) => {
+        // parse the data
+        const parsedData = JSON.parse(evt.data);
+
+        const addedResources: TFResource[] = [];
+        const erroredResources: TFResource[] = [];
+        const globalErrors: TFResourceError[] = [];
+
+        for (const streamVal of parsedData) {
+          const streamValData = JSON.parse(streamVal?.Values?.data);
+
+          switch (streamValData?.type) {
+            case "apply_complete":
+              addedResources.push({
+                addr: streamValData?.hook?.resource?.addr,
+                provisioned: true,
+                errored: {
+                  errored_out: false,
+                },
+              });
+
+              break;
+            case "diagnostic":
+              if (streamValData["@level"] == "error") {
+                if (streamValData?.hook?.resource?.addr !== "") {
+                  erroredResources.push({
+                    addr: streamValData?.hook?.resource?.addr,
+                    provisioned: false,
+                    errored: {
+                      errored_out: true,
+                      error_context: streamValData["@message"],
+                    },
+                  });
+                } else {
+                  globalErrors.push({
+                    errored_out: true,
+                    error_context: streamValData["@message"],
+                  });
+                }
+              }
+            default:
+          }
+        }
+
+        updateModuleResources(infra_id, [
+          ...addedResources,
+          ...erroredResources,
+        ]);
+
+        updateGlobalErrorsForModule(infra_id, globalErrors);
+      },
+
+      onclose: () => {
+        console.log(`closing websocket for infra_id: ${websocketId}`);
+      },
+
+      onerror: (err: ErrorEvent) => {
+        console.log(err);
+        closeWebsocket(`${websocketId}`);
+      },
+    };
+
+    newWebsocket(websocketId, apiPath, wsConfig);
+    openWebsocket(websocketId);
+  };
+
+  useEffect(() => {
+    getInfras();
+    return () => {
+      closeAllWebsockets();
+    };
+  }, []);
+
+  useEffect(() => {
+    if (!tfModules?.length) {
+      setInfraStatus(null);
+      return;
+    }
+    const hasModuleWithError = tfModules.find(
+      (module) => module.status === "error"
+    );
+    const hasModuleInCreatingState = tfModules.find(
+      (module) => module.status === "creating"
+    );
+
+    if (hasModuleInCreatingState) {
+      setInfraStatus(null);
+      return;
+    }
+
+    if (!hasModuleInCreatingState && !hasModuleWithError) {
+      setInfraStatus({ hasError: false });
+      return;
+    }
+
+    if (!hasModuleInCreatingState && hasModuleWithError) {
+      setInfraStatus({ hasError: true });
+      return;
+    }
+  }, [tfModules]);
+
+  const sortedModules = tfModules.sort((a, b) =>
+    b.id < a.id ? -1 : b.id > a.id ? 1 : 0
+  );
+
+  return <ProvisionerStatus modules={sortedModules} />;
+};
+
+type TFModulesState = {
+  [infraId: number]: TFModule;
+};
+
+const useTFModules = () => {
+  // Use a ref to keep track of all the Terraform modules
+  const modules = useRef<TFModulesState>({});
+
+  // Use state to keep the reactive array of terraform modules
+  const [tfModules, setTfModules] = useState<TFModule[]>([]);
+
+  /**
+   * This will map out the ref containing all the terraform modules and return a sorted array.
+   */
+  const updateTFModules = (): void => {
+    if (typeof modules.current !== "object") {
+      setTfModules([]);
+    }
+
+    const sortedModules = Object.values(modules.current).sort((a, b) =>
+      b.id < a.id ? -1 : b.id > a.id ? 1 : 0
+    );
+    setTfModules(sortedModules);
+  };
+
+  /**
+   * Init a TFModule based on a Infra, this infra is usually more basic
+   * and doesn't contain all the resources that it actually needs.
+   * The initialized TFModule will be used to keep track if the infra
+   * changed from creating status to another one.
+   *
+   * @param infra Infra object used to initialize the terraform module used to track provisioning status
+   */
+  const initModule = (infra: Infra) => {
+    const module: TFModule = {
+      id: infra.id,
+      kind: infra.kind,
+      status: infra.status,
+      got_desired: false,
+      created_at: infra.created_at,
+    };
+    setModule(infra.id, module);
+  };
+
+  /**
+   * Add or replace if existed, this function will set the module into the ref
+   * and call the updateTFModules to update the array used to show the infras
+   *
+   * @param infraId Infra ID to be updated
+   * @param module New updated module
+   */
+  const setModule = (infraId: number, module: TFModule) => {
+    modules.current = {
+      ...modules.current,
+      [infraId]: module,
+    };
+    updateTFModules();
+  };
+
+  const getModule = (infraId: number) => {
+    return { ...modules.current[infraId] };
+  };
+
+  /**
+   * @param infraId Module to be updated
+   * @param desired All the desired resources that are going to be needed to complete provisioning
+   */
+  const updateDesired = (infraId: number, desired: Desired[]) => {
+    const selectedModule = getModule(infraId);
+
+    if (!Array.isArray(selectedModule?.resources)) {
+      selectedModule.resources = [];
+    }
+
+    selectedModule.resources = desired.map((d) => {
+      return {
+        addr: d.addr,
+        errored: d.errored,
+        provisioned: false,
+      };
+    });
+
+    setModule(infraId, selectedModule);
+  };
+
+  /**
+   * @param infraId Module to be updated
+   * @param updatedResources Updated resources array, this may contain one or more objects with some status updates.
+   */
+  const updateModuleResources = (
+    infraId: number,
+    updatedResources: TFResource[]
+  ) => {
+    const selectedModule = getModule(infraId);
+
+    const updatedModuleResources = selectedModule.resources.map((resource) => {
+      const correspondedResource: TFResource = updatedResources.find(
+        (updatedResource) => updatedResource.addr === resource.addr
+      );
+      if (!correspondedResource) {
+        return resource;
+      }
+      let errored = undefined;
+
+      if (correspondedResource?.errored) {
+        errored = {
+          ...(correspondedResource?.errored || {}),
+        };
+      }
+
+      return {
+        ...resource,
+        provisioned: correspondedResource.provisioned,
+        errored,
+      };
+    });
+
+    selectedModule.resources = updatedModuleResources;
+
+    const isModuleCreated =
+      selectedModule.resources.every((resource) => {
+        return resource.provisioned;
+      }) && !selectedModule.global_errors?.length;
+
+    const isModuleOnError =
+      selectedModule.resources.find((resource) => {
+        return resource.errored?.errored_out;
+      }) || selectedModule.global_errors?.length;
+
+    if (isModuleCreated) {
+      selectedModule.status = "created";
+    } else if (isModuleOnError) {
+      selectedModule.status = "error";
+    } else {
+      selectedModule.status = selectedModule.status;
+    }
+
+    setModule(infraId, selectedModule);
+  };
+
+  /**
+   * @param infraId Module to be updated
+   * @param globalErrors Errors that may not belong to a resource but appeared during provisioning
+   */
+  const updateGlobalErrorsForModule = (
+    infraId: number,
+    globalErrors: TFResourceError[]
+  ) => {
+    const module = getModule(infraId);
+
+    module.global_errors = [...(module.global_errors || []), ...globalErrors];
+    if (globalErrors.length) {
+      module.status = "error";
+    }
+    setModule(infraId, module);
+  };
+
+  return {
+    tfModules,
+    initModule,
+    updateDesired,
+    updateModuleResources,
+    updateGlobalErrorsForModule,
+  };
+};

+ 0 - 1
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_AWSProvisionerForm.tsx

@@ -10,7 +10,6 @@ import {
 import React, { useEffect, useState } from "react";
 import React, { useEffect, useState } from "react";
 import api from "shared/api";
 import api from "shared/api";
 import { useSnapshot } from "valtio";
 import { useSnapshot } from "valtio";
-import { SharedStatus } from "./SharedStatus";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import Helper from "components/form-components/Helper";
 import Helper from "components/form-components/Helper";
 
 

+ 0 - 6
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_DOProvisionerForm.tsx

@@ -1,10 +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 SelectRow from "components/form-components/SelectRow";
 import SelectRow from "components/form-components/SelectRow";
-import ProvisionerStatus, {
-  TFModule,
-  TFResource,
-} from "components/ProvisionerStatus";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import { OFState } from "main/home/onboarding/state";
 import { OFState } from "main/home/onboarding/state";
 import { DOProvisionerConfig } from "main/home/onboarding/types";
 import { DOProvisionerConfig } from "main/home/onboarding/types";
@@ -12,8 +8,6 @@ import React, { useEffect, useState } from "react";
 import api from "shared/api";
 import api from "shared/api";
 import styled from "styled-components";
 import styled from "styled-components";
 import { useSnapshot } from "valtio";
 import { useSnapshot } from "valtio";
-import { useWebsockets } from "shared/hooks/useWebsockets";
-import { SharedStatus } from "./SharedStatus";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 
 
 const tierOptions = [
 const tierOptions = [

+ 1 - 3
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx

@@ -13,7 +13,6 @@ import React, { useEffect, useState } from "react";
 import api from "shared/api";
 import api from "shared/api";
 import styled from "styled-components";
 import styled from "styled-components";
 import { useSnapshot } from "valtio";
 import { useSnapshot } from "valtio";
-import { SharedStatus } from "./SharedStatus";
 
 
 const regionOptions = [
 const regionOptions = [
   { value: "asia-east1", label: "asia-east1" },
   { value: "asia-east1", label: "asia-east1" },
@@ -231,8 +230,7 @@ export const CredentialsForm: React.FC<{
           {lastConnectedAccount?.gcp_sa_email || "n/a"}
           {lastConnectedAccount?.gcp_sa_email || "n/a"}
         </Flex>
         </Flex>
         <Right>
         <Right>
-          Connected at{" "}
-          {readableDate(lastConnectedAccount.created_at)}
+          Connected at {readableDate(lastConnectedAccount.created_at)}
         </Right>
         </Right>
       </PreviewRow>
       </PreviewRow>
       <Helper>
       <Helper>