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

unify provisioner form, update cluster settings, case on ff w/ dummy provisioner status

Justin Rhee 3 лет назад
Родитель
Сommit
2ca2beb496

+ 21 - 0
dashboard/src/assets/settings-centered.svg

@@ -0,0 +1,21 @@
+<svg width="28" height="30" viewBox="0 0 28 30" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4_2)">
+<g filter="url(#filter0_d_4_2)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M21.9024 16.58C22.26 16.77 22.536 17.07 22.7301 17.37C23.1083 17.99 23.0776 18.75 22.7097 19.42L21.9943 20.62C21.6162 21.26 20.9111 21.66 20.1855 21.66C19.8278 21.66 19.4292 21.56 19.1022 21.36C18.8365 21.19 18.5299 21.13 18.2029 21.13C17.1911 21.13 16.3429 21.96 16.3122 22.95C16.3122 24.1 15.372 25 14.1968 25H12.8069C11.6215 25 10.6813 24.1 10.6813 22.95C10.6608 21.96 9.81259 21.13 8.80085 21.13C8.4636 21.13 8.15702 21.19 7.90153 21.36C7.5745 21.56 7.16572 21.66 6.81825 21.66C6.08244 21.66 5.37729 21.26 4.99917 20.62L4.29402 19.42C3.91589 18.77 3.89545 17.99 4.27358 17.37C4.43709 17.07 4.74368 16.77 5.09115 16.58C5.37729 16.44 5.56125 16.21 5.73498 15.94C6.24596 15.08 5.93937 13.95 5.07071 13.44C4.05897 12.87 3.73194 11.6 4.31446 10.61L4.99917 9.43C5.5919 8.44 6.85913 8.09 7.88109 8.67C8.77019 9.15 9.92499 8.83 10.4462 7.98C10.6097 7.7 10.7017 7.4 10.6813 7.1C10.6608 6.71 10.7732 6.34 10.9674 6.04C11.3455 5.42 12.0302 5.02 12.7763 5H14.2172C14.9735 5 15.6582 5.42 16.0363 6.04C16.2203 6.34 16.3429 6.71 16.3122 7.1C16.2918 7.4 16.3838 7.7 16.5473 7.98C17.0685 8.83 18.2233 9.15 19.1226 8.67C20.1344 8.09 21.4118 8.44 21.9943 9.43L22.679 10.61C23.2718 11.6 22.9448 12.87 21.9228 13.44C21.0541 13.95 20.7475 15.08 21.2687 15.94C21.4323 16.21 21.6162 16.44 21.9024 16.58ZM10.6097 15.01C10.6097 16.58 11.9076 17.83 13.5121 17.83C15.1166 17.83 16.3838 16.58 16.3838 15.01C16.3838 13.44 15.1166 12.18 13.5121 12.18C11.9076 12.18 10.6097 13.44 10.6097 15.01Z" fill="white"/>
+</g>
+</g>
+<defs>
+<filter id="filter0_d_4_2" x="0" y="5" width="27" height="28" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="4"/>
+<feGaussianBlur stdDeviation="2"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4_2"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4_2" result="shape"/>
+</filter>
+<clipPath id="clip0_4_2">
+<rect width="28" height="30" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 2 - 123
dashboard/src/components/ProvisionerForm.tsx

@@ -1,45 +1,11 @@
 import React, { useEffect, useState, useContext } from "react";
 import styled from "styled-components";
 
-import api from "shared/api";
 import aws from "assets/aws.png";
 
-import { Context } from "shared/Context";
-
-import SelectRow from "components/form-components/SelectRow";
 import Heading from "components/form-components/Heading";
 import Helper from "./form-components/Helper";
