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

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

mergin
Alexander Belanger 4 лет назад
Родитель
Сommit
f68e6fcb0f
23 измененных файлов с 299 добавлено и 108 удалено
  1. 16 15
      dashboard/src/components/Breadcrumb.tsx
  2. 18 1
      dashboard/src/main/home/Home.tsx
  3. 9 2
      dashboard/src/main/home/ModalHandler.tsx
  4. 10 4
      dashboard/src/main/home/modals/Modal.tsx
  5. 62 0
      dashboard/src/main/home/modals/RedirectToOnboardingModal.tsx
  6. 1 1
      dashboard/src/main/home/onboarding/Onboarding.tsx
  7. 41 52
      dashboard/src/main/home/onboarding/components/ProviderSelector.tsx
  8. 19 5
      dashboard/src/main/home/onboarding/components/RegistryImageList.tsx
  9. 12 1
      dashboard/src/main/home/onboarding/state/StepHandler.ts
  10. 2 2
      dashboard/src/main/home/onboarding/state/index.ts
  11. 6 3
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/ConnectRegistry.tsx
  12. 1 1
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/ConnectRegistryWrapper.tsx
  13. 6 5
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/FormFlow.tsx
  14. 4 2
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_AWSRegistryForm.tsx
  15. 11 1
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_DORegistryForm.tsx
  16. 11 1
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_GCPRegistryForm.tsx
  17. 10 3
      dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx
  18. 5 1
      dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResourcesWrapper.tsx
  19. 5 4
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/FormFlow.tsx
  20. 1 1
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_AWSProvisionerForm.tsx
  21. 47 1
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_ConnectExternalCluster.tsx
  22. 1 1
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_DOProvisionerForm.tsx
  23. 1 1
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx

+ 16 - 15
dashboard/src/components/Breadcrumb.tsx

@@ -1,28 +1,28 @@
 import { Steps } from "main/home/onboarding/types";
