Bladeren bron

Merge pull request #1401 from porter-dev/nico/implement-registry-listing-on-connect-registry

[IMPROVEMENT] Implement registry listing on connect registry
Nicolas Frati 4 jaren geleden
bovenliggende
commit
00ee0309b7

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

@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from "react";
+import React, { useMemo, useRef, useState } from "react";
 import { integrationList } from "shared/common";
 import styled from "styled-components";
 import { SupportedProviders } from "../types";
@@ -13,6 +13,7 @@ export type ProviderSelectorProps = {
     icon: string;
     label: string;
   }[];
+  defaultOption?: string;
 };
 
 export const registryOptions = [
@@ -68,22 +69,33 @@ export const provisionerOptionsWithExternal = [
 const ProviderSelector: React.FC<ProviderSelectorProps> = ({
   selectProvider,
   options,
+  defaultOption,
 }) => {
-  const [provider, setProvider] = useState(() => {
-    if (options.find((o) => o.value === "skip")) {
-      return "skip";
+  const [provider, setProvider] = useState(null);
+  const [isDirty, setIsDirty] = useState(false);
+
+  const activeProvider = useMemo(() => {
+    if (!isDirty || !provider) {
+      if (typeof defaultOption === "string") {
+        return defaultOption;
+      }
+      if (options.find((o) => o.value === "skip")) {
+        return "skip";
+      }
     }
-    return null;
-  });
+
+    return provider;
+  }, [provider, isDirty, defaultOption]);
 
   return (
     <>
       <Br />
       <Selector
-        activeValue={provider}
+        activeValue={activeProvider}
         options={options}
         placeholder="Select a cloud provider"
         setActiveValue={(provider) => {
+          setIsDirty(true);
           setProvider(provider);
           selectProvider(provider as SupportedProviders);
         }}

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

@@ -76,6 +76,21 @@ export const StateHandler = proxy({
         skip: true,
       };
     },
+    saveRegistryAndContinue: (data: any) => {
+      const serviceToProvider = {
+        ecr: "aws",
+        gcr: "gcp",
+        dcr: "do",
+      };
+      const connectedRegistry = {
+        skip: false,
+        provider: (serviceToProvider as any)[data?.service],
+        credentials: {
+          id: data?.id,
+        },
+      };
+      StateHandler.connected_registry = connectedRegistry;
+    },
     saveRegistryProvider: (provider: string) => {
       if (provider === StateHandler.connected_registry?.provider) {
         return;

+ 6 - 2
dashboard/src/main/home/onboarding/state/StepHandler.ts

@@ -16,15 +16,17 @@ type Step = {
       skip?: string;
       continue?: string;
       go_back?: string;
+      continue_with_current?: string;
     };
   };
 };
 
-export type Action = "skip" | "continue" | "go_back";
+export type Action = "skip" | "continue" | "go_back" | "continue_with_current";
 type ActionHandler = {
   skip?: string;
   continue: string;
   go_back?: string;
+  continue_with_current?: string;
 };
 
 export type FlowType = {
@@ -53,12 +55,14 @@ const flow: FlowType = {
       on: {
         skip: "provision_resources",
         continue: "connect_registry.credentials",
+        continue_with_current: "provision_resources",
         go_back: "connect_source",
       },
       execute: {
         on: {
           skip: "skipRegistryConnection",
           continue: "saveRegistryProvider",
+          continue_with_current: "saveRegistryAndContinue",
         },
       },
       substeps: {
@@ -112,7 +116,7 @@ const flow: FlowType = {
          * has a proper way of listing the registries and
          * manage them inside the step
          */
-        // go_back: "connect_registry",
+        go_back: "connect_registry",
       },
       execute: {
         on: {

+ 106 - 4
dashboard/src/main/home/onboarding/steps/ConnectRegistry/ConnectRegistry.tsx

@@ -1,7 +1,7 @@
 import Helper from "components/form-components/Helper";
 import SaveButton from "components/SaveButton";
 import TitleSection from "components/TitleSection";
-import React from "react";
+import React, { useEffect, useMemo, useState } from "react";
 import { useParams } from "react-router";
 
 import styled from "styled-components";
@@ -13,16 +13,66 @@ import backArrow from "assets/back_arrow.png";
 import FormFlowWrapper from "./forms/FormFlow";
 import { OFState } from "../../state";
 import { useSnapshot } from "valtio";
+import api from "shared/api";
+import Loading from "components/Loading";
+import { integrationList } from "shared/common";
+import Registry from "./components/Registry";
 
 const ConnectRegistry: React.FC<{}> = ({}) => {
   const snap = useSnapshot(OFState);
   const { step } = useParams<any>();
+  const [connectedRegistries, setConnectedRegistries] = useState(null);
+  const [isLoading, setIsLoading] = useState(true);
 
   const currentProvider = snap.StateHandler.connected_registry?.provider;
 
   const enableGoBack =
     snap.StepHandler.canGoBack && !snap.StepHandler.isSubFlow;
 
+  useEffect(() => {
+    let hookState = { isSubscribed: true };
+
+    getRegistries(hookState);
+
+    return () => {
+      hookState.isSubscribed = false;
+    };
+  }, [snap.StateHandler?.project]);
+
+  const getRegistries = async (
+    props: { isSubscribed: boolean } = { isSubscribed: true }
+  ) => {
+    const projectId = snap.StateHandler?.project?.id;
+
+    if (typeof projectId !== "number") {
+      return;
+    }
+
+    setIsLoading(true);
+    try {
+      const res = await api.getProjectRegistries(
+        "<token>",
+        {},
+        { id: projectId }
+      );
+      const registries = res?.data;
+      if (props.isSubscribed) {
+        if (Array.isArray(registries)) {
+          setConnectedRegistries(registries);
+        }
+      }
+    } catch (error) {
+      console.error(error);
+      if (props.isSubscribed) {
+        setConnectedRegistries(null);
+      }
+    } finally {
+      if (props.isSubscribed) {
+        setIsLoading(false);
+      }
+    }
+  };
+
   const handleGoBack = () => {
     OFState.actions.nextStep("go_back");
   };
@@ -35,6 +85,27 @@ const ConnectRegistry: React.FC<{}> = ({}) => {
     provider !== "skip" && OFState.actions.nextStep("continue", provider);
   };
 
+  const handleContinueWithCurrent = () => {
+    const connectedRegistry = connectedRegistries[0];
+    OFState.actions.nextStep("continue_with_current", connectedRegistry);
+  };
+
+  const selectorOptions = useMemo(() => {
+    const options = [...registryOptions];
+    if (Array.isArray(connectedRegistries) && connectedRegistries.length) {
+      const newOptions = options.filter((o) => o.value !== "skip");
+      return [
+        {
+          value: "use_current",
+          label: "Continue with current",
+          icon: "",
+        },
+        ...newOptions,
+      ];
+    }
+    return options;
+  }, [connectedRegistries]);
+
   return (
     <Div>
       {enableGoBack && (
@@ -62,22 +133,49 @@ const ConnectRegistry: React.FC<{}> = ({}) => {
           : "Link to an existing Docker registry or continue."}
       </Helper>
 
-      {step ? (
+      {!isLoading && step ? (
         <FormFlowWrapper currentStep={step} />
       ) : (
         <>
           <ProviderSelector
+            defaultOption={
+              Array.isArray(connectedRegistries) && connectedRegistries.length
+                ? "use_current"
+                : "skip"
+            }
             selectProvider={(provider) => {
               if (provider !== "external") {
                 handleSelectProvider(provider);
               }
             }}
-            options={registryOptions}
+            options={selectorOptions}
           />
+          {isLoading && <Loading />}
+
+          {!!connectedRegistries?.length && (
+            <IntegrationList>
+              {connectedRegistries.map((registry: any) => (
+                <Registry
+                  key={registry.name}
+                  registry={registry}
+                  onDelete={getRegistries}
+                />
+              ))}
+            </IntegrationList>
+          )}
           <NextStep
             text="Continue"
             disabled={false}
-            onClick={() => handleSkip()}
+            onClick={() => {
+              if (
+                Array.isArray(connectedRegistries) &&
+                connectedRegistries.length
+              ) {
+                handleContinueWithCurrent();
+              } else {
+                handleSkip();
+              }
+            }}
             status={""}
             makeFlush={true}
             clearPosition={true}
@@ -92,6 +190,10 @@ const ConnectRegistry: React.FC<{}> = ({}) => {
 
 export default ConnectRegistry;
 
+const IntegrationList = styled.div`
+  margin-top: 14px;
+`;
+
 const Div = styled.div`
   width: 100%;
 `;

+ 193 - 0
dashboard/src/main/home/onboarding/steps/ConnectRegistry/components/Registry.tsx

@@ -0,0 +1,193 @@
+import Loading from "components/Loading";
+import { OFState } from "main/home/onboarding/state";
+import React, { useContext, useState } from "react";
+import api from "shared/api";
+import { integrationList } from "shared/common";
+import { Context } from "shared/Context";
+import styled from "styled-components";
+import { useSnapshot } from "valtio";
+
+const serviceToProvider: {
+  [key: string]: string;
+} = {
+  docr: "do",
+  ecr: "aws",
+  gcr: "gcp",
+};
+
+const Registry: React.FC<{ registry: any; onDelete: () => void }> = (props) => {
+  const { registry, onDelete } = props;
+  const service = serviceToProvider[registry?.service];
+  const icon = integrationList[service || registry?.service]?.icon;
+  const subtitle = integrationList[registry?.service]?.label;
+  const snap = useSnapshot(OFState);
+  const { setCurrentError } = useContext(Context);
+
+  const [isDeleting, setIsDeleting] = useState(false);
+  const [hasError, setHasError] = useState(false);
+
+  const deleteRegistry = async (id: number) => {
+    const projectId = snap.StateHandler?.project?.id;
+
+    if (typeof projectId !== "number") {
+      return;
+    }
+    setIsDeleting(true);
+    try {
+      await api.deleteRegistryIntegration(
+        "<token>",
+        {},
+        {
+          project_id: projectId,
+          registry_id: id,
+        }
+      );
+      onDelete();
+      setIsDeleting(false);
+    } catch (error) {
+      setIsDeleting(false);
+      setCurrentError(error);
+      setHasError(true);
+      setTimeout(() => setHasError(false), 1000);
+    }
+  };
+
+  return (
+    <React.Fragment key={registry.name}>
+      <Integration>
+        <MainRow disabled={false}>
+          <Flex>
+            <Icon src={icon && icon} />
+            <Description>
+              <Label>{registry?.name}</Label>
+              <IntegrationSubtitle>{subtitle}</IntegrationSubtitle>
+            </Description>
+          </Flex>
+          <MaterialIconTray disabled={false}>
+            {isDeleting && (
+              <I disabled>
+                <Loading height={"28px"} width="28px" />
+              </I>
+            )}
+            {hasError && (
+              <ErrorI className="material-icons">priority_high</ErrorI>
+            )}
+            {!hasError && !isDeleting && (
+              <I
+                className="material-icons"
+                onClick={() => deleteRegistry(registry?.id)}
+              >
+                delete
+              </I>
+            )}
+          </MaterialIconTray>
+        </MainRow>
+      </Integration>
+    </React.Fragment>
+  );
+};
+
+export default Registry;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const Integration = styled.div`
+  margin-left: -2px;
+  display: flex;
+  flex-direction: column;
+  background: #26282f;
+  margin-bottom: 15px;
+  border-radius: 8px;
+  box-shadow: 0 4px 15px 0px #00000055;
+`;
+
+const IntegrationSubtitle = styled.div`
+  color: #aaaabb;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  padding-top: 5px;
+`;
+
+const Icon = styled.img`
+  width: 30px;
+  margin-right: 18px;
+`;
+
+const I = styled.i`
+  color: #ffffff44;
+  :hover {
+    cursor: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "not-allowed" : "pointer"};
+  }
+`;
+
+const ErrorI = styled(I)`
+  color: #ed5f85;
+`;
+
+const MainRow = styled.div`
+  height: 70px;
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25px;
+  border-radius: 5px;
+  :hover {
+    background: ${(props: { disabled: boolean }) =>
+      props.disabled ? "" : "#ffffff11"};
+    > i {
+      background: ${(props: { disabled: boolean }) =>
+        props.disabled ? "" : "#ffffff11"};
+    }
+  }
+
+  > i {
+    border-radius: 20px;
+    font-size: 18px;
+    padding: 5px;
+    color: #ffffff44;
+    margin-right: -7px;
+    :hover {
+      background: ${(props: { disabled: boolean }) =>
+        props.disabled ? "" : "#ffffff11"};
+    }
+  }
+`;
+
+const MaterialIconTray = styled.div`
+  max-width: 60px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  > i {
+    background: #26282f;
+    border-radius: 20px;
+    font-size: 18px;
+    padding: 5px;
+    margin: 0 5px;
+
+    :hover {
+      background: ${(props: { disabled: boolean }) =>
+        props.disabled ? "" : "#ffffff11"};
+    }
+  }
+`;
+
+const Description = styled.div`
+  display: flex;
+  flex-direction: column;
+  margin: 0;
+  padding: 0;
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  font-size: 14px;
+  font-weight: 500;
+`;