Răsfoiți Sursa

Create cluster list (#3735)

sdess09 2 ani în urmă
părinte
comite
02e95e4802

+ 39 - 2
dashboard/src/components/CloudFormationForm.tsx

@@ -69,6 +69,41 @@ const CloudFormationForm: React.FC<Props> = ({
     }
   };
 
+  const checkCloudFormation = async () => {
+    try {
+      if (currentProject == null) {
+        return false;
+      };
+      let externalId = getExternalId();
+      let targetARN = `arn:aws:iam::${AWSAccountID}:role/porter-manager`
+      await api
+        .createAWSIntegration(
+          "<token>",
+          {
+            aws_target_arn: targetARN,
+            aws_external_id: externalId,
+          },
+          {
+            id: currentProject.id,
+          }
+        );
+      setPreflightData({
+        "Msg": {
+          "preflight_checks": {
+            cloudFormation: {},
+          }
+        }
+      })
+      console.log("true")
+
+      return true;
+
+    } catch (err) {
+      console.log("false")
+      return false
+    }
+  }
+
   const { data: canProceed } = useQuery(
     ["createAWSIntegration", currentStep, hasClickedCloudformationButton, AWSAccountID],
     async () => {
@@ -122,14 +157,16 @@ const CloudFormationForm: React.FC<Props> = ({
 
   const handleAWSAccountIDChange = (accountId: string) => {
     setAWSAccountID(accountId);
+    setPreflightData(undefined);
     setHasClickedCloudformationButton(false);
     if (accountId === "open-sesame") {
       switchToCredentialFlow();
     }
   };
 
-  const handleContinueWithAWSAccountId = () => {
-    setCurrentStep(2);
+  const handleContinueWithAWSAccountId = async () => {
+    const cloudFormationCheck = await checkCloudFormation();
+    cloudFormationCheck ? setCurrentStep(3) : setCurrentStep(2);
     markStepStarted({ step: "aws-account-id-complete", account_id: AWSAccountID });
   }
 

+ 17 - 9
dashboard/src/components/ProvisionerSettings.tsx

@@ -99,6 +99,7 @@ type Props = RouteComponentProps & {
   provisionerError?: string;
   credentialId: string;
   clusterId?: number;
+  closeModal?: () => void;
 };
 
 const ProvisionerSettings: React.FC<Props> = (props) => {
@@ -260,6 +261,7 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
     setIsLoading(true);
     setIsClicked(true);
 
+
     let loadBalancerObj = new LoadBalancer({});
     loadBalancerObj.loadBalancerType = LoadBalancerType.NLB;
     if (loadBalancerType) {
@@ -389,6 +391,11 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
           console.error(err);
         });
       // }
+      {
+        props?.closeModal &&
+          props?.closeModal()
+      };
+
       setErrorMessage(undefined);
     } catch (err) {
       const errMessage = err.response.data?.error.replace("unknown: ", "");
@@ -409,7 +416,7 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
         setErrorMessage(DEFAULT_ERROR_MESSAGE);
       }
       markStepStarted("provisioning-failed", errMessage);
-      
+
       // enable edit again only in the case of an error
       setIsClicked(false);
       setIsReadOnly(false);
@@ -574,13 +581,13 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
             <>
               {user?.isPorterUser && (
                 <Input
-                width="350px"
-                type="string"
-                value={clusterVersion}
-                disabled={true}
-                setValue={(x: string) => setCidrRangeServices(x)}
-                label="Cluster version (only shown to porter.run emails)"
-              />
+                  width="350px"
+                  type="string"
+                  value={clusterVersion}
+                  disabled={true}
+                  setValue={(x: string) => setCidrRangeServices(x)}
+                  label="Cluster version (only shown to porter.run emails)"
+                />
 
               )}
               <Spacer y={1} />
@@ -1058,7 +1065,8 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
               <Spacer y={1} />
               <Button
                 // disabled={isDisabled()}
-                disabled={isDisabled() || preflightFailed || isLoading}
+                // disabled={isDisabled() || preflightFailed || isLoading}
+                disabled={preflightFailed || isLoading}
                 onClick={createCluster}
                 status={getStatus()}
               >

+ 20 - 6
dashboard/src/main/home/modals/UpdateClusterModal.tsx

@@ -5,6 +5,9 @@ import close from "assets/close.png";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { pushFiltered } from "shared/routing";
+import { OFState } from "main/home/onboarding/state";
+import { Onboarding as OnboardingSaveType } from "../onboarding/types"
+
 
 import SaveButton from "components/SaveButton";
 import InputRow from "components/form-components/InputRow";
@@ -79,12 +82,23 @@ class UpdateClusterModal extends Component<PropsType, StateType> {
             .catch(console.log);
 
           if (currentProject.simplified_view_enabled) {
-            await api.saveOnboardingState(
-              "<token>",
-              { current_step: "connect_source" },
-              { project_id: currentProject.id }
-            );
-            window.location.reload();
+            await api
+              .getClusters("<token>", {}, { id: currentProject?.id })
+              .then(async (res) => {
+                if (res.data) {
+                  let clusters = res.data;
+
+                  if (!currentProject.multi_cluster || clusters?.length() == 1) {
+                    await api.saveOnboardingState(
+                      "<token>",
+                      { current_step: "connect_source" },
+                      { project_id: currentProject.id }
+                    );
+                    window.location.reload();
+                  }
+
+                }
+              })
           }
           return;
         }

+ 167 - 0
dashboard/src/main/home/sidebar/AddCluster/AWSCredentialForm.tsx

@@ -0,0 +1,167 @@
+import React, { useContext, useEffect, useState } from "react";
+import InputRow from "components/form-components/InputRow";
+import SelectRow from "components/form-components/SelectRow";
+import SaveButton from "components/SaveButton";
+
+import { Context } from "shared/Context";
+import api from "shared/api";
+import styled from "styled-components";
+import Loading from "components/Loading";
+import { Operation, OperationStatus, OperationType } from "shared/types";
+import { readableDate } from "shared/string_utils";
+import Placeholder from "components/OldPlaceholder";
+
+type Props = {
+  setCreatedCredential: (aws_integration_id: number) => void;
+  cancel: () => void;
+};
+
+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" },
+];
+
+const AWSCredentialForm: React.FunctionComponent<Props> = ({
+  setCreatedCredential,
+}) => {
+  const { currentProject, setCurrentError } = useContext(Context);
+  const [accessId, setAccessId] = useState("");
+  const [secretKey, setSecretKey] = useState("");
+  const [assumeRoleArn, setAssumeRoleArn] = useState("");
+  const [buttonStatus, setButtonStatus] = useState("");
+  const [awsRegion, setAWSRegion] = useState("us-east-1");
+  const [isLoading, setIsLoading] = useState(false);
+  const [hasError, setHasError] = useState(false);
+
+  const submit = () => {
+    setIsLoading(true);
+
+    api
+      .createAWSIntegration(
+        "<token>",
+        {
+          aws_region: awsRegion,
+          aws_access_key_id: accessId,
+          aws_secret_access_key: secretKey,
+          aws_assume_role_arn: assumeRoleArn,
+        },
+        {
+          id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        setCreatedCredential(data.id);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setHasError(true);
+        setCurrentError(err.response?.data?.error);
+        setIsLoading(false);
+      });
+  };
+
+  if (hasError) {
+    return <Placeholder>Error</Placeholder>;
+  }
+
+  if (isLoading) {
+    return (
+      <Placeholder>
+        <Loading />
+      </Placeholder>
+    );
+  }
+
+  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%"
+        scrollBuffer={true}
+        value={awsRegion}
+        dropdownMaxHeight="240px"
+        setActiveValue={(x: string) => {
+          setAWSRegion(x);
+        }}
+        label="📍 AWS Region"
+      />
+      <InputRow
+        type="text"
+        value={assumeRoleArn}
+        setValue={(x: string) => {
+          setAssumeRoleArn(x);
+        }}
+        label="👤 (Optional) AWS Assume Role ARN"
+        placeholder="ex: arn:aws:iam::01234567890:role/my_assumed_role"
+        width="100%"
+        isRequired={false}
+      />
+      <Flex>
+        <SaveButton
+          text="Continue"
+          disabled={false}
+          onClick={submit}
+          makeFlush={true}
+          clearPosition={true}
+          status={buttonStatus}
+          statusPosition={"right"}
+        />
+      </Flex>
+    </>
+  );
+};
+
+export default AWSCredentialForm;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  margin-top: 30px;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;

+ 111 - 0
dashboard/src/main/home/sidebar/AddCluster/AWSCredentialList.tsx

@@ -0,0 +1,111 @@
+import React, { useContext, useEffect, useState } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import styled from "styled-components";
+import Loading from "components/Loading";
+import Placeholder from "components/OldPlaceholder";
+import AWSCredentialForm from "./AWSCredentialForm";
+import CredentialList from "./CredentialList";
+import Description from "components/Description";
+import ProvisionerForm from "components/ProvisionerForm";
+import CloudFormationForm from "components/CloudFormationForm";
+import ProvisionerFlow from "components/ProvisionerFlow";
+
+type Props = {
+  selectCredential: (aws_integration_id: number) => void;
+  setTargetARN: (target_arn: string) => void;
+};
+
+type AWSCredential = {
+  created_at: string;
+  id: number;
+  user_id: number;
+  project_id: number;
+  aws_arn: string;
+};
+
+const AWSCredentialsList: React.FunctionComponent<Props> = ({
+  selectCredential,
+  setTargetARN,
+}) => {
+  const { currentProject, setCurrentError } = useContext(Context);
+  const [isLoading, setIsLoading] = useState(true);
+  const [awsCredentials, setAWSCredentials] = useState<AWSCredential[]>(null);
+  const [shouldCreateCred, setShouldCreateCred] = useState(false);
+  const [hasError, setHasError] = useState(false);
+
+  useEffect(() => {
+    api
+      .getAWSIntegration(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        if (!Array.isArray(data)) {
+          throw Error("Data is not an array");
+        }
+
+        setAWSCredentials(data);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setHasError(true);
+        setCurrentError(err.response?.data?.error);
+        setIsLoading(false);
+      });
+  }, [currentProject]);
+
+  if (hasError) {
+    return <Placeholder>Error</Placeholder>;
+  }
+
+  if (isLoading) {
+    return (
+      <Placeholder>
+        <Loading />
+      </Placeholder>
+    );
+  }
+
+  const renderContents = () => {
+    if (shouldCreateCred) {
+      return (
+        <ProvisionerFlow />
+      );
+    }
+
+    return (
+      <>
+        <Description>
+          Select your credentials from the list below, or create a new
+          credential:
+        </Description>
+        <CredentialList
+          credentials={awsCredentials.map((cred) => {
+            return {
+              id: cred.id,
+              display_name: cred.aws_arn,
+              created_at: cred.created_at,
+            };
+          })}
+          selectCredential={selectCredential}
+          setTargetARN={setTargetARN}
+          shouldCreateCred={() => setShouldCreateCred(true)}
+          addNewText="Create new CloudFormation stack"
+        />
+      </>
+    );
+  };
+
+  return <AWSCredentialWrapper>{renderContents()}</AWSCredentialWrapper>;
+};
+
+export default AWSCredentialsList;
+
+const AWSCredentialWrapper = styled.div`
+  margin-top: 20px;
+`;

+ 140 - 0
dashboard/src/main/home/sidebar/AddCluster/AzureCredentialForm.tsx

@@ -0,0 +1,140 @@
+import React, { useContext, useState } from "react";
+import InputRow from "components/form-components/InputRow";
+import SaveButton from "components/SaveButton";
+
+import { Context } from "shared/Context";
+import api from "shared/api";
+import styled from "styled-components";
+import Loading from "components/Loading";
+import Placeholder from "components/OldPlaceholder";
+
+type Props = {
+  setCreatedCredential: (aws_integration_id: number) => void;
+  cancel: () => void;
+};
+
+const AzureCredentialForm: React.FunctionComponent<Props> = ({
+  setCreatedCredential,
+}) => {
+  const { currentProject, setCurrentError } = useContext(Context);
+  const [clientId, setClientId] = useState("");
+  const [servicePrincipalKey, setServicePrincipalKey] = useState("");
+  const [tenantId, setTenantId] = useState("");
+  const [subscriptionId, setSubscriptionId] = useState("");
+  const [buttonStatus, setButtonStatus] = useState("");
+  const [isLoading, setIsLoading] = useState(false);
+  const [hasError, setHasError] = useState(false);
+
+  const submit = () => {
+    setIsLoading(true);
+
+    api
+      .createAzureIntegration(
+        "<token>",
+        {
+          azure_client_id: clientId,
+          azure_subscription_id: subscriptionId,
+          azure_tenant_id: tenantId,
+          service_principal_key: servicePrincipalKey,
+        },
+        {
+          id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        setCreatedCredential(data.id);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setHasError(true);
+        setCurrentError(err.response?.data?.error);
+        setIsLoading(false);
+      });
+  };
+
+  if (hasError) {
+    return <Placeholder>Error</Placeholder>;
+  }
+
+  if (isLoading) {
+    return (
+      <Placeholder>
+        <Loading />
+      </Placeholder>
+    );
+  }
+
+  return (
+    <>
+      <InputRow
+        type="text"
+        value={clientId}
+        setValue={(x: string) => {
+          setClientId(x);
+        }}
+        label="👤 Azure Client ID"
+        placeholder="ex. 12345678-abcd-1234-abcd-12345678abcd"
+        width="100%"
+        isRequired={true}
+      />
+      <InputRow
+        type="password"
+        value={servicePrincipalKey}
+        setValue={(x: string) => {
+          setServicePrincipalKey(x);
+        }}
+        label="🔒 Azure Service Principal Key"
+        placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+        width="100%"
+        isRequired={true}
+      />
+      <InputRow
+        type="text"
+        value={tenantId}
+        setValue={(x: string) => {
+          setTenantId(x);
+        }}
+        label="Azure Tenant ID"
+        placeholder="ex. 12345678-abcd-1234-abcd-12345678abcd"
+        width="100%"
+        isRequired={true}
+      />
+      <InputRow
+        type="text"
+        value={subscriptionId}
+        setValue={(x: string) => {
+          setSubscriptionId(x);
+        }}
+        label="Azure Subscription ID"
+        placeholder="ex. 12345678-abcd-1234-abcd-12345678abcd"
+        width="100%"
+        isRequired={true}
+      />
+      <Flex>
+        <SaveButton
+          text="Continue"
+          disabled={false}
+          onClick={submit}
+          makeFlush={true}
+          clearPosition={true}
+          status={buttonStatus}
+          statusPosition={"right"}
+        />
+      </Flex>
+    </>
+  );
+};
+
+export default AzureCredentialForm;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;

+ 110 - 0
dashboard/src/main/home/sidebar/AddCluster/AzureCredentialList.tsx

@@ -0,0 +1,110 @@
+import React, { useContext, useEffect, useState } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import styled from "styled-components";
+import Loading from "components/Loading";
+import Placeholder from "components/OldPlaceholder";
+import AzureCredentialForm from "./AzureCredentialForm";
+import CredentialList from "./CredentialList";
+import Description from "components/Description";
+
+type Props = {
+  selectCredential: (azure_integration_id: number) => void;
+};
+
+type AzureCredential = {
+  created_at: string;
+  id: number;
+  user_id: number;
+  project_id: number;
+  azure_client_id: string;
+};
+
+const AzureCredentialsList: React.FunctionComponent<Props> = ({
+  selectCredential,
+}) => {
+  const { currentProject, setCurrentError } = useContext(Context);
+  const [isLoading, setIsLoading] = useState(true);
+  const [azCredentials, setAzureCredentials] = useState<AzureCredential[]>(
+    null
+  );
+  const [shouldCreateCred, setShouldCreateCred] = useState(false);
+  const [hasError, setHasError] = useState(false);
+
+  useEffect(() => {
+    api
+      .getAzureIntegration(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        if (!Array.isArray(data)) {
+          throw Error("Data is not an array");
+        }
+
+        setAzureCredentials(data);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setHasError(true);
+        setCurrentError(err.response?.data?.error);
+        setIsLoading(false);
+      });
+  }, [currentProject]);
+
+  if (hasError) {
+    return <Placeholder>Error</Placeholder>;
+  }
+
+  if (isLoading) {
+    return (
+      <Placeholder>
+        <Loading />
+      </Placeholder>
+    );
+  }
+
+  const renderContents = () => {
+    if (shouldCreateCred) {
+      return (
+        <AzureCredentialForm
+          setCreatedCredential={selectCredential}
+          cancel={() => {}}
+        />
+      );
+    }
+
+    return (
+      <>
+        <Description>
+          Select your credentials from the list below, or create a new
+          credential:
+        </Description>
+        <CredentialList
+          credentials={azCredentials.map((cred) => {
+            return {
+              id: cred.id,
+              display_name: cred.azure_client_id,
+              created_at: cred.created_at,
+            };
+          })}
+          selectCredential={selectCredential}
+          shouldCreateCred={() => setShouldCreateCred(true)}
+          addNewText="Add New Azure Credential"
+        />
+      </>
+    );
+  };
+
+  return <AzureCredentialWrapper>{renderContents()}</AzureCredentialWrapper>;
+};
+
+export default AzureCredentialsList;
+
+const AzureCredentialWrapper = styled.div`
+  margin-top: 20px;
+`;

+ 101 - 0
dashboard/src/main/home/sidebar/AddCluster/ClusterList.tsx

@@ -0,0 +1,101 @@
+import React, { useContext, useEffect, useState } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import Loading from "components/Loading";
+import Placeholder from "components/OldPlaceholder";
+import Description from "components/Description";
+import { ClusterType } from "shared/types";
+import SelectRow from "components/form-components/SelectRow";
+import SaveButton from "components/SaveButton";
+
+type Props = {
+  selectCluster: (cluster_id: number) => void;
+};
+
+const ClusterList: React.FunctionComponent<Props> = ({ selectCluster }) => {
+  const { currentProject, setCurrentError } = useContext(Context);
+  const [isLoading, setIsLoading] = useState(true);
+  const [clusters, setClusters] = useState<ClusterType[]>([]);
+  const [selectedClusterID, setSelectedClusterID] = useState<number>();
+  const [hasError, setHasError] = useState(false);
+
+  useEffect(() => {
+    api
+      .getClusters(
+        "<token>",
+        {},
+        {
+          id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        if (!Array.isArray(data)) {
+          throw Error("Data is not an array");
+        }
+
+        setClusters(data);
+        setSelectedClusterID(data[0]?.id);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setHasError(true);
+        setCurrentError(err.response?.data?.error);
+        setIsLoading(false);
+      });
+  }, [currentProject]);
+
+  if (hasError) {
+    return <Placeholder>Error</Placeholder>;
+  }
+
+  if (isLoading || !clusters) {
+    return (
+      <Placeholder>
+        <Loading />
+      </Placeholder>
+    );
+  }
+
+  if (clusters.length == 0) {
+    return (
+      <Placeholder>
+        At least one cluster must exist to create this resource
+      </Placeholder>
+    );
+  }
+
+  return (
+    <>
+      <Description>
+        Select your credentials from the list below, or create a new credential:
+      </Description>
+      <SelectRow
+        options={clusters.map((cluster, i) => {
+          return {
+            label: cluster.name,
+            value: "" + cluster.id,
+          };
+        })}
+        width="100%"
+        scrollBuffer={true}
+        value={"" + selectedClusterID}
+        dropdownMaxHeight="240px"
+        setActiveValue={(x: string) => {
+          setSelectedClusterID(parseInt(x));
+        }}
+        label="Cluster Options"
+      />
+      <SaveButton
+        text="Continue"
+        disabled={false}
+        onClick={() => selectCluster(selectedClusterID)}
+        makeFlush={true}
+        clearPosition={true}
+        statusPosition={"right"}
+      />
+    </>
+  );
+};
+
+export default ClusterList;

+ 119 - 0
dashboard/src/main/home/sidebar/AddCluster/CredentialList.tsx

@@ -0,0 +1,119 @@
+import React from "react";
+import styled from "styled-components";
+import { readableDate } from "shared/string_utils";
+
+type Props = {
+  selectCredential: (id: number) => void;
+  credentials: GenericCredential[];
+  addNewText: string;
+  shouldCreateCred: () => void;
+  isLink?: boolean;
+  linkHref?: string;
+  setTargetARN: (targetARN: string) => void;
+};
+
+type GenericCredential = {
+  id: number;
+  display_name: string;
+  created_at: string;
+};
+
+const CredentialList: React.FunctionComponent<Props> = (props) => {
+  const renderCreateSection = () => {
+    let inner = (
+      <Flex>
+        <i className="material-icons">account_circle</i>
+        {props.addNewText}
+      </Flex>
+    );
+
+    if (props.isLink) {
+      return <CreateNewRowLink href={props.linkHref}>{inner}</CreateNewRowLink>;
+    }
+
+    return (
+      <CreateNewRow onClick={props.shouldCreateCred}>{inner}</CreateNewRow>
+    );
+  };
+
+  return (
+    <>
+      {props.credentials.map((cred) => {
+        return (
+          <PreviewRow
+            key={cred.id}
+            onClick={() => {
+              props.setTargetARN(cred.display_name)
+              props.selectCredential(cred.id)
+            }}
+          >
+            <Flex>
+              <i className="material-icons">account_circle</i>
+              {cred.display_name || "Name N/A"}
+            </Flex>
+            {/* <Right>Connected at {readableDate(cred.created_at)}</Right> */}
+          </PreviewRow>
+        );
+      })}
+      {renderCreateSection()}
+    </>
+  );
+};
+
+export default CredentialList;
+
+const PreviewRow = styled.div`
+  display: flex;
+  align-items: center;
+  padding: 12px 15px;
+  color: #ffffff55;
+  background: #ffffff01;
+  border: 1px solid #aaaabb;
+  justify-content: space-between;
+  font-size: 13px;
+  border-radius: 5px;
+  cursor: pointer;
+  margin: 16px 0;
+
+  :hover {
+    background: #ffffff10;
+  }
+`;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;
+
+const Right = styled.div`
+  text-align: right;
+`;
+
+const CreateNewRow = styled(PreviewRow)`
+  background: none;
+`;
+
+const CreateNewRowLink = styled.a`
+  background: none;
+  display: flex;
+  align-items: center;
+  padding: 12px 15px;
+  color: #ffffff55;
+  background: #ffffff01;
+  border: 1px solid #aaaabb;
+  justify-content: space-between;
+  font-size: 13px;
+  border-radius: 5px;
+  cursor: pointer;
+  margin: 16px 0;
+
+  :hover {
+    background: #ffffff10;
+  }
+`;

+ 103 - 0
dashboard/src/main/home/sidebar/AddCluster/DOCredentialList.tsx

@@ -0,0 +1,103 @@
+import React, { useContext, useEffect, useState } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import styled from "styled-components";
+import Loading from "components/Loading";
+import Placeholder from "components/OldPlaceholder";
+import CredentialList from "./CredentialList";
+import Description from "components/Description";
+
+type Props = {
+  selectCredential: (do_integration_id: number) => void;
+};
+
+type DOCredential = {
+  created_at: string;
+  id: number;
+  user_id: number;
+  project_id: number;
+  target_email: string;
+  target_id: string;
+};
+
+const DOCredentialsList: React.FunctionComponent<Props> = ({
+  selectCredential,
+}) => {
+  const { currentProject, setCurrentError } = useContext(Context);
+  const [isLoading, setIsLoading] = useState(true);
+  const [doCredentials, setDOCredentials] = useState<DOCredential[]>(null);
+  const [hasError, setHasError] = useState(false);
+
+  useEffect(() => {
+    api
+      .getOAuthIds(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        if (!Array.isArray(data)) {
+          throw Error("Data is not an array");
+        }
+
+        setDOCredentials(data);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setHasError(true);
+        setCurrentError(err.response?.data?.error);
+        setIsLoading(false);
+      });
+  }, [currentProject]);
+
+  if (hasError) {
+    return <Placeholder>Error</Placeholder>;
+  }
+
+  if (isLoading) {
+    return (
+      <Placeholder>
+        <Loading />
+      </Placeholder>
+    );
+  }
+
+  const renderContents = () => {
+    return (
+      <>
+        <Description>
+          Select your credentials from the list below, or create a new
+          credential:
+        </Description>
+        <CredentialList
+          credentials={doCredentials.map((cred) => {
+            return {
+              id: cred.id,
+              display_name:
+                cred.target_email && cred.target_id
+                  ? `${cred.target_email} (${cred.target_id})`
+                  : "",
+              created_at: cred.created_at,
+            };
+          })}
+          selectCredential={selectCredential}
+          shouldCreateCred={() => {}}
+          addNewText="Add New DO Credential"
+          isLink={true}
+          linkHref={`/api/projects/${currentProject?.id}/oauth/digitalocean`}
+        />
+      </>
+    );
+  };
+
+  return <DOCredentialWrapper>{renderContents()}</DOCredentialWrapper>;
+};
+
+export default DOCredentialsList;
+
+const DOCredentialWrapper = styled.div`
+  margin-top: 20px;
+`;

