Răsfoiți Sursa

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

mergin
Alexander Belanger 4 ani în urmă
părinte
comite
6aab4484d0

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

@@ -1,5 +1,6 @@
 import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
+import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import RegistryImageList from "main/home/onboarding/components/RegistryImageList";
 import { OFState } from "main/home/onboarding/state";
@@ -11,6 +12,16 @@ import api from "shared/api";
 import styled from "styled-components";
 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
  *
@@ -23,36 +34,78 @@ export const CredentialsForm: React.FC<{
   nextFormStep: (data: Partial<DORegistryConfig>) => void;
   project: any;
 }> = ({ nextFormStep, project }) => {
-  const location = useLocation();
+  const snap = useSnapshot(OFState);
+
+  const [isLoading, setIsLoading] = useState(true);
+  const [connectedAccount, setConnectedAccount] = useState(null);
+
   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 encoded_redirect_uri = encodeURIComponent(url);
+
+  if (isLoading) {
+    return <Loading />;
+  }
+
   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"}
+        />
+      )}
+    </>
   );
 };
 

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

@@ -177,6 +177,22 @@ export const SettingsForm: React.FC<{
     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) => {
     console.log("Started provision ECR");
 
@@ -222,16 +238,33 @@ export const SettingsForm: React.FC<{
       setButtonStatus(validation.error);
       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;
+
     let registryProvisionResponse = null;
     let clusterProvisionResponse = null;
 
     const shouldProvisionECR = snap.StateHandler.connected_registry.skip;
 
     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({
       settings: {

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

@@ -14,6 +14,7 @@ import styled from "styled-components";
 import { useSnapshot } from "valtio";
 import { useWebsockets } from "shared/hooks/useWebsockets";
 import { SharedStatus } from "./SharedStatus";
+import Loading from "components/Loading";
 
 const tierOptions = [
   { value: "basic", label: "Basic" },
@@ -33,6 +34,16 @@ const regionOptions = [
   { 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
  *
@@ -45,34 +56,77 @@ export const CredentialsForm: React.FC<{
   nextFormStep: (data: Partial<DOProvisionerConfig>) => void;
   project: any;
 }> = ({ nextFormStep, project }) => {
+  const snap = useSnapshot(OFState);
+
+  const [isLoading, setIsLoading] = useState(true);
+  const [connectedAccount, setConnectedAccount] = useState(null);
+
   useEffect(() => {
     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";
       });
 
-      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 encoded_redirect_uri = encodeURIComponent(url);
 
+  if (isLoading) {
+    return <Loading />;
+  }
+
   return (
     <>
+      {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}`}
       >
-        Sign In to Digital Ocean
+        {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"}
+        />
+      )}
     </>
   );
 };
@@ -110,6 +164,22 @@ export const SettingsForm: React.FC<{
     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) => {
     console.log("Provisioning DOCR...");
     try {
@@ -165,18 +235,34 @@ export const SettingsForm: React.FC<{
       setButtonStatus(validation.error);
       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;
     let registryProvisionResponse = null;
     let clusterProvisionResponse = null;
 
     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({
       settings: {

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

@@ -161,6 +161,22 @@ export const SettingsForm: React.FC<{
     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) => {
     console.error(error);
   };
@@ -168,20 +184,36 @@ export const SettingsForm: React.FC<{
   const submit = async () => {
     const validation = validate();
 
+    setButtonStatus("loading");
+
     if (validation.hasError) {
       setButtonStatus(validation.error);
       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;
 
     let registryProvisionResponse = null;
     let clusterProvisionResponse = null;
     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({
       settings: {
@@ -192,27 +224,29 @@ export const SettingsForm: React.FC<{
     });
   };
 
-  const provisionGCR = (id: number) => {
+  const provisionGCR = async (id: number) => {
     console.log("Provisioning GCR");
 
-    return api
-      .createGCR(
+    try {
+      const res = await api.createGCR(
         "<token>",
         {
           gcp_integration_id: id,
           issuer_email: snap.StateHandler.user_email,
         },
         { 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");
 
-    return api
-      .createGKE(
+    try {
+      const res = await api.createGKE(
         "<token>",
         {
           gke_name: clusterName,
@@ -220,9 +254,11 @@ export const SettingsForm: React.FC<{
           issuer_email: snap.StateHandler.user_email,
         },
         { project_id: project.id }
-      )
-      .then((res) => res?.data)
-      .catch(catchError);
+      );
+      return res?.data;
+    } catch (error) {
+      return catchError(error);
+    }
   };
 
   return (

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

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