-import InputRow from "./form-components/InputRow";
-import SaveButton from "./SaveButton";
-
-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 machineTypeOptions = [
-  { value: "t3.medium", label: "t3.medium" },
-  { value: "t3.xlarge", label: "t3.xlarge" },
-  { value: "t3.2xlarge", label: "t3.2xlarge" },
-];
+import ProvisionerSettings from "./ProvisionerSettings";
 
 type Props = {
   goBack: () => void;
@@ -50,48 +16,6 @@ const ProvisionerForm: React.FC<Props> = ({
   goBack,
   credentialId,
 }) => {
-  const { currentProject } = useContext(Context);
-  const [createStatus, setCreateStatus] = useState("");
-  const [clusterName, setClusterName] = useState("");
-  const [awsRegion, setAwsRegion] = useState("us-east-1");
-  const [machineType, setMachineType] = useState("t3.medium")
-
-  const createCluster = async () => {
-    try {
-      await api.provisionCluster(
-        "<token>",
-        {
-          project_id: currentProject.id,
-          cloud_provider: "aws",
-          cloud_provider_credentials_id: credentialId,
-          cluster_settings: {
-            cluster_name: clusterName,
-            cluster_version: "v1.24.0",
-            cidr_range: "172.0.0.0/16",
-            region: awsRegion,
-            node_groups: [
-              {
-                instance_type: "t3.medium",
-                min_instances: 1,
-                max_instances: 5,
-                node_group_type: 1
-              },
-              {
-                instance_type: machineType,
-                min_instances: 1,
-                max_instances: 10,
-                node_group_type: 3
-              }
-            ]
-          }
-        },
-        { project_id: currentProject.id }
-      );
-    } catch (err) {
-      console.log(err);
-    }
-  }
-
   return (
     <>
       <Heading isAtTop>
@@ -106,42 +30,7 @@ const ProvisionerForm: React.FC<Props> = ({
       <Helper>
         Configure settings for your new cluster. 
       </Helper>
-      <StyledForm>
-        <Heading isAtTop>EKS configuration</Heading>
-        <InputRow
-          width="350px"
-          isRequired
-          type="string"
-          value={clusterName}
-          setValue={(x: string) => setClusterName(x)}
-          label="🏷️ Cluster name"
-          placeholder="ex: total-perspective-vortex"
-        />
-        <SelectRow
-          options={regionOptions}
-          width="350px"
-          value={awsRegion}
-          scrollBuffer={true}
-          dropdownMaxHeight="240px"
-          setActiveValue={setAwsRegion}
-          label="📍 AWS Region"
-        />
-        <SelectRow
-          options={machineTypeOptions}
-          width="350px"
-          value={machineType}
-          scrollBuffer={true}
-          dropdownMaxHeight="240px"
-          setActiveValue={setMachineType}
-          label="⚙️ Machine type"
-        />
-      </StyledForm>
-      <SaveButton
-        disabled={!clusterName && true}
-        onClick={createCluster}
-        clearPosition
-        text="Provision"
-      />
+      <ProvisionerSettings credentialId={credentialId} />
     </>
   );
 };
@@ -183,14 +72,4 @@ const BackButton = styled.div`
     margin-right: 6px;
     margin-left: -2px;
   }
-`;
-
-const StyledForm = styled.div`
-  position: relative;
-  padding: 30px 30px 25px;
-  border-radius: 5px;
-  background: #26292e;
-  border: 1px solid #494b4f;
-  font-size: 13px;
-  margin-bottom: 30px;
 `;

+ 207 - 0
dashboard/src/components/ProvisionerSettings.tsx

@@ -0,0 +1,207 @@
+import React, { useEffect, useState, useContext } from "react";
+import styled from "styled-components";
+
+import api from "shared/api";
+import aws from "assets/aws.png";
+
+import { Context } from "shared/Context";
+
+import SelectRow from "components/form-components/SelectRow";
+import Heading from "components/form-components/Heading";
+import InputRow from "./form-components/InputRow";
+import SaveButton from "./SaveButton";
+
+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 machineTypeOptions = [
+  { value: "t3.medium", label: "t3.medium" },
+  { value: "t3.xlarge", label: "t3.xlarge" },
+  { value: "t3.2xlarge", label: "t3.2xlarge" },
+];
+
+type Props = {
+  credentialId: any;
+  clusterId?: number;
+};
+
+const ProvisionerForm: React.FC<Props> = ({
+  credentialId,
+  clusterId,
+}) => {
+  const { currentProject } = useContext(Context);
+  const [createStatus, setCreateStatus] = useState("");
+  const [clusterName, setClusterName] = useState("");
+  const [awsRegion, setAwsRegion] = useState("us-east-1");
+  const [machineType, setMachineType] = useState("t3.medium");
+  const [isExpanded, setIsExpanded] = useState(false);
+  const [minInstances, setMinInstances] = useState(1);
+  const [maxInstances, setMaxInstances] = useState(10);
+  const [cidrRange, setCidrRange] = useState("172.0.0.0/16");
+
+  const createCluster = async () => {
+    var data: any = {
+      project_id: currentProject.id,
+      cloud_provider: "aws",
+      cloud_provider_credentials_id: credentialId,
+      cluster_settings: {
+        cluster_name: clusterName,
+        cluster_version: "v1.24.0",
+        cidr_range: cidrRange || "172.0.0.0/16",
+        region: awsRegion,
+        node_groups: [
+          {
+            instance_type: "t3.medium",
+            min_instances: 1,
+            max_instances: 5,
+            node_group_type: 1
+          },
+          {
+            instance_type: machineType,
+            min_instances: minInstances || 1,
+            max_instances: maxInstances || 10,
+            node_group_type: 3
+          }
+        ]
+      }
+    };
+
+    if (clusterId) {
+      data["cluster_id"] = clusterId;
+    }
+
+    try {
+      await api.provisionCluster(
+        "<token>",
+        data,
+        { project_id: currentProject.id }
+      );
+    } catch (err) {
+      console.log(err);
+    }
+  }
+
+  return (
+    <>
+      <StyledForm>
+        <Heading isAtTop>EKS configuration</Heading>
+        <InputRow
+          width="350px"
+          isRequired
+          type="string"
+          value={clusterName}
+          setValue={(x: string) => setClusterName(x)}
+          label="🏷️ Cluster name"
+          placeholder="ex: total-perspective-vortex"
+        />
+        <SelectRow
+          options={regionOptions}
+          width="350px"
+          value={awsRegion}
+          scrollBuffer={true}
+          dropdownMaxHeight="240px"
+          setActiveValue={setAwsRegion}
+          label="📍 AWS Region"
+        />
+        <SelectRow
+          options={machineTypeOptions}
+          width="350px"
+          value={machineType}
+          scrollBuffer={true}
+          dropdownMaxHeight="240px"
+          setActiveValue={setMachineType}
+          label="⚙️ Machine type"
+        />
+
+        <Heading>
+          <ExpandHeader
+            onClick={() => setIsExpanded(!isExpanded)}
+            isExpanded={isExpanded}
+          >
+            <i className="material-icons">arrow_drop_down</i>
+            Advanced settings
+          </ExpandHeader>
+        </Heading>
+        {
+          isExpanded && (
+            <>
+              <InputRow
+                width="350px"
+                type="number"
+                value={minInstances}
+                setValue={(x: number) => setMinInstances(x)}
+                label="Minimum number of application EC2 instances"
+                placeholder="ex: 1"
+              />
+              <InputRow
+                width="350px"
+                type="number"
+                value={maxInstances}
+                setValue={(x: number) => setMaxInstances(x)}
+                label="Minimum number of application EC2 instances"
+                placeholder="ex: 1"
+              />
+              <InputRow
+                width="350px"
+                type="string"
+                value={cidrRange}
+                setValue={(x: string) => setCidrRange(x)}
+                label="VPC CIDR range"
+                placeholder="ex: 172.0.0.0/16"
+              />
+            </>
+          )
+        }
+      </StyledForm>
+      <SaveButton
+        disabled={!clusterName && true}
+        onClick={createCluster}
+        clearPosition
+        text="Provision"
+      />
+    </>
+  );
+};
+
+export default ProvisionerForm;
+
+const ExpandHeader = styled.div<{ isExpanded: boolean }>`
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  > i {
+    margin-right: 7px;
+    margin-left: -7px;
+    transform: ${(props) => props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
+  }
+`;
+
+const StyledForm = styled.div`
+  position: relative;
+  padding: 30px 30px 25px;
+  border-radius: 5px;
+  background: #26292e;
+  border: 1px solid #494b4f;
+  font-size: 13px;
+  margin-bottom: 30px;
+`;

+ 2 - 0
dashboard/src/components/TitleSection.tsx

@@ -78,6 +78,8 @@ const StyledTitleSection = styled.div`
 const Icon = styled.img<{ width: string }>`
   width: ${(props) => props.width || "25px"};
   margin-right: 16px;
+  display: flex;
+  align-items: center;
 `;
 
 const MaterialIcon = styled.span<{ width: string }>`

+ 2 - 2
dashboard/src/components/repo-selector/ActionDetails.tsx

@@ -153,7 +153,7 @@ const ActionDetails: React.FC<PropsType> = (props) => {
               onClick={() => setShowBuildpacksConfig((prev) => !prev)}
               isExpanded={showBuildpacksConfig}
             >
-              Buildpacks Settings
+              Buildpacks settings
               <i className="material-icons">arrow_drop_down</i>
             </ExpandHeader>
           </Heading>
@@ -217,7 +217,7 @@ const ExpandHeader = styled.div<{ isExpanded: boolean }>`
   cursor: pointer;
   > i {
     margin-left: 10px;
-    transform: ${(props) => (props.isExpanded ? "" : "rotate(180deg)")};
+    transform: ${(props) => (props.isExpanded ? "rotate(180deg)" : "")};
   }
 `;
 

+ 115 - 31
dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx

@@ -1,10 +1,12 @@
 import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
+import settings from "assets/settings-centered.svg";
 
+import DashboardHeader from "../DashboardHeader";
 import { Context } from "shared/Context";
 import TabSelector from "components/TabSelector";
-import Heading from "components/form-components/Heading";
-import TitleSection from "components/TitleSection";
+import ProvisionerSettings from "components/ProvisionerSettings";
+import ProvisionerStatus from "./ProvisionerStatus";
 import api from "shared/api";
 
 import NodeList from "./NodeList";
@@ -20,23 +22,13 @@ import CopyToClipboard from "components/CopyToClipboard";
 import Loading from "components/Loading";
 
 import { DetailedIngressError } from "shared/types";
-import SelectRow from "components/form-components/SelectRow";
 
 type TabEnum = "nodes" | "settings" | "namespaces" | "metrics" | "incidents" | "configuration";
 
-const tabOptions: {
+var tabOptions: {
   label: string;
   value: TabEnum;
-}[] = [
-  // { label: "Configuration", value: "configuration" },
-  { label: "Nodes", value: "nodes" },
-  /*
-  { label: "Incidents", value: "incidents" },
-  */
-  { label: "Metrics", value: "metrics" },
-  { label: "Namespaces", value: "namespaces" },
-  { label: "Settings", value: "settings" },
-];
+}[] = [{ label: "Additional settings", value: "settings" }];
 
 export const Dashboard: React.FunctionComponent = () => {
   const [currentTab, setCurrentTab] = useState<TabEnum>("nodes");
@@ -55,30 +47,40 @@ export const Dashboard: React.FunctionComponent = () => {
         return <Metrics />;
       case "namespaces":
         return <NamespaceList />;
-      /*
       case "configuration":
         return (
-          <FormWrapper>
-            <Heading isAtTop>
-              Cluster configuration
-            </Heading>
-            <SelectRow
-              value={"us-east-1"}
-              width="150px"
-              options={[
-                { label: "us-east-1", value: "us-east-1" }
-              ]}
-              setActiveValue={(option) => null}
-              label="AWS region"
+          <>
+            <Br />
+            <ProvisionerSettings
+              clusterId={context.currentCluster.id}
+              credentialId={context.currentCluster.cloud_provider_credential_identifier}
             />
-          </FormWrapper>
+            <Div />
+          </>
         );
-      */
       default:
         return <NodeList />;
     }
   };
 
+  useEffect(() => {
+    if (
+      context.currentCluster.status !== "UPDATING_UNAVAILABLE" &&
+      !tabOptions.find((tab) => tab.value === "nodes")
+    ) {      
+      tabOptions.unshift({ label: "Namespaces", value: "namespaces" });
+      tabOptions.unshift({ label: "Metrics", value: "metrics" });
+      tabOptions.unshift({ label: "Nodes", value: "nodes" }); 
+    }
+    
+    if (
+      context.currentProject.capi_provisioner_enabled &&
+      !tabOptions.find((tab) => tab.value === "configuration")
+    ) {
+      tabOptions.unshift({ value: "configuration", label: "Configuration" });
+    } 
+  }, []);
+
   useEffect(() => {
     setCurrentTabOptions(
       tabOptions.filter((option) => {
@@ -99,7 +101,11 @@ export const Dashboard: React.FunctionComponent = () => {
 
   // Need to reset tab to reset views that don't auto-update on cluster switch (esp namespaces + settings)
   useEffect(() => {
-    setCurrentTab("nodes");
+    if (context.currentProject.capi_provisioner_enabled) {
+      setCurrentTab("configuration");
+    } else {
+      setCurrentTab("nodes");
+    }
   }, [context.currentCluster]);
 
   const renderIngressIp = (
@@ -172,13 +178,21 @@ export const Dashboard: React.FunctionComponent = () => {
 
   return (
     <>
+      <DashboardHeader
+        image={settings}
+        title={context.currentCluster.name}
+        description={`Cluster settings and status for ${context.currentCluster.name}.`}
+        disableLineBreak
+        capitalize={false}
+      />
+
+      {/*
       <TitleSection>
         <DashboardIcon>
           <i className="material-icons">device_hub</i>
         </DashboardIcon>
         {context.currentCluster.name}
       </TitleSection>
-
       <InfoSection>
         <TopRow>
           <InfoLabel>
@@ -187,6 +201,21 @@ export const Dashboard: React.FunctionComponent = () => {
         </TopRow>
         <Description>{renderIngressIp(ingressIp, ingressError)}</Description>
       </InfoSection>
+      */}
+
+      {
+        context.currentProject.capi_provisioner_enabled && (
+          <>
+            <ProvisionerStatus />
+            <RevisionHeader isCurrent={true} showRevisions={false}>
+              <RevisionPreview>
+                Current version - <Revision>No. 4</Revision>
+                <i className="material-icons">arrow_drop_down</i>
+              </RevisionPreview>
+            </RevisionHeader>
+          </>
+        )
+      }
 
       <TabSelector
         options={currentTabOptions}
@@ -198,6 +227,61 @@ export const Dashboard: React.FunctionComponent = () => {
   );
 };
 
+const Div = styled.div`
+  width: 100%;
+  height: 50px;
+`;
+
+const Br = styled.div`
+  width: 100%;
+  height: 35px;
+`;
+
+const RevisionHeader = styled.div`
+  color: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+    props.isCurrent ? "#ffffff66" : "#f5cb42"};
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  height: 40px;
+  font-size: 13px;
+  width: 100%;
+  padding-left: 15px;
+  cursor: pointer;
+  :hover {
+    background: ${props => props.showRevisions && "#ffffff18"};
+    > div > i {
+      background: ${props => props.showRevisions && "#ffffff22"};
+    }
+  }
+  border-radius: 5px;
+  background: #26292e;
+  border: 1px solid #494b4f;
+  margin-top: 25px;
+  margin-bottom: 22px;
+
+  > div > i {
+    margin-left: 12px;
+    font-size: 20px;
+    cursor: pointer;
+    border-radius: 20px;
+    background: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+      props.showRevisions ? "#ffffff18" : ""};
+    transform: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
+      props.showRevisions ? "rotate(180deg)" : ""};
+  }
+`;
+
+const Revision = styled.div`
+  color: #ffffff;
+  margin-left: 5px;
+`;
+
+const RevisionPreview = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
 const DashboardIcon = styled.div`
   height: 35px;
   min-width: 35px;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/Metrics.tsx

@@ -566,7 +566,7 @@ const StyledMetricsSection = styled.div`
   flex-direction: column;
   position: relative;
   font-size: 13px;
-  border-radius: 8px;
+  border-radius: 5px;
   border: 1px solid #ffffff33;
   padding: 18px 22px;
   animation: floatIn 0.3s;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx

@@ -155,7 +155,7 @@ const StyledChart = styled.div`
   :not(:last-child) {
     margin-bottom: 25px;
   }
-  border-radius: 8px;
+  border-radius: 5px;
   background: #26292e;
   border: 1px solid #494b4f;
 `;

+ 63 - 0
dashboard/src/main/home/cluster-dashboard/dashboard/ProvisionerStatus.tsx

@@ -0,0 +1,63 @@
+import React, { useEffect, useState, useContext } from "react";
+import styled from "styled-components";
+
+import api from "shared/api";
+import loading from "assets/loading.gif";
+
+import { Context } from "shared/Context";
+
+type Props = {};
+
+const ProvisionerStatus: React.FC<Props> = ({}) => {
+  const { currentProject, setCurrentCluster } = useContext(Context);
+  const [isExpanded, setIsExpanded] = useState(false);
+
+  return (
+    <StyledProvisionerStatus>
+      <Flex>
+        <Icon src="https://img.stackshare.io/service/7991/amazon-eks.png" />
+        Elastic Kubernetes Service
+        <Status>
+          <Img src={loading} /> Updating
+        </Status>
+      </Flex>
+    </StyledProvisionerStatus>
+  );
+};
+
+export default ProvisionerStatus;
+
+const Icon = styled.img`
+  height: 20px;
+  margin-right: 10px;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+`;
+
+const Img = styled.img`
+  height: 15px;
+  margin-right: 7px;
+`;
+
+const Status = styled.div`
+  color: #aaaabb;
+  display: flex;
+  align-items: center;
+  margin-left: 15px;
+`;
+
+const StyledProvisionerStatus = styled.div`
+  border-radius: 5px;
+  background: #26292e;
+  border: 1px solid #494b4f;
+  height: 40px;
+  margin-bottom: 22px;
+  font-size: 14px;
+  padding-left: 12px;
+  display: flex;
+  align-items: center;
+`;

+ 142 - 142
dashboard/src/main/home/dashboard/ClusterList.tsx

@@ -1,68 +1,82 @@
-import React, { Component } from "react";
+import React, { useEffect, useState, useContext } from "react";
 import styled from "styled-components";
-
-import { Context } from "shared/Context";
-import api from "shared/api";
-import { ClusterType, DetailedClusterType } from "shared/types";
-import Helper from "components/form-components/Helper";
 import { pushFiltered } from "shared/routing";
+import { useHistory, useLocation } from "react-router";
 
-import { RouteComponentProps, withRouter } from "react-router";
-
-import Modal from "../modals/Modal";
-import Heading from "components/form-components/Heading";
-
-type PropsType = RouteComponentProps & {
-  currentCluster: ClusterType;
-};
-
-type StateType = {
-  loading: boolean;
-  error: string;
-  clusters: DetailedClusterType[];
-  showErrorModal?: {
-    clusterId: number;
-    show: boolean;
-  };
-};
-
-class Templates extends Component<PropsType, StateType> {
-  state: StateType = {
-    loading: true,
-    error: "",
-    clusters: [],
-    showErrorModal: undefined,
-  };
+import api from "shared/api";
+import loading from "assets/loading.gif";
+import Loading from "components/Loading";
 
-  componentDidMount() {
-    this.updateClusterList();
-  }
+import { Context } from "shared/Context";
 
-  componentDidUpdate(prevProps: PropsType) {
-    if (prevProps.currentCluster?.name != this.props.currentCluster?.name) {
-      this.updateClusterList();
-    }
-  }
+type Props = {};
 
-  updateClusterList = async () => {
-    try {
-      const res = await api.getClusters(
-        "<token>",
-        {},
-        { id: this.context.currentProject.id }
-      );
+const ClusterList: React.FC<Props> = ({}) => {
+  const { currentProject, setCurrentCluster } = useContext(Context);
+  const [isLoading, setIsLoading] = useState(true);
+  const [clusters, setClusters] = useState(null);
+  const location = useLocation();
+  const history = useHistory();
 
-      if (res.data) {
-        this.setState({ clusters: res.data, loading: false, error: "" });
-      } else {
-        this.setState({ loading: false, error: "Response data missing" });
-      }
-    } catch (err) {
-      this.setState(err);
-    }
-  };
+  useEffect(() => {
+    api.getClusters(
+      "<token>",
+      {},
+      { id: currentProject.id },
+    )
+      .then(({ data }) => {
+        setClusters(data);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setIsLoading(false);
+      });
+   /*
+    const dummyData = [
+      {
+        id: 3,
+        project_id: 2,
+        name: "dummy-cluster-one",
+        server: "https://73727E5A0EF0FD07D24D7C1FDCE041E6.gr7.us-east-1.eks.amazonaws.com",
+        service: "eks",
+        agent_integration_enabled: false,
+        infra_id: 0,
+        aws_integration_id: 5,
+        preview_envs_enabled: true,
+        status: "READY",
+      },
+      {
+        id: 4,
+        project_id: 2,
+        name: "dummy-cluster-two",
+        server: "https://73727E5A0EF0FD07D24D7C1FDCE041E6.gr7.us-east-1.eks.amazonaws.com",
+        service: "eks",
+        agent_integration_enabled: false,
+        infra_id: 0,
+        aws_integration_id: 5,
+        preview_envs_enabled: true,
+        status: "UPDATING",
+      },
+      {
+        id: 5,
+        project_id: 2,
+        name: "dummy-cluster-three",
+        server: "https://73727E5A0EF0FD07D24D7C1FDCE041E6.gr7.us-east-1.eks.amazonaws.com",
+        service: "eks",
+        agent_integration_enabled: false,
+        infra_id: 0,
+        aws_integration_id: 5,
+        preview_envs_enabled: true,
+        status: "UPDATING_UNAVAILABLE",
+      },
+    ];
+    setClusters(dummyData);
+    setIsLoading(false);
+    */
+  }, [currentProject]);
 
-  renderIcon = () => {
+  const renderIcon = () => {
     return (
       <DashboardIcon>
         <svg
@@ -123,84 +137,73 @@ class Templates extends Component<PropsType, StateType> {
     );
   };
 
-  renderClusters = () => {
-    return this.state.clusters.map(
-      (cluster: DetailedClusterType, i: number) => {
-        return (
-          <TemplateBlock
-            onClick={() => {
-              this.context.setCurrentCluster(cluster);
-              pushFiltered(this.props, "/applications", ["project_id"], {
-                cluster: cluster.name,
-              });
-            }}
-            key={i}
-          >
-            {this.renderIcon()}
-            <TemplateTitle>{cluster.name}</TemplateTitle>
-          </TemplateBlock>
-        );
+  return (
+    <>
+      {
+        isLoading ? (
+          <LoadingWrapper><Loading /></LoadingWrapper>
+        ) : (
+          <StyledClusterList>
+            {clusters.map((cluster: any) => {
+              return (
+                <ClusterRow
+                  onClick={() => {
+                    setCurrentCluster(cluster);
+                    pushFiltered({ location, history }, "/applications", ["project_id"], {
+                      cluster: cluster.name,
+                    });
+                  }}
+                >
+                  {renderIcon()}
+                  {cluster.name}
+                  {
+                    cluster.status === "UPDATING" && (
+                      <Status
+                        onClick={(e) => {
+                          e.stopPropagation();
+                          setCurrentCluster(cluster);
+                          pushFiltered({ location, history }, "/cluster-dashboard", ["project_id"], {
+                            cluster: cluster.name,
+                          });
+                        }}
+                      >
+                        <Img src={loading} /> Updating
+                      </Status>
+                    )
+                  }
+                </ClusterRow>
+              )
+            })}
+          </StyledClusterList>
+        )
       }
-    );
-  };
-
-  renderErrorModal = () => {
-    const clusterError =
-      this.state.showErrorModal?.show &&
-      this.state.clusters.find(
-        (c) => c.id === this.state.showErrorModal?.clusterId
-      );
-    const ingressError = clusterError?.ingress_error;
-    return (
-      <>
-        {clusterError && (
-          <Modal
-            onRequestClose={() => this.setState({ showErrorModal: undefined })}
-            width="665px"
-            height="min-content"
-          >
-            Porter encountered an error. Full error log:
-            <CodeBlock>{ingressError.error}</CodeBlock>
-          </Modal>
-        )}
-      </>
-    );
-  };
-
-  render() {
-    return (
-      <StyledClusterList>
-        {/* <Heading isAtTop>Connected clusters</Heading> */}
-        <TemplateList>{this.renderClusters()}</TemplateList>
-        {this.renderErrorModal()}
-      </StyledClusterList>
-    );
-  }
-}
-
-Templates.contextType = Context;
+    </>
+  );
+};
 
-export default withRouter(Templates);
+export default ClusterList;
 
-const CodeBlock = styled.span`
-  display: block;
-  background-color: #1b1d26;
-  color: white;
-  border-radius: 5px;
-  font-family: monospace;
-  user-select: text;
-  max-height: 400px;
-  width: 90%;
-  margin-left: 5%;
-  margin-top: 20px;
-  overflow-y: auto;
-  padding: 10px;
-  overflow-wrap: break-word;
+const Img = styled.img`
+  height: 15px;
+  margin-right: 7px;
 `;
 
-const StyledClusterList = styled.div`
-  padding-left: 2px;
-  overflow: visible;
+const Status = styled.div`
+  margin-left: 15px;
+  border-radius: 50px;
+  padding: 5px 10px;
+  background: #ffffff11;
+  color: #aaaabb;
+  display: flex;
+  align-items: center;
+
+  :hover {
+    background: #ffffff22;
+    border: 1px solid #7a7b80;
+    margin-top: -1px;
+    margin-bottom: -1px;
+    margin-left: 14px;
+  }
 `;
 
 const DashboardIcon = styled.div`
@@ -220,15 +223,7 @@ const DashboardIcon = styled.div`
   }
 `;
 
-const TemplateTitle = styled.div`
-  text-align: center;
-  white-space: nowrap;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-`;
-
-const TemplateBlock = styled.div`
+const ClusterRow = styled.div`
   align-items: center;
   user-select: none;
   display: flex;
@@ -258,7 +253,12 @@ const TemplateBlock = styled.div`
   }
 `;
 
-const TemplateList = styled.div`
-  overflow-y: auto;
-  overflow: visible;
+const StyledClusterList = styled.div`
 `;
+
+const LoadingWrapper = styled.div`
+  height: calc(100vh - 450px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;

+ 1 - 1
dashboard/src/main/home/dashboard/ClusterPlaceholder.tsx

@@ -4,7 +4,7 @@ import styled from "styled-components";
 import { Context } from "shared/Context";
 import { ClusterType } from "shared/types";
 
-import ClusterList from "./ClusterList";
+import ClusterList from "./OldClusterList";
 import Loading from "components/Loading";
 import NoClusterPlaceholder from "../NoClusterPlaceholder";
 

+ 2 - 2
dashboard/src/main/home/dashboard/ClusterSection.tsx

@@ -6,7 +6,7 @@ import { Context } from "shared/Context";
 import Banner from "components/Banner";
 
 import ProvisionerFlow from "components/ProvisionerFlow";
-import ClusterPlaceholderContainer from "./ClusterPlaceholderContainer";
+import ClusterList from "./ClusterList";
 import TitleSection from "components/TitleSection";
 
 type Props = {
@@ -95,7 +95,7 @@ const ClusterSection = (props: Props) => {
       <Button onClick={() => setCurrentStep("cloud")}>
         <i className="material-icons">add</i> Create a cluster
       </Button>
-      <ClusterPlaceholderContainer />
+      <ClusterList />
     </>
   );
 };

+ 264 - 0
dashboard/src/main/home/dashboard/OldClusterList.tsx

@@ -0,0 +1,264 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { ClusterType, DetailedClusterType } from "shared/types";
+import Helper from "components/form-components/Helper";
+import { pushFiltered } from "shared/routing";
+
+import { RouteComponentProps, withRouter } from "react-router";
+
+import Modal from "../modals/Modal";
+import Heading from "components/form-components/Heading";
+
+type PropsType = RouteComponentProps & {
+  currentCluster: ClusterType;
+};
+
+type StateType = {
+  loading: boolean;
+  error: string;
+  clusters: DetailedClusterType[];
+  showErrorModal?: {
+    clusterId: number;
+    show: boolean;
+  };
+};
+
+class Templates extends Component<PropsType, StateType> {
+  state: StateType = {
+    loading: true,
+    error: "",
+    clusters: [],
+    showErrorModal: undefined,
+  };
+
+  componentDidMount() {
+    this.updateClusterList();
+  }
+
+  componentDidUpdate(prevProps: PropsType) {
+    if (prevProps.currentCluster?.name != this.props.currentCluster?.name) {
+      this.updateClusterList();
+    }
+  }
+
+  updateClusterList = async () => {
+    try {
+      const res = await api.getClusters(
+        "<token>",
+        {},
+        { id: this.context.currentProject.id }
+      );
+
+      if (res.data) {
+        this.setState({ clusters: res.data, loading: false, error: "" });
+      } else {
+        this.setState({ loading: false, error: "Response data missing" });
+      }
+    } catch (err) {
+      this.setState(err);
+    }
+  };
+
+  renderIcon = () => {
+    return (
+      <DashboardIcon>
+        <svg
+          width="16"
+          height="16"
+          viewBox="0 0 19 19"
+          fill="none"
+          xmlns="http://www.w3.org/2000/svg"
+        >
+          <path
+            d="M15.207 12.4403C16.8094 12.4403 18.1092 11.1414 18.1092 9.53907C18.1092 7.93673 16.8094 6.63782 15.207 6.63782"
+            stroke="white"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            stroke-linejoin="round"
+          />
+          <path
+            d="M3.90217 12.4403C2.29983 12.4403 1 11.1414 1 9.53907C1 7.93673 2.29983 6.63782 3.90217 6.63782"
+            stroke="white"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            stroke-linejoin="round"
+          />
+          <path
+            fillRule="evenodd"
+            clipRule="evenodd"
+            d="M9.54993 13.4133C7.4086 13.4133 5.69168 11.6964 5.69168 9.55417C5.69168 7.41284 7.4086 5.69592 9.54993 5.69592C11.6913 5.69592 13.4082 7.41284 13.4082 9.55417C13.4082 11.6964 11.6913 13.4133 9.54993 13.4133Z"
+            stroke="white"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            stroke-linejoin="round"
+          />
+          <path
+            d="M6.66895 15.207C6.66895 16.8094 7.96787 18.1092 9.5702 18.1092C11.1725 18.1092 12.4715 16.8094 12.4715 15.207"
+            stroke="white"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            stroke-linejoin="round"
+          />
+          <path
+            d="M6.66895 3.90217C6.66895 2.29983 7.96787 1 9.5702 1C11.1725 1 12.4715 2.29983 12.4715 3.90217"
+            stroke="white"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            stroke-linejoin="round"
+          />
+          <path
+            fillRule="evenodd"
+            clipRule="evenodd"
+            d="M5.69591 9.54996C5.69591 7.40863 7.41283 5.69171 9.55508 5.69171C11.6964 5.69171 13.4133 7.40863 13.4133 9.54996C13.4133 11.6913 11.6964 13.4082 9.55508 13.4082C7.41283 13.4082 5.69591 11.6913 5.69591 9.54996Z"
+            stroke="white"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            stroke-linejoin="round"
+          />
+        </svg>
+      </DashboardIcon>
+    );
+  };
+
+  renderClusters = () => {
+    return this.state.clusters.map(
+      (cluster: DetailedClusterType, i: number) => {
+        return (
+          <TemplateBlock
+            onClick={() => {
+              this.context.setCurrentCluster(cluster);
+              pushFiltered(this.props, "/applications", ["project_id"], {
+                cluster: cluster.name,
+              });
+            }}
+            key={i}
+          >
+            {this.renderIcon()}
+            <TemplateTitle>{cluster.name}</TemplateTitle>
+          </TemplateBlock>
+        );
+      }
+    );
+  };
+
+  renderErrorModal = () => {
+    const clusterError =
+      this.state.showErrorModal?.show &&
+      this.state.clusters.find(
+        (c) => c.id === this.state.showErrorModal?.clusterId
+      );
+    const ingressError = clusterError?.ingress_error;
+    return (
+      <>
+        {clusterError && (
+          <Modal
+            onRequestClose={() => this.setState({ showErrorModal: undefined })}
+            width="665px"
+            height="min-content"
+          >
+            Porter encountered an error. Full error log:
+            <CodeBlock>{ingressError.error}</CodeBlock>
+          </Modal>
+        )}
+      </>
+    );
+  };
+
+  render() {
+    return (
+      <StyledClusterList>
+        {/* <Heading isAtTop>Connected clusters</Heading> */}
+        <TemplateList>{this.renderClusters()}</TemplateList>
+        {this.renderErrorModal()}
+      </StyledClusterList>
+    );
+  }
+}
+
+Templates.contextType = Context;
+
+export default withRouter(Templates);
+
+const CodeBlock = styled.span`
+  display: block;
+  background-color: #1b1d26;
+  color: white;
+  border-radius: 5px;
+  font-family: monospace;
+  user-select: text;
+  max-height: 400px;
+  width: 90%;
+  margin-left: 5%;
+  margin-top: 20px;
+  overflow-y: auto;
+  padding: 10px;
+  overflow-wrap: break-word;
+`;
+
+const StyledClusterList = styled.div`
+  padding-left: 2px;
+  overflow: visible;
+`;
+
+const DashboardIcon = styled.div`
+  position: relative;
+  height: 25px;
+  min-width: 25px;
+  width: 25px;
+  border-radius: 200px;
+  margin-right: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #676c7c;
+  border: 1px solid #8e94aa;
+  > i {
+    font-size: 22px;
+  }
+`;
+
+const TemplateTitle = styled.div`
+  text-align: center;
+  white-space: nowrap;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+const TemplateBlock = styled.div`
+  align-items: center;
+  user-select: none;
+  display: flex;
+  font-size: 13px;
+  font-weight: 500;
+  padding: 15px;
+  margin-bottom: 20px;
+  align-item: center;
+  cursor: pointer;
+  color: #ffffff;
+  position: relative;
+  border-radius: 5px;
+  background: #26292e;
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const TemplateList = styled.div`
+  overflow-y: auto;
+  overflow: visible;
+`;

+ 6 - 0
dashboard/src/main/home/onboarding/Onboarding.tsx

@@ -165,6 +165,7 @@ const Onboarding = () => {
           />
           <Br />
           <ProvisionerFlow />
+          <Div />
         </Wrapper>
       )
     } else {
@@ -183,6 +184,11 @@ const Onboarding = () => {
 
 export default Onboarding;
 
+const Div = styled.div`
+  width: 100%;
+  height: 100px;
+`;
+
 const Br = styled.div`
   width: 100%;
   height: 1px;

+ 1 - 1
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -134,7 +134,7 @@ export const ClusterSection: React.FC<Props> = ({
               window.location.pathname.startsWith("/cluster-dashboard")
             }
           >
-            <Icon className="material-icons">device_hub</Icon>
+            <Img enlarge={true} src={settings} />
             Cluster settings
           </NavButton>
         </Relative>

+ 1 - 0
dashboard/src/shared/api.tsx

@@ -857,6 +857,7 @@ const getInfraTemplate = baseApi<
 const provisionCluster = baseApi<
   {
     project_id: number,
+    cluster_id?: number,
     cloud_provider: string,
     cloud_provider_credentials_id: string,
     cluster_settings: {

+ 2 - 0
dashboard/src/shared/types.tsx

@@ -11,6 +11,8 @@ export interface ClusterType {
   aws_integration_id?: number;
   aws_cluster_id?: string;
   preview_envs_enabled?: boolean;
+  cloud_provider_credential_identifier?: string;
+  status?: string;
 }
 
 export interface DetailedClusterType extends ClusterType {