+ 114 - 0
dashboard/src/main/home/sidebar/AddCluster/GCPCredentialForm.tsx

@@ -0,0 +1,114 @@
+import React, { useContext, useState } from "react";
+import InputRow from "components/form-components/InputRow";
+import SaveButton from "components/SaveButton";
+
+import { Context } from "shared/Context";
+import api from "shared/api";
+import styled from "styled-components";
+import Loading from "components/Loading";
+import Placeholder from "components/OldPlaceholder";
+import Helper from "components/form-components/Helper";
+import UploadArea from "components/form-components/UploadArea";
+
+type Props = {
+  setCreatedCredential: (aws_integration_id: number) => void;
+  cancel: () => void;
+};
+
+const GCPCredentialForm: React.FunctionComponent<Props> = ({
+  setCreatedCredential,
+}) => {
+  const { currentProject, setCurrentError } = useContext(Context);
+  const [buttonStatus, setButtonStatus] = useState("");
+  const [projectId, setProjectId] = useState("");
+  const [serviceAccountKey, setServiceAccountKey] = useState("");
+  const [isLoading, setIsLoading] = useState(false);
+  const [hasError, setHasError] = useState(false);
+
+  const submit = () => {
+    setIsLoading(true);
+    api
+      .createGCPIntegration(
+        "<token>",
+        {
+          gcp_key_data: serviceAccountKey,
+          gcp_project_id: projectId,
+        },
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        setCreatedCredential(data.id);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setHasError(true);
+        setCurrentError(err.response?.data?.error);
+        setIsLoading(false);
+      });
+  };
+
+  if (hasError) {
+    return <Placeholder>Error</Placeholder>;
+  }
+
+  if (isLoading) {
+    return (
+      <Placeholder>
+        <Loading />
+      </Placeholder>
+    );
+  }
+
+  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}
+      />
+      <Flex>
+        <SaveButton
+          text="Continue"
+          disabled={false}
+          onClick={submit}
+          makeFlush={true}
+          clearPosition={true}
+          status={buttonStatus}
+          statusPosition={"right"}
+        />
+      </Flex>
+    </>
+  );
+};
+
+export default GCPCredentialForm;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;