-import React, { useState } from "react";
+import React, { Fragment, useState } from "react";
 
 import styled from "styled-components";
 
 type Props = {
   currentStep: string;
-  steps: { value: string, label: string }[];
+  steps: { value: string; label: string }[];
   onClickStep: (step: string) => void;
 };
 
 const Breadcrumb: React.FC<Props> = ({ currentStep, steps, onClickStep }) => {
   return (
     <StyledBreadcrumb>
-      {steps.map((step: { value: string, label: string }, i: number) => {
+      {steps.map((step: { value: string; label: string }, i: number) => {
         return (
-          <>
-          <Crumb
-            bold={currentStep === step.value}
-            onClick={() => onClickStep(step.value)}
-          >
-            {step.label}
-          </Crumb>
-          {i !== steps.length - 1 && " > "}
-          </>
+          <Fragment key={i}>
+            <Crumb
+              bold={currentStep === step.value}
+              onClick={() => onClickStep && onClickStep(step.value)}
+            >
+              {step.label}
+            </Crumb>
+            {i !== steps.length - 1 && " > "}
+          </Fragment>
         );
       })}
     </StyledBreadcrumb>
@@ -32,10 +32,11 @@ const Breadcrumb: React.FC<Props> = ({ currentStep, steps, onClickStep }) => {
 export default Breadcrumb;
 
 const StyledBreadcrumb = styled.div`
+  color: #aaaabb;
 `;
 
 const Crumb = styled.span<{ bold: boolean }>`
-  font-weight: ${(props) => (props.bold ? "600" : "normal")};
+  font-weight: ${props => props.bold ? "600" : "normal"};
+  color: ${props => props.bold ? "#ffffff" : "#aaaabb"};
   font-size: 13px;
-  cursor: pointer;
-`;
+`;

+ 18 - 1
dashboard/src/main/home/Home.tsx

@@ -198,6 +198,14 @@ class Home extends Component<PropsType, StateType> {
     urlParams.delete("gh_oauth");
     this.getProjects(defaultProjectId);
     this.getMetadata();
+
+    if (
+      !this.context.hasFinishedOnboarding && 
+      this.props.history.location.pathname &&
+      !this.props.history.location.pathname.includes("onboarding")
+    ) {
+      this.context.setCurrentModal("RedirectToOnboardingModal");
+    }
   }
 
   async checkIfProjectHasBilling(projectId: number) {
@@ -223,7 +231,6 @@ class Home extends Component<PropsType, StateType> {
       const res = await api.getOnboardingState("<token>", {}, { project_id });
 
       if (res?.data && res?.data.current_step !== "clean_up") {
-        // this.redirectToOnboarding();
         this.context.setHasFinishedOnboarding(false);
       } else {
         this.context.setHasFinishedOnboarding(true);
@@ -236,6 +243,16 @@ class Home extends Component<PropsType, StateType> {
   // 2. Make sure switching projects shows appropriate initial view (dashboard || provisioner)
   // 3. Make sure initializing from URL (DO oauth) displays the appropriate initial view
   componentDidUpdate(prevProps: PropsType) {
+    if (
+      !this.context.hasFinishedOnboarding && 
+      prevProps.match.url !== this.props.match.url &&
+      this.props.history.location.pathname &&
+      !this.props.history.location.pathname.includes("onboarding") &&
+      !this.props.history.location.pathname.includes("new-project") &&
+      !this.props.history.location.pathname.includes("project-settings")
+    ) {
+      this.context.setCurrentModal("RedirectToOnboardingModal");
+    }
     if (prevProps.currentProject?.id !== this.props.currentProject?.id) {
       this.checkOnboarding();
       this.checkIfProjectHasBilling(this?.context?.currentProject?.id)

+ 9 - 2
dashboard/src/main/home/ModalHandler.tsx

@@ -10,6 +10,7 @@ import NamespaceModal from "./modals/NamespaceModal";
 import DeleteNamespaceModal from "./modals/DeleteNamespaceModal";
 import EditInviteOrCollaboratorModal from "./modals/EditInviteOrCollaboratorModal";
 import AccountSettingsModal from "./modals/AccountSettingsModal";
+import RedirectToOnboardingModal from "./modals/RedirectToOnboardingModal";
 
 import UsageWarningModal from "./modals/UsageWarningModal";
 
@@ -31,8 +32,14 @@ const ModalHandler: React.FC<{
 
   return (
     <>
-      {currentModal === "RedirectToOnboarding" && (
-        <a href="/onboarding/new-project"></a>
+      {currentModal === "RedirectToOnboardingModal" && (
+        <Modal
+          width="600px"
+          height="180px"
+          title="You're almost ready..."
+        >
+          <RedirectToOnboardingModal />
+        </Modal>
       )}
 
       {currentModal === "ClusterInstructionsModal" && (

+ 10 - 4
dashboard/src/main/home/modals/Modal.tsx

@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import styled from "styled-components";
 
 type PropsType = {
-  onRequestClose: () => void;
+  onRequestClose?: () => void;
   width?: string;
   height?: string;
   title?: string;
@@ -26,6 +26,7 @@ export default class Modal extends Component<PropsType, StateType> {
 
   handleClickOutside = (event: any) => {
     if (
+      this.props.onRequestClose &&
       this.wrapperRef &&
       this.wrapperRef.current &&
       !this.wrapperRef.current.contains(event.target)
@@ -39,9 +40,13 @@ export default class Modal extends Component<PropsType, StateType> {
     return (
       <Overlay>
         <StyledModal ref={this.wrapperRef} width={width} height={height}>
-          <CloseButton onClick={this.props.onRequestClose}>
-            <i className="material-icons">close</i>
-          </CloseButton>
+          {
+            this.props.onRequestClose && (
+              <CloseButton onClick={this.props.onRequestClose}>
+                <i className="material-icons">close</i>
+              </CloseButton>
+            )
+          }
           {this.props.title && <ModalTitle>{this.props.title}</ModalTitle>}
           {this.props.children}
         </StyledModal>
@@ -104,6 +109,7 @@ const StyledModal = styled.div`
     props.height ? props.height : "425px"};
   overflow: visible;
   padding: 25px 32px;
+  z-index: 999;
   font-size: 13px;
   border-radius: 10px;
   background: #202227;

+ 62 - 0
dashboard/src/main/home/modals/RedirectToOnboardingModal.tsx

@@ -0,0 +1,62 @@
+import React, { useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+
+import api from "../../../shared/api";
+import Loading from "../../../components/Loading";
+import Heading from "components/form-components/Heading";
+import Helper from "components/form-components/Helper";
+
+const RedirectToOnboardingModal = () => {
+  return (
+    <>
+      <Helper>You need to complete the onboarding process in order to use Porter.</Helper>
+      <ContinueButton href="/onboarding">
+        <i className="material-icons">east</i>
+        Continue Setup
+      </ContinueButton>
+    </>
+  );
+};
+
+export default RedirectToOnboardingModal;
+
+const ContinueButton = styled.a`
+  height: 35px;
+  font-size: 13px;
+  font-weight: 500;
+  font-family: "Work Sans", sans-serif;
+  color: white;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 6px 15px 7px 15px;
+  text-align: left;
+  border: 0;
+  margin-top: 25px;
+  width: 160px;
+  border-radius: 5px;
+  background: #616FEEcc;
+  box-shadow: 0 2px 5px 0 #00000030;
+  cursor: pointer;
+  user-select: none;
+  :focus {
+    outline: 0;
+  }
+  :hover {
+    filter: brightness(120%);
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 14px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 7px;
+    margin-left: -5px;
+    justify-content: center;
+  }
+`;

+ 1 - 1
dashboard/src/main/home/onboarding/Onboarding.tsx

@@ -130,7 +130,7 @@ const ViewWrapper = styled.div`
 const StyledOnboarding = styled.div`
   max-width: 700px;
   width: 50%;
-  z-index: 999;
+  z-index: 1;
   display: flex;
   align-items: center;
   margin-top: -6%;

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

@@ -8,19 +8,27 @@ export type ProviderSelectorProps = {
   selectProvider: (
     provider: SupportedProviders | (SupportedProviders | "external")
   ) => void;
-  enableExternal?: boolean;
-  enableSkip?: boolean;
+  options: {
+    value: string;
+    icon: string;
+    label: string;
+  }[];
 };
 
-const baseOptions = [
+export const registryOptions = [
+  {
+    value: "skip",
+    label: "Skip / I don't know what this is",
+    icon: "",
+  },
   {
     value: "aws",
-    icon: integrationList["aws"]?.icon,
+    icon: integrationList["ecr"]?.icon,
     label: "Amazon Elastic Container Registry (ECR)",
   },
   {
     value: "gcp",
-    icon: integrationList["gcp"]?.icon,
+    icon: integrationList["gcr"]?.icon,
     label: "Google Cloud Registry (GCR)",
   },
   {
@@ -30,69 +38,50 @@ const baseOptions = [
   },
 ];
 
-const skipOption = {
-  value: "skip",
-  label: "Skip / I don't know what this is",
-  icon: "",
-};
-
-const externalOption = {
-  value: "external",
-  icon: integrationList["kubernetes"]?.icon,
-  label: "Link to an existing cluster",
-};
+export const provisionerOptions = [
+  {
+    value: "aws",
+    icon: integrationList["aws"]?.icon,
+    label: "Amazon Web Services (AWS)",
+  },
+  {
+    value: "gcp",
+    icon: integrationList["gcp"]?.icon,
+    label: "Google Cloud Platform (GCP)",
+  },
+  {
+    value: "do",
+    icon: integrationList["do"]?.icon,
+    label: "DigitalOcean (DO)",
+  },
+];
 
-const dummyOption = {
-  value: "dummy",
-  icon: "",
-  label: "Select a cloud provider",
-};
+export const provisionerOptionsWithExternal = [
+  ...provisionerOptions,
+  {
+    value: "external",
+    icon: integrationList["kubernetes"]?.icon,
+    label: "Link to an existing cluster",
+  },
+];
 
 const ProviderSelector: React.FC<ProviderSelectorProps> = ({
   selectProvider,
-  enableExternal,
-  enableSkip,
+  options,
 }) => {
   const [provider, setProvider] = useState(() => {
-    if (enableSkip) {
+    if (options.find((o) => o.value === "skip")) {
       return "skip";
     }
-
     return null;
   });
 
-  const availableOptions = useMemo(() => {
-    let options = [...baseOptions];
-    if (enableSkip) {
-      // Check if dummy option was deleted or not
-      const dummyOptionIndex = options.findIndex((o) => o.value === "dummy");
-      if (dummyOptionIndex >= 0) {
-        options.splice(dummyOptionIndex, 1);
-      }
-      options.unshift(skipOption);
-    } else {
-      // Check if skip option was deleted or not
-      const skipOptionIndex = options.findIndex((o) => o.value === "skip");
-      if (skipOptionIndex >= 0) {
-        options.splice(skipOptionIndex, 1);
-      }
-    }
-
-    if (enableExternal) {
-      if (!options.find((o) => o.value === "external")) {
-        options.push(externalOption);
-      }
-    }
-
-    return [...options];
-  }, [enableSkip, enableExternal]);
-
   return (
     <>
       <Br />
       <Selector
         activeValue={provider}
-        options={availableOptions}
+        options={options}
         placeholder="Select a cloud provider"
         setActiveValue={(provider) => {
           setProvider(provider);

+ 19 - 5
dashboard/src/main/home/onboarding/components/RegistryList.tsx → dashboard/src/main/home/onboarding/components/RegistryImageList.tsx

@@ -1,5 +1,6 @@
 import React, { useEffect, useState } from "react";
 import api from "shared/api";
+import styled from "styled-components";
 
 const RegistryImageList: React.FC<{
   project: {
@@ -31,14 +32,27 @@ const RegistryImageList: React.FC<{
     return () => {};
   }, []);
   return (
-    <div>
-      {imageList.map((data) => (
-        <div>
+    <ImageList>
+      {imageList.map((data, i) => (
+        <ImageRow isLast={i === imageList.length - 1}>
           Name{data.name}, timestamp: {data.created_at}, URI: {data.uri}
-        </div>
+        </ImageRow>
       ))}
-    </div>
+    </ImageList>
   );
 };
 
 export default RegistryImageList;
+
+const ImageRow = styled.div<{ isLast?: boolean }>`
+  width: 100%;
+  height: 30px;
+  border-bottom: ${props => props.isLast ? "" : "1px solid #aaaabb"};
+`;
+
+const ImageList = styled.div`
+  border-radius: 5px;
+  border: 1px solid #aaaabb;
+  background: #ffffff11;
+  margin: 25px 0 30px;
+`;

+ 12 - 1
dashboard/src/main/home/onboarding/state/StepHandler.ts

@@ -93,6 +93,12 @@ const flow: FlowType = {
           url: "/onboarding/registry/test_connection",
           on: {
             continue: "provision_resources",
+            /**
+             * Enable this go_back as soon as connect registry
+             * has a proper way of listing the registries and
+             * manage them inside the step
+             */
+            // go_back: "connect_registry",
           },
         },
       },
@@ -102,7 +108,12 @@ const flow: FlowType = {
       on: {
         skip: "provision_resources.connect_own_cluster",
         continue: "provision_resources.credentials",
-        go_back: "connect_registry",
+        /**
+         * Enable this go_back as soon as connect registry
+         * has a proper way of listing the registries and
+         * manage them inside the step
+         */
+        // go_back: "connect_registry",
       },
       execute: {
         on: {

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

@@ -121,9 +121,9 @@ const decompressState = (prev_state: any) => {
 
   let provision: any = {
     skip: state.skip_resource_provision,
-    provider: state.resource_provision_provider,
+    provider: state.cluster_infra_provider,
     credentials: {
-      id: state.resource_provision_credentials_id,
+      id: state.cluster_infra_credential_id,
     },
     settings: {
       cluster_name: state.resource_provision_settings_cluster_name,

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

@@ -5,7 +5,9 @@ import React from "react";
 import { useParams } from "react-router";
 
 import styled from "styled-components";
-import ProviderSelector from "../../components/ProviderSelector";
+import ProviderSelector, {
+  registryOptions,
+} from "../../components/ProviderSelector";
 import { SupportedProviders } from "../../types";
 import backArrow from "assets/back_arrow.png";
 
@@ -56,7 +58,7 @@ const ConnectRegistry: React.FC<{
           : "Link to an existing Docker registry or continue."}
       </Helper>
 
-      {provider ? (
+      {step ? (
         <FormFlowWrapper
           provider={provider}
           onSaveCredentials={onSaveCredentials}
@@ -64,16 +66,17 @@ const ConnectRegistry: React.FC<{
           onSuccess={onSuccess}
           project={project}
           currentStep={step}
+          goBack={goBack}
         />
       ) : (
         <>
           <ProviderSelector
-            enableSkip
             selectProvider={(provider) => {
               if (provider !== "external") {
                 onSelectProvider(provider);
               }
             }}
+            options={registryOptions}
           />
           <NextStep
             text="Continue"

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

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

+ 6 - 5
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/FormFlow.tsx

@@ -64,6 +64,7 @@ type Props = {
   onSuccess: () => void;
   project: { id: number; name: string };
   currentStep: "credentials" | "settings" | "test_connection";
+  goBack: () => void;
 };
 
 const FormFlowWrapper: React.FC<Props> = ({
@@ -73,6 +74,7 @@ const FormFlowWrapper: React.FC<Props> = ({
   provider,
   project,
   currentStep,
+  goBack,
 }) => {
   const nextFormStep = (
     data?: Partial<Exclude<ConnectedRegistryConfig, SkipRegistryConnection>>
@@ -106,20 +108,19 @@ const FormFlowWrapper: React.FC<Props> = ({
   return (
     <FormWrapper>
       <FormHeader>
-        <CloseButton onClick={() => alert("go back")}>
+        <CloseButton onClick={() => goBack()}>
           <i className="material-icons">keyboard_backspace</i>
         </CloseButton>
-        <img src={FormTitle[provider].icon} />
-        {FormTitle[provider].label}
+        {FormTitle[provider] && <img src={FormTitle[provider].icon} />}
+        {FormTitle[provider] && FormTitle[provider].label}
       </FormHeader>
-      <Breadcrumb 
+      <Breadcrumb
         currentStep={currentStep}
         steps={[
           { value: "credentials", label: "Credentials" },
           { value: "settings", label: "Settings" },
           { value: "test_connection", label: "Test Connection" },
         ]}
-        onClickStep={(step: string) => alert(step)}
       />
       {CurrentForm}
     </FormWrapper>

+ 4 - 2
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_AWSRegistryForm.tsx

@@ -1,5 +1,6 @@
 import InputRow from "components/form-components/InputRow";
 import SelectRow from "components/form-components/SelectRow";
+import Helper from "components/form-components/Helper";
 import SaveButton from "components/SaveButton";
 import { AWSRegistryConfig } from "main/home/onboarding/types";
 import React, { useState } from "react";
@@ -9,7 +10,7 @@ import { useSnapshot } from "valtio";
 import { OFState } from "../../../state/index";
 import IntegrationCategories from "main/home/integrations/IntegrationCategories";
 import { StateHandler } from "main/home/onboarding/state/StateHandler";
-import RegistryImageList from "main/home/onboarding/components/RegistryList";
+import RegistryImageList from "main/home/onboarding/components/RegistryImageList";
 
 const regionOptions = [
   { value: "us-east-1", label: "US East (N. Virginia) us-east-1" },
@@ -193,6 +194,7 @@ export const SettingsForm: React.FC<{
 
   return (
     <>
+      <Helper>Provide a name for Porter to use when displaying your registry.</Helper>
       <InputRow
         type="text"
         value={registryName}
@@ -243,5 +245,5 @@ export const TestRegistryConnection: React.FC<{ nextFormStep: () => void }> = ({
 
 const Br = styled.div`
   width: 100%;
-  height: 10px;
+  height: 15px;
 `;

+ 11 - 1
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_DORegistryForm.tsx

@@ -1,7 +1,9 @@
 import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import SaveButton from "components/SaveButton";
+import RegistryImageList from "main/home/onboarding/components/RegistryImageList";
 import { OFState } from "main/home/onboarding/state";
+import { StateHandler } from "main/home/onboarding/state/StateHandler";
 import { DORegistryConfig } from "main/home/onboarding/types";
 import React, { useEffect, useState } from "react";
 import { useLocation } from "react-router";
@@ -86,6 +88,9 @@ export const SettingsForm: React.FC<{
 
   return (
     <>
+      <Helper>
+        Provide a name for Porter to use when displaying your registry.
+      </Helper>
       <InputRow
         type="text"
         value={registryName}
@@ -127,8 +132,13 @@ export const TestRegistryConnection: React.FC<{
   nextFormStep: () => void;
   project: any;
 }> = ({ nextFormStep }) => {
+  const snap = useSnapshot(StateHandler);
   return (
     <>
+      <RegistryImageList
+        project={snap.project}
+        registry_id={snap.connected_registry.settings.registry_connection_id}
+      />
       <SaveButton
         text="Continue"
         disabled={false}
@@ -144,7 +154,7 @@ export const TestRegistryConnection: React.FC<{
 
 const Br = styled.div`
   width: 100%;
-  height: 10px;
+  height: 15px;
 `;
 
 const CodeBlock = styled.span`

+ 11 - 1
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_GCPRegistryForm.tsx

@@ -2,7 +2,9 @@ import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import UploadArea from "components/form-components/UploadArea";
 import SaveButton from "components/SaveButton";
+import RegistryImageList from "main/home/onboarding/components/RegistryImageList";
 import { OFState } from "main/home/onboarding/state";
+import { StateHandler } from "main/home/onboarding/state/StateHandler";
 import { GCPRegistryConfig } from "main/home/onboarding/types";
 import React, { useState } from "react";
 import api from "shared/api";
@@ -163,6 +165,9 @@ export const SettingsForm: React.FC<{
   };
   return (
     <>
+      <Helper>
+        Provide a name for Porter to use when displaying your registry.
+      </Helper>
       <InputRow
         type="text"
         value={registryName}
@@ -204,8 +209,13 @@ export const TestRegistryConnection: React.FC<{
   nextFormStep: () => void;
   project: any;
 }> = ({ nextFormStep, project }) => {
+  const snap = useSnapshot(StateHandler);
   return (
     <>
+      <RegistryImageList
+        project={snap.project}
+        registry_id={snap.connected_registry.settings.registry_connection_id}
+      />
       <SaveButton
         text="Continue"
         disabled={false}
@@ -221,7 +231,7 @@ export const TestRegistryConnection: React.FC<{
 
 const Br = styled.div`
   width: 100%;
-  height: 10px;
+  height: 15px;
 `;
 
 const CodeBlock = styled.span`

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

@@ -4,7 +4,10 @@ import TitleSection from "components/TitleSection";
 import React from "react";
 import { useParams } from "react-router";
 import styled from "styled-components";
-import ProviderSelector from "../../components/ProviderSelector";
+import ProviderSelector, {
+  provisionerOptions,
+  provisionerOptionsWithExternal,
+} from "../../components/ProviderSelector";
 
 import FormFlowWrapper from "./forms/FormFlow";
 import ConnectExternalCluster from "./forms/_ConnectExternalCluster";
@@ -54,6 +57,7 @@ const ProvisionResources: React.FC<Props> = ({
               onSaveCredentials={onSaveCredentials}
               onSaveSettings={onSaveSettings}
               project={project}
+              goBack={goBack}
             />
           </>
         );
@@ -80,8 +84,11 @@ const ProvisionResources: React.FC<Props> = ({
               selectProvider={(provider) => {
                 onSelectProvider(provider);
               }}
-              enableSkip={false}
-              enableExternal={!shouldProvisionRegistry}
+              options={
+                shouldProvisionRegistry
+                  ? provisionerOptions
+                  : provisionerOptionsWithExternal
+              }
             />
           </>
         );

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

@@ -21,7 +21,11 @@ const ProvisionResourcesWrapper = () => {
       onSaveSettings={(data) => OFState.actions.nextStep("continue", data)}
       onSuccess={() => OFState.actions.nextStep("continue")}
       onSkip={() => OFState.actions.nextStep("skip")}
-      enable_go_back={snap.StepHandler.canGoBack}
+      enable_go_back={
+        snap.StepHandler.canGoBack &&
+        (!snap.StepHandler.isSubFlow ||
+          snap.StepHandler.currentStepName.includes("connect_own_cluster"))
+      }
       goBack={() => OFState.actions.nextStep("go_back")}
     />
   );

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

@@ -62,6 +62,7 @@ type Props = {
   provider: SupportedProviders | "external";
   currentStep: "credentials" | "settings";
   project: { id: number; name: string };
+  goBack: () => void;
 };
 
 const FormFlowWrapper: React.FC<Props> = ({
@@ -70,6 +71,7 @@ const FormFlowWrapper: React.FC<Props> = ({
   provider,
   currentStep,
   project,
+  goBack,
 }) => {
   const nextFormStep = (
     data?: Partial<Exclude<ProvisionerConfig, SkipProvisionConfig>>
@@ -103,11 +105,11 @@ const FormFlowWrapper: React.FC<Props> = ({
   return (
     <FormWrapper>
       <FormHeader>
-        <CloseButton onClick={() => alert("go back")}>
+        <CloseButton onClick={() => goBack()}>
           <i className="material-icons">keyboard_backspace</i>
         </CloseButton>
-        <img src={FormTitle[provider].icon} />
-        {FormTitle[provider].label}
+        {FormTitle[provider] && <img src={FormTitle[provider].icon} />}
+        {FormTitle[provider] && FormTitle[provider].label}
       </FormHeader>
       <Breadcrumb
         currentStep={currentStep}
@@ -115,7 +117,6 @@ const FormFlowWrapper: React.FC<Props> = ({
           { value: "credentials", label: "Credentials" },
           { value: "settings", label: "Settings" },
         ]}
-        onClickStep={(step: string) => alert(step)}
       />
       {CurrentForm}
     </FormWrapper>

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

@@ -289,5 +289,5 @@ export const Status: React.FC<{
 
 const Br = styled.div`
   width: 100%;
-  height: 10px;
+  height: 15px;
 `;

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

@@ -1,6 +1,8 @@
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
 import styled from "styled-components";
 import TabSelector from "components/TabSelector";
+import api from "shared/api";
+import SaveButton from "components/SaveButton";
 
 type Props = {
   nextStep: () => void;
@@ -19,6 +21,36 @@ const tabOptions = [{ label: "MacOS", value: "mac" }];
 const ConnectExternalCluster: React.FC<Props> = ({ nextStep, project }) => {
   const [currentPage, setCurrentPage] = useState(0);
   const [currentTab, setCurrentTab] = useState("mac");
+  const [enableContinue, setEnableContinue] = useState(false);
+
+  const getClusters = async (
+    status: { isSubscribed: boolean },
+    retryCount = 0
+  ) => {
+    try {
+      api.getClusters("<token>", {}, { id: project.id }).then((res) => {
+        if (Array.isArray(res.data) && res.data.length > 0) {
+          if (status.isSubscribed) {
+            setEnableContinue(true);
+          }
+        } else {
+          if (status.isSubscribed) {
+            setTimeout(() => {
+              getClusters(status, retryCount + 1);
+            }, 1000);
+          }
+        }
+      });
+    } catch (error) {}
+  };
+
+  useEffect(() => {
+    let status = { isSubscribed: true };
+    getClusters(status);
+    return () => {
+      status.isSubscribed = false;
+    };
+  }, []);
 
   const renderPage = () => {
     switch (currentPage) {
@@ -118,12 +150,26 @@ const ConnectExternalCluster: React.FC<Props> = ({ nextStep, project }) => {
           arrow_forward
         </i>
       </PageSection>
+      <NextStep
+        text="Continue"
+        disabled={!enableContinue}
+        onClick={() => nextStep()}
+        status={""}
+        makeFlush={true}
+        clearPosition={true}
+        statusPosition="right"
+        saveText=""
+      />
     </StyledClusterInstructionsModal>
   );
 };
 
 export default ConnectExternalCluster;
 
+const NextStep = styled(SaveButton)`
+  margin-top: 24px;
+`;
+
 const PageCount = styled.div`
   margin-right: 9px;
   user-select: none;

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

@@ -243,7 +243,7 @@ export const Status: React.FC<{
 
 const Br = styled.div`
   width: 100%;
-  height: 10px;
+  height: 15px;
 `;
 
 const CodeBlock = styled.span`

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

@@ -265,7 +265,7 @@ export const Status: React.FC<{
 
 const Br = styled.div`
   width: 100%;
-  height: 10px;
+  height: 15px;
 `;
 
 const CodeBlock = styled.span`