Преглед изворни кода

Added functions to provisioner form flow

jnfrati пре 4 година
родитељ
комит
3e0295d6aa

+ 7 - 5
dashboard/src/main/home/onboarding/state/StateHandler.ts

@@ -15,6 +15,12 @@ export type ConnectedRegistryConfig =
   | DORegistryConfig
   | SkipRegistryConnection;
 
+export type ProvisionerConfig =
+  | AWSProvisionerConfig
+  | GCPProvisionerConfig
+  // | DOProvisionerConfig
+  | SkipProvisionConfig;
+
 export type ProjectData = {
   id: number;
   name: string;
@@ -28,11 +34,7 @@ export type OnboardingState = {
   project: ProjectData | null;
   connected_source: ConnectedSourceData | null;
   connected_registry: ConnectedRegistryConfig | null;
-  provision_resources:
-    | GCPProvisionerConfig
-    | AWSProvisionerConfig
-    | SkipProvisionConfig
-    | null;
+  provision_resources: ProvisionerConfig | null;
   actions: {
     restoreState: (state: OnboardingState) => void;
     clearState: () => void;

+ 19 - 15
dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResourcesState.ts

@@ -1,29 +1,33 @@
 import { proxy } from "valtio";
-import { SupportedProviders } from "../../types";
+import { ProvisionerConfig } from "../../state/StateHandler";
+import { SkipProvisionConfig, SupportedProviders } from "../../types";
 
-type AllowedSteps = "credentials" | "settings" | "test_connection" | null;
+type AllowedSteps = "credentials" | "settings" | "status" | null;
 
 interface ConnectRegistryState {
   selectedProvider: SupportedProviders | null;
   currentStep: AllowedSteps;
-  actions: typeof actions;
+  config: Partial<Exclude<ProvisionerConfig, SkipProvisionConfig>> | null;
+  actions: {
+    selectProvider: (provider: SupportedProviders) => void;
+    clearState: () => void;
+  };
 }
 
-const actions = {
-  selectProvider(provider: SupportedProviders) {
-    State.selectedProvider = provider;
-  },
-
-  clearState() {
-    State.selectedProvider = null;
-    State.currentStep = "credentials";
-  },
-};
-
 const initialState: ConnectRegistryState = {
   selectedProvider: null,
   currentStep: "credentials",
-  actions,
+  config: null,
+  actions: {
+    selectProvider(provider: SupportedProviders) {
+      State.selectedProvider = provider;
+    },
+
+    clearState() {
+      State.selectedProvider = null;
+      State.currentStep = "credentials";
+    },
+  },
 };
 
 export const State = proxy(initialState);

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

@@ -0,0 +1,112 @@
+import { ProvisionerConfig } from "main/home/onboarding/state/StateHandler";
+import { SkipProvisionConfig } from "main/home/onboarding/types";
+import React, { useContext, useMemo } from "react";
+import { Context } from "shared/Context";
+import styled from "styled-components";
+import { useSnapshot } from "valtio";
+import { State } from "../ProvisionResourcesState";
+import {
+  CredentialsForm as AWSCredentialsForm,
+  SettingsForm as AWSSettingsForm,
+  Status as AWSProvisionerStatus,
+} from "./_AWSProvsionerForm";
+
+import {
+  CredentialsForm as DOCredentialsForm,
+  SettingsForm as DOSettingsForm,
+  Status as DOProvisionerStatus,
+} from "./_DOProvisionerForm";
+
+import {
+  CredentialsForm as GCPCredentialsForm,
+  SettingsForm as GCPSettingsForm,
+  Status as GCPProvisionerStatus,
+} from "./_GCPProvisionerForm";
+
+const Forms = {
+  aws: {
+    credentials: AWSCredentialsForm,
+    settings: AWSSettingsForm,
+    status: AWSProvisionerStatus,
+  },
+  gcp: {
+    credentials: GCPCredentialsForm,
+    settings: GCPSettingsForm,
+    status: GCPProvisionerStatus,
+  },
+  do: {
+    credentials: DOCredentialsForm,
+    settings: DOSettingsForm,
+    status: DOProvisionerStatus,
+  },
+};
+
+const FormTitle = {
+  aws: "Amazon Web Services (AWS)",
+  gcp: "Google Cloud Platform  (GCP)",
+  do: "Digital Ocean",
+};
+
+type Props = {
+  nextStep: () => void;
+};
+
+const FormFlowWrapper: React.FC<Props> = ({ nextStep }) => {
+  const snap = useSnapshot(State);
+  const { currentProject } = useContext(Context);
+
+  const nextFormStep = (
+    data?: Partial<Exclude<ProvisionerConfig, SkipProvisionConfig>>
+  ) => {
+    if (snap.currentStep === "credentials") {
+      State.config.credentials = data.credentials;
+      State.currentStep = "settings";
+    } else if (snap.currentStep === "settings") {
+      State.config.settings = data.settings;
+      State.currentStep = "status";
+    } else if (snap.currentStep === "status") {
+      nextStep();
+    }
+  };
+
+  const CurrentForm = useMemo(() => {
+    const providerSteps = Forms[snap.selectedProvider];
+    if (!providerSteps) {
+      return null;
+    }
+
+    const currentForm = providerSteps[snap.currentStep];
+    if (!currentForm) {
+      return null;
+    }
+
+    return React.createElement(currentForm as any, {
+      nextFormStep,
+      project: currentProject,
+    });
+  }, [snap.currentStep, snap.selectedProvider]);
+
+  return (
+    <>
+      {FormTitle[snap.selectedProvider]}
+      <Breadcrumb>
+        <Text bold={snap.currentStep === "credentials"}>Credentials</Text>
+        {" > "}
+        <Text bold={snap.currentStep === "settings"}>Settings</Text>
+        {" > "}
+        <Text bold={snap.currentStep === "status"}>Status</Text>
+      </Breadcrumb>
+      {CurrentForm}
+    </>
+  );
+};
+
+export default FormFlowWrapper;
+
+const Text = styled.span<{ bold: boolean }>`
+  font-weight: ${(props) => (props.bold ? "600" : "normal")};
+`;
+
+const Breadcrumb = styled.div`
+  margin: 0 10px;
+`;

+ 272 - 0
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_AWSProvsionerForm.tsx

@@ -0,0 +1,272 @@
+import InputRow from "components/form-components/InputRow";
+import SelectRow from "components/form-components/SelectRow";
+import SaveButton from "components/SaveButton";
+import {
+  AWSProvisionerConfig,
+  AWSRegistryConfig,
+} from "main/home/onboarding/types";
+import React, { useState } from "react";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { useSnapshot } from "valtio";
+import { State } from "../ProvisionResourcesState";
+
+const regionOptions = [
+  { value: "us-east-1", label: "US East (N. Virginia) us-east-1" },
+  { value: "us-east-2", label: "US East (Ohio) us-east-2" },
+  { value: "us-west-1", label: "US West (N. California) us-west-1" },
+  { value: "us-west-2", label: "US West (Oregon) us-west-2" },
+  { value: "af-south-1", label: "Africa (Cape Town) af-south-1" },
+  { value: "ap-east-1", label: "Asia Pacific (Hong Kong) ap-east-1" },
+  { value: "ap-south-1", label: "Asia Pacific (Mumbai) ap-south-1" },
+  { value: "ap-northeast-2", label: "Asia Pacific (Seoul) ap-northeast-2" },
+  { value: "ap-southeast-1", label: "Asia Pacific (Singapore) ap-southeast-1" },
+  { value: "ap-southeast-2", label: "Asia Pacific (Sydney) ap-southeast-2" },
+  { value: "ap-northeast-1", label: "Asia Pacific (Tokyo) ap-northeast-1" },
+  { value: "ca-central-1", label: "Canada (Central) ca-central-1" },
+  { value: "eu-central-1", label: "Europe (Frankfurt) eu-central-1" },
+  { value: "eu-west-1", label: "Europe (Ireland) eu-west-1" },
+  { value: "eu-west-2", label: "Europe (London) eu-west-2" },
+  { value: "eu-south-1", label: "Europe (Milan) eu-south-1" },
+  { value: "eu-west-3", label: "Europe (Paris) eu-west-3" },
+  { value: "eu-north-1", label: "Europe (Stockholm) eu-north-1" },
+  { value: "me-south-1", label: "Middle East (Bahrain) me-south-1" },
+  { value: "sa-east-1", label: "South America (São Paulo) sa-east-1" },
+];
+
+export const CredentialsForm: React.FC<{
+  nextFormStep: (data: Partial<AWSRegistryConfig>) => void;
+  project: any;
+}> = ({ nextFormStep, project }) => {
+  const [accessId, setAccessId] = useState("");
+  const [secretKey, setSecretKey] = useState("");
+  const [awsRegion, setAWSRegion] = useState("us-east-1");
+  const [buttonStatus, setButtonStatus] = useState("");
+
+  const validate = () => {
+    if (!accessId) {
+      return {
+        hasError: true,
+        error: "Access ID cannot be empty",
+      };
+    }
+    if (!secretKey) {
+      return {
+        hasError: true,
+        error: "AWS Secret key cannot be empty",
+      };
+    }
+    return {
+      hasError: false,
+      error: "",
+    };
+  };
+
+  const submit = async () => {
+    const validation = validate();
+    if (validation.hasError) {
+      setButtonStatus(validation.error);
+      return;
+    }
+
+    const res = await api.createAWSIntegration(
+      "token",
+      {
+        aws_region: awsRegion,
+        aws_access_key_id: accessId,
+        aws_secret_access_key: secretKey,
+      },
+      {
+        id: project.id,
+      }
+    );
+
+    nextFormStep({
+      credentials: {
+        id: res.data.id,
+      },
+    });
+  };
+
+  return (
+    <>
+      <InputRow
+        type="text"
+        value={accessId}
+        setValue={(x: string) => {
+          setAccessId(x);
+        }}
+        label="👤 AWS Access ID"
+        placeholder="ex: AKIAIOSFODNN7EXAMPLE"
+        width="100%"
+        isRequired={true}
+      />
+      <InputRow
+        type="password"
+        value={secretKey}
+        setValue={(x: string) => {
+          setSecretKey(x);
+        }}
+        label="🔒 AWS Secret Key"
+        placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+        width="100%"
+        isRequired={true}
+      />
+      <SelectRow
+        options={regionOptions}
+        width="100%"
+        value={awsRegion}
+        dropdownMaxHeight="240px"
+        setActiveValue={(x: string) => {
+          setAWSRegion(x);
+        }}
+        label="📍 AWS Region"
+      />
+      <SaveButton
+        text="Continue"
+        disabled={false}
+        onClick={submit}
+        makeFlush={true}
+        clearPosition={true}
+        status={buttonStatus}
+        statusPosition={"right"}
+      />
+    </>
+  );
+};
+
+const machineTypeOptions = [
+  { value: "t2.medium", label: "t2.medium" },
+  { value: "t2.xlarge", label: "t2.xlarge" },
+  { value: "t2.2xlarge", label: "t2.2xlarge" },
+  { value: "t3.medium", label: "t3.medium" },
+  { value: "t3.xlarge", label: "t3.xlarge" },
+  { value: "t3.2xlarge", label: "t3.2xlarge" },
+];
+
+export const SettingsForm: React.FC<{
+  nextFormStep: (data: Partial<AWSProvisionerConfig>) => void;
+  project: any;
+}> = ({ nextFormStep, project }) => {
+  const snap = useSnapshot(State);
+  const [clusterName, setClusterName] = useState("");
+  const [machineType, setMachineType] = useState("t2.medium");
+  const [buttonStatus, setButtonStatus] = useState("");
+
+  const validate = () => {
+    if (!clusterName) {
+      return {
+        hasError: true,
+        error: "Registry name cannot be empty",
+      };
+    }
+    return {
+      hasError: false,
+      error: "",
+    };
+  };
+
+  const catchError = (error: any) => {
+    console.error(error);
+  };
+
+  const provisionECR = async (awsIntegrationId: string) => {
+    console.log("Started provision ECR");
+
+    try {
+      await api.provisionECR(
+        "<token>",
+        {
+          aws_integration_id: awsIntegrationId,
+          ecr_name: `${project.name}-registry`,
+        },
+        { id: project.id }
+      );
+    } catch (error) {
+      catchError(error);
+    }
+  };
+
+  const provisionEKS = async (awsIntegrationId: string) => {
+    try {
+      await api.provisionEKS(
+        "<token>",
+        {
+          aws_integration_id: awsIntegrationId,
+          eks_name: clusterName,
+          machine_type: machineType,
+        },
+        { id: project.id }
+      );
+    } catch (error) {
+      catchError(error);
+    }
+  };
+
+  const submit = async () => {
+    const validation = validate();
+    if (validation.hasError) {
+      setButtonStatus(validation.error);
+      return;
+    }
+
+    nextFormStep({
+      settings: {
+        cluster_name: clusterName,
+        aws_machine_type: machineType,
+      },
+    });
+  };
+
+  return (
+    <>
+      <InputRow
+        type="text"
+        value={clusterName}
+        setValue={(x) => {
+          setClusterName(String(x));
+        }}
+        label="🏷️ Registry Name"
+        placeholder="ex: porter-awesome-registry"
+        width="100%"
+      />
+      <SelectRow
+        options={machineTypeOptions}
+        width="100%"
+        value={machineType}
+        dropdownMaxHeight="240px"
+        setActiveValue={(x: string) => {
+          setMachineType(x);
+        }}
+        label="⚙️ AWS Machine Type"
+      />
+      <SaveButton
+        text="Provision resources"
+        disabled={false}
+        onClick={submit}
+        makeFlush={true}
+        clearPosition={true}
+        status={buttonStatus}
+        statusPosition={"right"}
+      />
+    </>
+  );
+};
+
+export const Status: React.FC<{ nextFormStep: () => void }> = ({
+  nextFormStep,
+}) => {
+  return (
+    <>
+      <SaveButton
+        text="Continue"
+        disabled={false}
+        onClick={nextFormStep}
+        makeFlush={true}
+        clearPosition={true}
+        status={""}
+        statusPosition={"right"}
+      />
+    </>
+  );
+};

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

@@ -0,0 +1,202 @@
+import Helper from "components/form-components/Helper";
+import InputRow from "components/form-components/InputRow";
+import SelectRow from "components/form-components/SelectRow";
+import SaveButton from "components/SaveButton";
+import { DORegistryConfig } from "main/home/onboarding/types";
+import React, { useContext, useEffect, useState } from "react";
+import api from "shared/api";
+import styled from "styled-components";
+import { useSnapshot } from "valtio";
+import { State } from "../ProvisionResourcesState";
+
+const tierOptions = [
+  { value: "basic", label: "Basic" },
+  { value: "professional", label: "Professional" },
+];
+
+const regionOptions = [
+  { value: "ams3", label: "Amsterdam 3" },
+  { value: "blr1", label: "Bangalore 1" },
+  { value: "fra1", label: "Frankfurt 1" },
+  { value: "lon1", label: "London 1" },
+  { value: "nyc1", label: "New York 1" },
+  { value: "nyc3", label: "New York 3" },
+  { value: "sfo2", label: "San Francisco 2" },
+  { value: "sfo3", label: "San Francisco 3" },
+  { value: "sgp1", label: "Singapore 1" },
+  { value: "tor1", label: "Toronto 1" },
+];
+
+/**
+ * This will redirect to DO, and we should pass the redirection URI to be /onboarding/registry?provider=do
+ *
+ * After the oauth flow comes back, the first render will go and check if it exists a integration_id for DO in the
+ * current onboarding project, after getting it, the CredentialsForm will use nextFormStep to save the onboarding state.
+ *
+ * If it happens to be an error, it will be shown with the default error handling through the modal.
+ */
+export const CredentialsForm: React.FC<{
+  nextFormStep: (data: Partial<DORegistryConfig>) => void;
+  project: any;
+}> = ({ nextFormStep, project }) => {
+  useEffect(() => {
+    api.getOAuthIds("<token>", {}, { project_id: project?.id }).then((res) => {
+      let tgtIntegration = res.data.find((integration: any) => {
+        return integration.client === "do";
+      });
+
+      if (tgtIntegration) {
+        nextFormStep({
+          credentials: {
+            id: tgtIntegration.id,
+          },
+        });
+      }
+    });
+  }, []);
+  return (
+    <>
+      <ConnectDigitalOceanButton
+        target={"_blank"}
+        href={`/api/projects/${project?.id}/oauth/digitalocean`}
+      >
+        Connect Digital Ocean
+      </ConnectDigitalOceanButton>
+    </>
+  );
+};
+
+export const SettingsForm: React.FC<{
+  nextFormStep: (data: Partial<DORegistryConfig>) => void;
+  project: any;
+}> = ({ nextFormStep, project }) => {
+  const [registryUrl, setRegistryUrl] = useState("basic");
+  const [registryName, setRegistryName] = useState("");
+  const [buttonStatus] = useState("");
+  const snap = useSnapshot(State);
+
+  const submit = async () => {
+    await api.connectDORegistry(
+      "<token>",
+      {
+        name: registryName,
+        do_integration_id: snap.config.credentials.id,
+        url: registryUrl,
+      },
+      { project_id: project.id }
+    );
+    nextFormStep({
+      settings: {
+        registry_url: registryUrl,
+      },
+    });
+  };
+
+  return (
+    <>
+      <InputRow
+        type="text"
+        value={registryName}
+        setValue={(registryName: string) => setRegistryName(registryName)}
+        isRequired={true}
+        label="🏷️ Registry Name"
+        placeholder="ex: paper-straw"
+        width="100%"
+      />
+      <Helper>
+        DOC R URI, in the form{" "}
+        <CodeBlock>registry.digitalocean.com/[REGISTRY_NAME]</CodeBlock>. For
+        example, <CodeBlock>registry.digitalocean.com/porter-test</CodeBlock>.
+      </Helper>
+      <InputRow
+        type="text"
+        value={registryUrl}
+        setValue={(url: string) => setRegistryUrl(url)}
+        label="🔗 GCR URL"
+        placeholder="ex: registry.digitalocean.com/porter-test"
+        width="100%"
+        isRequired={true}
+      />
+      <SaveButton
+        text="Connect Registry"
+        disabled={false}
+        onClick={submit}
+        makeFlush={true}
+        clearPosition={true}
+        status={buttonStatus}
+        statusPosition={"right"}
+      />
+    </>
+  );
+};
+
+export const Status: React.FC<{
+  nextFormStep: () => void;
+  project: any;
+}> = ({ nextFormStep }) => {
+  return (
+    <>
+      <SaveButton
+        text="Continue"
+        disabled={false}
+        onClick={nextFormStep}
+        makeFlush={true}
+        clearPosition={true}
+        status={""}
+        statusPosition={"right"}
+      />
+    </>
+  );
+};
+
+const CodeBlock = styled.span`
+  display: inline-block;
+  background-color: #1b1d26;
+  color: white;
+  border-radius: 5px;
+  font-family: monospace;
+  padding: 2px 3px;
+  margin-top: -2px;
+  user-select: text;
+`;
+
+const ConnectDigitalOceanButton = styled.a`
+  width: 200px;
+  justify-content: center;
+  border-radius: 5px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  color: white;
+  font-weight: 500;
+  padding: 10px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  box-shadow: 0 5px 8px 0px #00000010;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;

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

@@ -0,0 +1,215 @@
+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 { GCPRegistryConfig } from "main/home/onboarding/types";
+import React, { useState } from "react";
+import api from "shared/api";
+import styled from "styled-components";
+import { useSnapshot } from "valtio";
+import { State } from "../ProvisionResourcesState";
+
+export const CredentialsForm: React.FC<{
+  nextFormStep: (data: Partial<GCPRegistryConfig>) => void;
+  project: any;
+}> = ({ nextFormStep, project }) => {
+  const [projectId, setProjectId] = useState("");
+  const [serviceAccountKey, setServiceAccountKey] = useState("");
+  const [buttonStatus, setButtonStatus] = useState("");
+
+  const validate = () => {
+    if (!projectId) {
+      return { hasError: true, error: "Project ID cannot be empty" };
+    }
+
+    if (!serviceAccountKey) {
+      return { hasError: true, error: "GCP Key Data cannot be empty" };
+    }
+    return {
+      hasError: false,
+      error: "",
+    };
+  };
+
+  const submit = async () => {
+    const validation = validate();
+
+    if (validation.hasError) {
+      setButtonStatus(validation.error);
+      return;
+    }
+    setButtonStatus("loading");
+    const gcpIntegration = await api
+      .createGCPIntegration(
+        "<token>",
+        {
+          gcp_region: "",
+          gcp_key_data: serviceAccountKey,
+          gcp_project_id: projectId,
+        },
+        { project_id: project.id }
+      )
+      .then((res) => res.data);
+
+    nextFormStep({
+      credentials: {
+        id: gcpIntegration.id,
+      },
+    });
+  };
+  return (
+    <>
+      <InputRow
+        type="text"
+        value={projectId}
+        setValue={(x: string) => {
+          setProjectId(x);
+        }}
+        label="🏷️ GCP Project ID"
+        placeholder="ex: blindfold-ceiling-24601"
+        width="100%"
+        isRequired={true}
+      />
+
+      <Helper>Service account credentials for GCP permissions.</Helper>
+      <UploadArea
+        setValue={(x: any) => setServiceAccountKey(x)}
+        label="🔒 GCP Key Data (JSON)"
+        placeholder="Choose a file or drag it here."
+        width="100%"
+        height="100%"
+        isRequired={true}
+      />
+      <SaveButton
+        text="Continue"
+        disabled={false}
+        onClick={submit}
+        makeFlush={true}
+        clearPosition={true}
+        status={buttonStatus}
+        statusPosition={"right"}
+      />
+    </>
+  );
+};
+
+export const SettingsForm: React.FC<{
+  nextFormStep: (data: Partial<GCPRegistryConfig>) => void;
+  project: any;
+}> = ({ nextFormStep, project }) => {
+  const [registryName, setRegistryName] = useState("");
+  const [registryUrl, setRegistryUrl] = useState("");
+  const [buttonStatus, setButtonStatus] = useState("");
+  const snap = useSnapshot(State);
+
+  const validate = () => {
+    if (!registryName) {
+      return {
+        hasError: true,
+        error: "Registry Name cannot be empty",
+      };
+    }
+    if (!registryUrl) {
+      return {
+        hasError: true,
+        error: "Registry Url cannot be empty",
+      };
+    }
+    return { hasError: false, error: "" };
+  };
+
+  const submit = async () => {
+    const validation = validate();
+
+    if (validation.hasError) {
+      setButtonStatus(validation.error);
+      return;
+    }
+
+    setButtonStatus("loading");
+
+    await api.connectGCRRegistry(
+      "<token>",
+      {
+        name: registryName,
+        gcp_integration_id: snap.config.credentials.id,
+        url: registryUrl,
+      },
+      {
+        id: project.id,
+      }
+    );
+    nextFormStep({
+      settings: {
+        gcr_url: registryUrl,
+        registry_name: registryName,
+      },
+    });
+  };
+  return (
+    <>
+      <InputRow
+        type="text"
+        value={registryName}
+        setValue={(name: string) => setRegistryName(name)}
+        isRequired={true}
+        label="🏷️ Registry Name"
+        placeholder="ex: paper-straw"
+        width="100%"
+      />
+      <Helper>
+        GCR URI, in the form{" "}
+        <CodeBlock>[gcr_domain]/[gcp_project_id]</CodeBlock>. For example,{" "}
+        <CodeBlock>gcr.io/skynet-dev-172969</CodeBlock>.
+      </Helper>
+      <InputRow
+        type="text"
+        value={registryUrl}
+        setValue={(url: string) => setRegistryUrl(url)}
+        label="🔗 GCR URL"
+        placeholder="ex: gcr.io/skynet-dev-172969"
+        width="100%"
+        isRequired={true}
+      />
+      <SaveButton
+        text="Continue"
+        disabled={false}
+        onClick={submit}
+        makeFlush={true}
+        clearPosition={true}
+        status={buttonStatus}
+        statusPosition={"right"}
+      />
+    </>
+  );
+};
+
+export const Status: React.FC<{
+  nextFormStep: () => void;
+  project: any;
+}> = ({ nextFormStep, project }) => {
+  return (
+    <>
+      <SaveButton
+        text="Continue"
+        disabled={false}
+        onClick={nextFormStep}
+        makeFlush={true}
+        clearPosition={true}
+        status={""}
+        statusPosition={"right"}
+      />
+    </>
+  );
+};
+
+const CodeBlock = styled.span`
+  display: inline-block;
+  background-color: #1b1d26;
+  color: white;
+  border-radius: 5px;
+  font-family: monospace;
+  padding: 2px 3px;
+  margin-top: -2px;
+  user-select: text;
+`;

+ 0 - 1
dashboard/src/main/home/onboarding/types.ts

@@ -53,7 +53,6 @@ export type AWSProvisionerConfig = {
   };
   settings: {
     cluster_name: string;
-    aws_region: string;
     aws_machine_type: string;
   };
 };