+ 108 - 0
dashboard/src/main/home/sidebar/AddCluster/GCPCredentialList.tsx

@@ -0,0 +1,108 @@
+import React, { useContext, useEffect, useState } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import styled from "styled-components";
+import Loading from "components/Loading";
+import Placeholder from "components/OldPlaceholder";
+import GCPCredentialForm from "./GCPCredentialForm";
+import CredentialList from "./CredentialList";
+import Description from "components/Description";
+
+type Props = {
+  selectCredential: (gcp_integration_id: number) => void;
+};
+
+type GCPCredential = {
+  created_at: string;
+  id: number;
+  user_id: number;
+  project_id: number;
+  gcp_sa_email: string;
+};
+
+const GCPCredentialsList: React.FunctionComponent<Props> = ({
+  selectCredential,
+}) => {
+  const { currentProject, setCurrentError } = useContext(Context);
+  const [isLoading, setIsLoading] = useState(true);
+  const [gcpCredentials, setGCPCredentials] = useState<GCPCredential[]>(null);
+  const [shouldCreateCred, setShouldCreateCred] = useState(false);
+  const [hasError, setHasError] = useState(false);
+
+  useEffect(() => {
+    api
+      .getGCPIntegration(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        if (!Array.isArray(data)) {
+          throw Error("Data is not an array");
+        }
+
+        setGCPCredentials(data);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setHasError(true);
+        setCurrentError(err.response?.data?.error);
+        setIsLoading(false);
+      });
+  }, [currentProject]);
+
+  if (hasError) {
+    return <Placeholder>Error</Placeholder>;
+  }
+
+  if (isLoading) {
+    return (
+      <Placeholder>
+        <Loading />
+      </Placeholder>
+    );
+  }
+
+  const renderContents = () => {
+    if (shouldCreateCred) {
+      return (
+        <GCPCredentialForm
+          setCreatedCredential={selectCredential}
+          cancel={() => {}}
+        />
+      );
+    }
+
+    return (
+      <>
+        <Description>
+          Select your credentials from the list below, or create a new
+          credential:
+        </Description>
+        <CredentialList
+          credentials={gcpCredentials.map((cred) => {
+            return {
+              id: cred.id,
+              display_name: cred.gcp_sa_email,
+              created_at: cred.created_at,
+            };
+          })}
+          selectCredential={selectCredential}
+          shouldCreateCred={() => setShouldCreateCred(true)}
+          addNewText="Add New GCP Credential"
+        />
+      </>
+    );
+  };
+
+  return <GCPCredentialWrapper>{renderContents()}</GCPCredentialWrapper>;
+};
+
+export default GCPCredentialsList;
+
+const GCPCredentialWrapper = styled.div`
+  margin-top: 20px;
+`;

+ 37 - 9
dashboard/src/main/home/sidebar/ClusterList.tsx

@@ -11,10 +11,14 @@ import Icon from "components/porter/Icon";
 import Spacer from "components/porter/Spacer";
 import { pushFiltered } from "shared/routing";
 import SidebarLink from "./SidebarLink";
+import { OFState } from "main/home/onboarding/state";
+import ProvisionClusterModal from "./ProvisionClusterModal";
+
 
 const ClusterList: React.FC<PropsType> = (props) => {
-  const { setCurrentCluster, user, currentCluster, currentProject } = useContext(Context);
+  const { setCurrentCluster, user, currentCluster, currentProject, setHasFinishedOnboarding } = useContext(Context);
   const [expanded, setExpanded] = useState<boolean>(false);
+  const [clusterModalVisible, setClusterModalVisible] = useState<boolean>(false);
   const wrapperRef = useRef<HTMLDivElement>(null);
   const [clusters, setClusters] = useState<ClusterType[]>([]);
   const [options, setOptions] = useState<any[]>([]);
@@ -55,14 +59,15 @@ const ClusterList: React.FC<PropsType> = (props) => {
           }
         });
     }
-  }, [currentProject]);
-  const truncate = (input: string) => input.length > 21 ? `${input.substring(0, 21)}...` : input;
+  }, [currentProject, currentCluster]);
+  const truncate = (input: string) => input.length > 27 ? `${input.substring(0, 27)}...` : input;
 
   const renderOptionList = () =>
     options.map((option, i: number) => (
       <Option
         key={i}
         selected={option.value === currentCluster?.name}
+        title={option.label}
         onClick={() => {
           setExpanded(false);
           const cluster = clusters.find(c => c.name === option.value);
@@ -73,13 +78,31 @@ const ClusterList: React.FC<PropsType> = (props) => {
         <Icon src={infra} height={"14px"} />
         <ClusterLabel>{option.label}</ClusterLabel>
       </Option>
+
     ));
 
   const renderDropdown = () =>
     expanded && (
-      <Dropdown>
-        {renderOptionList()}
-      </Dropdown>
+      <>
+        <Dropdown>
+          {renderOptionList()}
+
+          {/* Connect Cluster Option */}
+          {
+            currentProject?.enable_reprovision && <Option
+              onClick={() => {
+                setClusterModalVisible(true)
+                setExpanded(false);
+
+              }}>
+
+              <Plus>+</Plus>    Deploy new cluster
+            </Option>
+          }
+
+        </Dropdown>
+
+      </>
     );
 
   if (currentCluster) {
@@ -94,10 +117,14 @@ const ClusterList: React.FC<PropsType> = (props) => {
             <Img src={infra} />
             <ClusterName>{truncate(currentCluster.vanity_name ? currentCluster.vanity_name : currentCluster?.name)}</ClusterName>
 
-            {clusters.length > 1 && <i className="material-icons">arrow_drop_down</i>}
+            {(clusters.length > 1 || user.isPorterUser) && <i className="material-icons">arrow_drop_down</i>}
           </NavButton>
         </MainSelector>
-        {clusters.length > 1 && renderDropdown()}
+        {(clusters.length > 1 || user.isPorterUser) && renderDropdown()}
+        {
+          clusterModalVisible && <ProvisionClusterModal
+            closeModal={() => setClusterModalVisible(false)} />
+        }
       </StyledClusterSection >
     );
   }
@@ -165,6 +192,7 @@ const Option = styled.div<{ selected: boolean }>`
   opacity: 0.6;
   :hover {
     opacity: 1;
+
   }
 
   > i {
@@ -195,7 +223,7 @@ const ClusterName = styled.div`
   text-overflow: ellipsis;
   display: flex;
   align-items: center;
-  max-width: 180px; // You can adjust this value according to your needs
+  max-width: 200px; 
 `;
 
 const MainSelector = styled.div`

+ 63 - 0
dashboard/src/main/home/sidebar/ProvisionClusterModal.tsx

@@ -0,0 +1,63 @@
+import { RouteComponentProps, withRouter } from "react-router";
+import styled, { css } from "styled-components";
+import React, { useContext, useEffect, useState } from "react";
+import Loading from "components/Loading";
+
+import Modal from "components/porter/Modal";
+import Text from "components/porter/Text";
+import AWSCredentialsList from "./AddCluster/AWSCredentialList";
+import { InfraCredentials } from "shared/types";
+import ProvisionerSettings from "components/ProvisionerSettings";
+import Spacer from "components/porter/Spacer";
+import ProvisionerForm from "components/ProvisionerForm";
+
+
+type Props = RouteComponentProps & {
+    closeModal: () => void;
+
+}
+
+const ProvisionClusterModal: React.FC<Props> = ({
+    closeModal,
+
+}) => {
+    const [currentCredential, setCurrentCredential] = useState<InfraCredentials>(
+        null
+    );
+    const [currentStep, setCurrentStep] = useState("cloud");
+    const [targetArn, setTargetARN] = useState("")
+
+    return (
+        <Modal closeModal={closeModal} width={"900px"}>
+            <Text size={16}>
+                Provision A New Cluster
+            </Text>
+            <Spacer y={1} />
+            {currentCredential && targetArn ? (<>
+                <ProvisionerSettings
+                    credentialId={targetArn}
+                    closeModal={closeModal}
+                />
+
+                {/* <ProvisionerForm
+                    goBack={() => setCurrentStep("credentials")}
+                    credentialId={String(currentCredential.aws_integration_id)}
+                    provider={"aws"}
+                /> */}
+            </>) : (
+                < AWSCredentialsList
+                    setTargetARN={setTargetARN}
+                    selectCredential={
+                        (i) =>
+                            setCurrentCredential({
+                                aws_integration_id: i,
+                            })
+                    }
+                />)
+            }
+        </Modal >
+    )
+}
+
+export default ProvisionClusterModal;
+