Переглянути джерело

[Por 1025] Infra-Settings Overhaul (#2848)

* POR-1025 Infra-Settings FE Overhaul

* Tooltip

* Fix Configure Custom Domain

---------

Co-authored-by: Soham Dessai <sohamdessai@sohams-mbp.mynetworksettings.com>
sdess09 3 роки тому
батько
коміт
91bdff9195

+ 1 - 0
dashboard/src/assets/edit-button.svg

@@ -0,0 +1 @@
+<svg fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="48px" height="48px"><path d="M 36 5.0097656 C 34.205301 5.0097656 32.410791 5.6901377 31.050781 7.0507812 L 8.9160156 29.183594 C 8.4960384 29.603571 8.1884588 30.12585 8.0253906 30.699219 L 5.0585938 41.087891 A 1.50015 1.50015 0 0 0 6.9121094 42.941406 L 17.302734 39.974609 A 1.50015 1.50015 0 0 0 17.304688 39.972656 C 17.874212 39.808939 18.39521 39.50518 18.816406 39.083984 L 40.949219 16.949219 C 43.670344 14.228094 43.670344 9.7719064 40.949219 7.0507812 C 39.589209 5.6901377 37.794699 5.0097656 36 5.0097656 z M 36 7.9921875 C 37.020801 7.9921875 38.040182 8.3855186 38.826172 9.171875 A 1.50015 1.50015 0 0 0 38.828125 9.171875 C 40.403 10.74675 40.403 13.25325 38.828125 14.828125 L 36.888672 16.767578 L 31.232422 11.111328 L 33.171875 9.171875 C 33.957865 8.3855186 34.979199 7.9921875 36 7.9921875 z M 29.111328 13.232422 L 34.767578 18.888672 L 16.693359 36.962891 C 16.634729 37.021121 16.560472 37.065723 16.476562 37.089844 L 8.6835938 39.316406 L 10.910156 31.521484 A 1.50015 1.50015 0 0 0 10.910156 31.519531 C 10.933086 31.438901 10.975086 31.366709 11.037109 31.304688 L 29.111328 13.232422 z"/></svg>

+ 54 - 0
dashboard/src/components/NoClusterPlaceHolder.tsx

@@ -0,0 +1,54 @@
+import React, { useEffect, useState, useContext } from "react";
+import styled from "styled-components";
+import { RouteComponentProps, withRouter } from "react-router";
+import { pushFiltered } from "shared/routing";
+
+import loading from "assets/loading.gif";
+
+import { Context } from "shared/Context";
+import Heading from "components/form-components/Heading";
+import Helper from "components/form-components/Helper";
+import Text from "./porter/Text";
+import Spacer from "./porter/Spacer";
+
+type Props = {};
+
+const NoClusterPlaceholder: React.FC<RouteComponentProps> = (props) => {
+  const { currentCluster } = useContext(Context);
+
+  return (
+    <ClusterPlaceholder>
+      <Text size={16}>No Cluster Provisioned</Text>
+      <Spacer height="15px" />
+      <Text color="helper">Finish provisioning a cluster to continue</Text>
+    </ClusterPlaceholder>
+  );
+};
+
+export default withRouter(NoClusterPlaceholder);
+
+const Link = styled.a`
+  text-decoration: underline;
+  position: relative;
+  cursor: pointer;
+  > i {
+    color: #aaaabb;
+    font-size: 15px;
+    position: absolute;
+    right: -17px;
+    top: 1px;
+  }
+`;
+
+const Img = styled.img`
+  height: 15px;
+  margin-right: 15px;
+`;
+
+const ClusterPlaceholder = styled.div`
+  padding: 25px;
+  border-radius: 5px;
+  background: ${(props) => props.theme.fg};
+  border: 1px solid #494b4f;
+  padding-bottom: 35px;
+`;

+ 308 - 179
dashboard/src/components/ProvisionerSettings.tsx

@@ -11,7 +11,15 @@ import SelectRow from "components/form-components/SelectRow";
 import Heading from "components/form-components/Heading";
 import Helper from "components/form-components/Helper";
 import InputRow from "./form-components/InputRow";
-import { Contract, EnumKubernetesKind, EnumCloudProvider, NodeGroupType, EKSNodeGroup, EKS, Cluster } from "@porter-dev/api-contracts";
+import {
+  Contract,
+  EnumKubernetesKind,
+  EnumCloudProvider,
+  NodeGroupType,
+  EKSNodeGroup,
+  EKS,
+  Cluster,
+} from "@porter-dev/api-contracts";
 import { ClusterType } from "shared/types";
 import Button from "./porter/Button";
 import Error from "./porter/Error";
@@ -57,11 +65,12 @@ const clusterVersionOptions = [
 
 type Props = RouteComponentProps & {
   selectedClusterVersion?: Contract;
+  provisionerError?: string;
   credentialId: string;
   clusterId?: number;
 };
 
-const ProvisionerSettings: React.FC<Props> = props => {
+const ProvisionerSettings: React.FC<Props> = (props) => {
   const {
     user,
     currentProject,
@@ -92,20 +101,26 @@ const ProvisionerSettings: React.FC<Props> = props => {
     } catch (err) {
       console.log(err);
     }
-  }
+  };
 
   const getStatus = () => {
     if (isReadOnly) {
-      return "Provisioning is still in progress..."
+      return "Provisioning is still in progress...";
     } else if (errorMessage) {
-      return <Error
-        message={errorMessage}
-        ctaText={errorMessage !== DEFAULT_ERROR_MESSAGE ? "Troubleshooting steps" : null}
-        errorModalContents={errorMessageToModal(errorMessage)}
-      />;
+      return (
+        <Error
+          message={errorMessage}
+          ctaText={
+            errorMessage !== DEFAULT_ERROR_MESSAGE
+              ? "Troubleshooting steps"
+              : null
+          }
+          errorModalContents={errorMessageToModal(errorMessage)}
+        />
+      );
     }
     return undefined;
-  }
+  };
 
   const createCluster = async () => {
     markProvisioningStarted();
@@ -144,11 +159,11 @@ const ProvisionerSettings: React.FC<Props> = props => {
                 maxInstances: maxInstances || 10,
                 nodeGroupType: NodeGroupType.APPLICATION,
                 isStateful: false,
-              })
-            ]
-          })
+              }),
+            ],
+          }),
         },
-      })
+      }),
     });
 
     if (props.clusterId) {
@@ -156,34 +171,28 @@ const ProvisionerSettings: React.FC<Props> = props => {
     }
 
     try {
-      setIsReadOnly(true)
-      setErrorMessage(undefined)
-      await api
-        .preflightCheckAWSUsage(
-          "<token>",
-          {
-            target_arn: props.credentialId,
-            region: awsRegion
-          },
-          {
-            id: currentProject.id,
-          }
-        );
-
-      const res = await api.createContract(
+      setIsReadOnly(true);
+      setErrorMessage(undefined);
+      await api.preflightCheckAWSUsage(
         "<token>",
-        data,
-        { project_id: currentProject.id }
+        {
+          target_arn: props.credentialId,
+          region: awsRegion,
+        },
+        {
+          id: currentProject.id,
+        }
       );
 
+      const res = await api.createContract("<token>", data, {
+        project_id: currentProject.id,
+      });
+
       // Only refresh and set clusters on initial create
       if (!props.clusterId) {
         setShouldRefreshClusters(true);
-        api.getClusters(
-          "<token>",
-          {},
-          { id: currentProject.id },
-        )
+        api
+          .getClusters("<token>", {}, { id: currentProject.id })
           .then(({ data }) => {
             data.forEach((cluster: ClusterType) => {
               if (cluster.id === res.data.contract_revision?.cluster_id) {
@@ -202,32 +211,31 @@ const ProvisionerSettings: React.FC<Props> = props => {
       }
       setErrorMessage(undefined);
     } catch (err) {
-      const errMessage = err.response.data.error.replace('unknown: ', '');
+      const errMessage = err.response.data.error.replace("unknown: ", "");
       // hacky, need to standardize error contract with backend
-      if (errMessage.includes('elastic IP')) {
-        setErrorMessage(AWS_EIP_QUOTA_ERROR_MESSAGE)
-      } else if (errMessage.includes('VPC')) {
-        setErrorMessage(AWS_VPC_QUOTA_ERROR_MESSAGE)
-      } else if (errMessage.includes('NAT Gateway')) {
-        setErrorMessage(AWS_NAT_GATEWAY_QUOTA_ERROR_MESSAGE)
-      } else if (errMessage.includes('vCPU')) {
-        setErrorMessage(AWS_VCPU_QUOTA_ERROR_MESSAGE)
-      } else if (errMessage.includes('AWS account')) {
-        setErrorMessage(AWS_LOGIN_ERROR_MESSAGE)
+      if (errMessage.includes("elastic IP")) {
+        setErrorMessage(AWS_EIP_QUOTA_ERROR_MESSAGE);
+      } else if (errMessage.includes("VPC")) {
+        setErrorMessage(AWS_VPC_QUOTA_ERROR_MESSAGE);
+      } else if (errMessage.includes("NAT Gateway")) {
+        setErrorMessage(AWS_NAT_GATEWAY_QUOTA_ERROR_MESSAGE);
+      } else if (errMessage.includes("vCPU")) {
+        setErrorMessage(AWS_VCPU_QUOTA_ERROR_MESSAGE);
+      } else if (errMessage.includes("AWS account")) {
+        setErrorMessage(AWS_LOGIN_ERROR_MESSAGE);
       } else {
-        setErrorMessage(DEFAULT_ERROR_MESSAGE)
+        setErrorMessage(DEFAULT_ERROR_MESSAGE);
       }
     } finally {
-      setIsReadOnly(false)
+      setIsReadOnly(false);
     }
-  }
+  };
 
   useEffect(() => {
     setIsReadOnly(
-      props.clusterId && (
-        currentCluster.status === "UPDATING" ||
-        currentCluster.status === "UPDATING_UNAVAILABLE"
-      )
+      props.clusterId &&
+        (currentCluster.status === "UPDATING" ||
+          currentCluster.status === "UPDATING_UNAVAILABLE")
     );
     setClusterName(`${currentProject.name}-cluster`);
   }, []);
@@ -251,7 +259,6 @@ const ProvisionerSettings: React.FC<Props> = props => {
   }, [props.selectedClusterVersion]);
 
   const renderForm = () => {
-
     // Render simplified form if initial create
     if (!props.clusterId) {
       return (
@@ -259,7 +266,8 @@ const ProvisionerSettings: React.FC<Props> = props => {
           <Text size={16}>Select an AWS region</Text>
           <Spacer y={1} />
           <Text color="helper">
-            Porter will automatically provision your infrastructure in the specified region.
+            Porter will automatically provision your infrastructure in the
+            specified region.
           </Text>
           <Spacer height="10px" />
           <SelectRow
@@ -273,7 +281,7 @@ const ProvisionerSettings: React.FC<Props> = props => {
             label="📍 AWS region"
           />
         </>
-      )
+      );
     }
 
     // If settings, update full form
@@ -290,77 +298,75 @@ const ProvisionerSettings: React.FC<Props> = props => {
           setActiveValue={setAwsRegion}
           label="📍 AWS region"
         />
-        {
-          user?.isPorterUser && (
-            <Heading>
-              <ExpandHeader
-                onClick={() => setIsExpanded(!isExpanded)}
-                isExpanded={isExpanded}
-              >
-                <i className="material-icons">arrow_drop_down</i>
-                Advanced settings
-              </ExpandHeader>
-            </Heading>
-          )
-        }
-        {
-          isExpanded && (
-            <>
-              <SelectRow
-                options={clusterVersionOptions}
-                width="350px"
-                disabled={isReadOnly}
-                value={clusterVersion}
-                scrollBuffer={true}
-                dropdownMaxHeight="240px"
-                setActiveValue={setClusterVersion}
-                label="Cluster version"
-              />
-              <SelectRow
-                options={machineTypeOptions}
-                width="350px"
-                disabled={isReadOnly}
-                value={machineType}
-                scrollBuffer={true}
-                dropdownMaxHeight="240px"
-                setActiveValue={setMachineType}
-                label="Machine type"
-              />
-              <InputRow
-                width="350px"
-                type="number"
-                disabled={isReadOnly}
-                value={maxInstances}
-                setValue={(x: number) => setMaxInstances(x)}
-                label="Maximum number of application EC2 instances"
-                placeholder="ex: 1"
-              />
-              <InputRow
-                width="350px"
-                type="string"
-                disabled={isReadOnly}
-                value={cidrRange}
-                setValue={(x: string) => setCidrRange(x)}
-                label="VPC CIDR range"
-                placeholder="ex: 10.78.0.0/16"
-              />
-            </>
-          )
-        }
+        {user?.isPorterUser && (
+          <Heading>
+            <ExpandHeader
+              onClick={() => setIsExpanded(!isExpanded)}
+              isExpanded={isExpanded}
+            >
+              <i className="material-icons">arrow_drop_down</i>
+              Advanced settings
+            </ExpandHeader>
+          </Heading>
+        )}
+        {isExpanded && (
+          <>
+            <SelectRow
+              options={clusterVersionOptions}
+              width="350px"
+              disabled={isReadOnly}
+              value={clusterVersion}
+              scrollBuffer={true}
+              dropdownMaxHeight="240px"
+              setActiveValue={setClusterVersion}
+              label="Cluster version"
+            />
+            <SelectRow
+              options={machineTypeOptions}
+              width="350px"
+              disabled={isReadOnly}
+              value={machineType}
+              scrollBuffer={true}
+              dropdownMaxHeight="240px"
+              setActiveValue={setMachineType}
+              label="Machine type"
+            />
+            <InputRow
+              width="350px"
+              type="number"
+              disabled={isReadOnly}
+              value={maxInstances}
+              setValue={(x: number) => setMaxInstances(x)}
+              label="Maximum number of application EC2 instances"
+              placeholder="ex: 1"
+            />
+            <InputRow
+              width="350px"
+              type="string"
+              disabled={isReadOnly}
+              value={cidrRange}
+              setValue={(x: string) => setCidrRange(x)}
+              label="VPC CIDR range"
+              placeholder="ex: 10.78.0.0/16"
+            />
+          </>
+        )}
       </>
-    )
-  }
+    );
+  };
 
   return (
     <>
-      <StyledForm>
-        {renderForm()}
-      </StyledForm>
+      <StyledForm>{renderForm()}</StyledForm>
       <Button
-        disabled={(!clusterName && true) || isReadOnly}
+        disabled={
+          (!clusterName && true) || isReadOnly || props.provisionerError == ""
+        }
         onClick={createCluster}
         status={getStatus()}
-      >Provision</Button>
+      >
+        Provision
+      </Button>
     </>
   );
 };
@@ -374,7 +380,8 @@ const ExpandHeader = styled.div<{ isExpanded: boolean }>`
   > i {
     margin-right: 7px;
     margin-left: -7px;
-    transform: ${(props) => props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
+    transform: ${(props) =>
+      props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
   }
 `;
 
@@ -398,28 +405,41 @@ const ErrorContainer = styled.div`
   font-size: 13px;
   margin-bottom: 30px;
   color: red;
-`
+`;
 
-const AWS_LOGIN_ERROR_MESSAGE = "Porter could not access your AWS account. Please make sure you have granted permissions and try again."
-const AWS_EIP_QUOTA_ERROR_MESSAGE = "Your AWS account has reached the limit of elastic IPs allowed in the region. Additional addresses must be requested in order to provision."
-const AWS_VPC_QUOTA_ERROR_MESSAGE = "Your AWS account has reached the limit of VPCs allowed in the region. Additional VPCs must be requested in order to provision."
-const AWS_NAT_GATEWAY_QUOTA_ERROR_MESSAGE = "Your AWS account has reached the limit of NAT Gateways allowed in the region. Additional NAT Gateways must be requested in order to provision."
-const AWS_VCPU_QUOTA_ERROR_MESSAGE = "Your AWS account has reached the limit of vCPUs allowed in the region. Additional vCPUs must be requested in order to provision."
-const DEFAULT_ERROR_MESSAGE = "An error occurred while provisioning your infrastructure. Please try again."
+const AWS_LOGIN_ERROR_MESSAGE =
+  "Porter could not access your AWS account. Please make sure you have granted permissions and try again.";
+const AWS_EIP_QUOTA_ERROR_MESSAGE =
+  "Your AWS account has reached the limit of elastic IPs allowed in the region. Additional addresses must be requested in order to provision.";
+const AWS_VPC_QUOTA_ERROR_MESSAGE =
+  "Your AWS account has reached the limit of VPCs allowed in the region. Additional VPCs must be requested in order to provision.";
+const AWS_NAT_GATEWAY_QUOTA_ERROR_MESSAGE =
+  "Your AWS account has reached the limit of NAT Gateways allowed in the region. Additional NAT Gateways must be requested in order to provision.";
+const AWS_VCPU_QUOTA_ERROR_MESSAGE =
+  "Your AWS account has reached the limit of vCPUs allowed in the region. Additional vCPUs must be requested in order to provision.";
+const DEFAULT_ERROR_MESSAGE =
+  "An error occurred while provisioning your infrastructure. Please try again.";
 
 const errorMessageToModal = (errorMessage: string) => {
   switch (errorMessage) {
     case AWS_LOGIN_ERROR_MESSAGE:
       return (
         <>
-          <Text size={16} weight={500}>Granting Porter access to AWS</Text>
+          <Text size={16} weight={500}>
+            Granting Porter access to AWS
+          </Text>
           <Spacer y={1} />
           <Text color="helper">
-            Porter needs access to your AWS account in order to create infrastructure. You can grant Porter access to AWS by following these steps:
+            Porter needs access to your AWS account in order to create
+            infrastructure. You can grant Porter access to AWS by following
+            these steps:
           </Text>
           <Spacer y={1} />
           <Step number={1}>
-            <Link to="https://aws.amazon.com/resources/create-account/" target="_blank">
+            <Link
+              to="https://aws.amazon.com/resources/create-account/"
+              target="_blank"
+            >
               Create an AWS account
             </Link>
             <Spacer inline width="5px" />
@@ -429,139 +449,248 @@ const errorMessageToModal = (errorMessage: string) => {
           <Step number={2}>
             Once you are logged in to your AWS account,
             <Spacer inline width="5px" />
-            <Link to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account" target="_blank">
+            <Link
+              to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+              target="_blank"
+            >
               copy your account ID
-            </Link>.
+            </Link>
+            .
           </Step>
           <Spacer y={1} />
-          <Step number={3}>Fill in your account ID on Porter and select "Grant permissions".</Step>
+          <Step number={3}>
+            Fill in your account ID on Porter and select "Grant permissions".
+          </Step>
           <Spacer y={1} />
-          <Step number={4}>After being redirected to AWS, select "Create stack" on the AWS console.</Step>
+          <Step number={4}>
+            After being redirected to AWS, select "Create stack" on the AWS
+            console.
+          </Step>
           <Spacer y={1} />
           <Step number={5}>Return to Porter and select "Continue".</Step>
         </>
-      )
+      );
     case AWS_EIP_QUOTA_ERROR_MESSAGE:
       return (
         <>
-          <Text size={16} weight={500}>Requesting more EIP Adresses</Text>
+          <Text size={16} weight={500}>
+            Requesting more EIP Adresses
+          </Text>
           <Spacer y={1} />
           <Text color="helper">
-            You will need to either request more EIP addresses or delete existing ones in order to provision in the region specified. You can request more addresses by following these steps:
+            You will need to either request more EIP addresses or delete
+            existing ones in order to provision in the region specified. You can
+            request more addresses by following these steps:
           </Text>
           <Spacer y={1} />
           <Step number={1}>
             Log into
             <Spacer inline width="5px" />
-            <Link to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account" target="_blank">your AWS account
-            </Link>.
+            <Link
+              to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+              target="_blank"
+            >
+              your AWS account
+            </Link>
+            .
           </Step>
           <Spacer y={1} />
           <Step number={2}>
             Navigate to
             <Spacer inline width="5px" />
-            <Link to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/ec2/quotas" target="_blank">the Amazon Elastic Compute Cloud (Amazon EC2) Service Quotas portal
-            </Link>.
+            <Link
+              to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/ec2/quotas"
+              target="_blank"
+            >
+              the Amazon Elastic Compute Cloud (Amazon EC2) Service Quotas
+              portal
+            </Link>
+            .
           </Step>
           <Spacer y={1} />
-          <Step number={3}>Search for "EC2-VPC Elastic IPs" in the search box and click on the search result.</Step>
+          <Step number={3}>
+            Search for "EC2-VPC Elastic IPs" in the search box and click on the
+            search result.
+          </Step>
           <Spacer y={1} />
-          <Step number={4}>Click on "Request quota increase". In order to provision with Porter, you will need to request at least 3 addresses above your current quota limit.</Step>
+          <Step number={4}>
+            Click on "Request quota increase". In order to provision with
+            Porter, you will need to request at least 3 addresses above your
+            current quota limit.
+          </Step>
           <Spacer y={1} />
-          <Step number={5}>Once that request is approved, return to Porter and retry the provision.</Step>
+          <Step number={5}>
+            Once that request is approved, return to Porter and retry the
+            provision.
+          </Step>
         </>
-      )
+      );
     case AWS_VPC_QUOTA_ERROR_MESSAGE:
       return (
         <>
-          <Text size={16} weight={500}>Requesting more VPCs</Text>
+          <Text size={16} weight={500}>
+            Requesting more VPCs
+          </Text>
           <Spacer y={1} />
           <Text color="helper">
-            You will need to either request more VPCs or delete existing ones in order to provision in the region specified. You can request more VPCs by following these steps:
+            You will need to either request more VPCs or delete existing ones in
+            order to provision in the region specified. You can request more
+            VPCs by following these steps:
           </Text>
           <Spacer y={1} />
           <Step number={1}>
             Log into
             <Spacer inline width="5px" />
-            <Link to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account" target="_blank">your AWS account
-            </Link>.
+            <Link
+              to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+              target="_blank"
+            >
+              your AWS account
+            </Link>
+            .
           </Step>
           <Spacer y={1} />
           <Step number={2}>
             Navigate to
             <Spacer inline width="5px" />
-            <Link to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/vpc/quotas" target="_blank">the Amazon Virtual Private Cloud (Amazon VPC) Service Quotas portal
-            </Link>.
+            <Link
+              to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/vpc/quotas"
+              target="_blank"
+            >
+              the Amazon Virtual Private Cloud (Amazon VPC) Service Quotas
+              portal
+            </Link>
+            .
           </Step>
           <Spacer y={1} />
-          <Step number={3}>Search for "VPCs per Region" in the search box and click on the search result.</Step>
+          <Step number={3}>
+            Search for "VPCs per Region" in the search box and click on the
+            search result.
+          </Step>
           <Spacer y={1} />
-          <Step number={4}>Click on "Request quota increase". In order to provision with Porter, you will need to request at least 1 VPCs above your current quota limit.</Step>
+          <Step number={4}>
+            Click on "Request quota increase". In order to provision with
+            Porter, you will need to request at least 1 VPCs above your current
+            quota limit.
+          </Step>
           <Spacer y={1} />
-          <Step number={5}>Once that request is approved, return to Porter and retry the provision.</Step>
+          <Step number={5}>
+            Once that request is approved, return to Porter and retry the
+            provision.
+          </Step>
         </>
-      )
+      );
     case AWS_NAT_GATEWAY_QUOTA_ERROR_MESSAGE:
       return (
         <>
-          <Text size={16} weight={500}>Requesting more NAT Gateways</Text>
+          <Text size={16} weight={500}>
+            Requesting more NAT Gateways
+          </Text>
           <Spacer y={1} />
           <Text color="helper">
-            You will need to either request more NAT Gateways or delete existing ones in order to provision in the region specified. You can request more NAT Gateways by following these steps:
+            You will need to either request more NAT Gateways or delete existing
+            ones in order to provision in the region specified. You can request
+            more NAT Gateways by following these steps:
           </Text>
           <Spacer y={1} />
           <Step number={1}>
             Log into
             <Spacer inline width="5px" />
-            <Link to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account" target="_blank">your AWS account
-            </Link>.
+            <Link
+              to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+              target="_blank"
+            >
+              your AWS account
+            </Link>
+            .
           </Step>
           <Spacer y={1} />
           <Step number={2}>
             Navigate to
             <Spacer inline width="5px" />
-            <Link to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/vpc/quotas" target="_blank">the Amazon Virtual Private Cloud (Amazon VPC) Service Quotas portal
-            </Link>.
+            <Link
+              to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/vpc/quotas"
+              target="_blank"
+            >
+              the Amazon Virtual Private Cloud (Amazon VPC) Service Quotas
+              portal
+            </Link>
+            .
           </Step>
           <Spacer y={1} />
-          <Step number={3}>Search for "NAT gateways per Availability Zone" in the search box and click on the search result.</Step>
+          <Step number={3}>
+            Search for "NAT gateways per Availability Zone" in the search box
+            and click on the search result.
+          </Step>
           <Spacer y={1} />
-          <Step number={4}>Click on "Request quota increase". In order to provision with Porter, you will need to request at least 3 NAT Gateways above your current quota limit.</Step>
+          <Step number={4}>
+            Click on "Request quota increase". In order to provision with
+            Porter, you will need to request at least 3 NAT Gateways above your
+            current quota limit.
+          </Step>
           <Spacer y={1} />
-          <Step number={5}>Once that request is approved, return to Porter and retry the provision.</Step>
+          <Step number={5}>
+            Once that request is approved, return to Porter and retry the
+            provision.
+          </Step>
         </>
-      )
+      );
     case AWS_VCPU_QUOTA_ERROR_MESSAGE:
       return (
         <>
-          <Text size={16} weight={500}>Requesting more vCPUs</Text>
+          <Text size={16} weight={500}>
+            Requesting more vCPUs
+          </Text>
           <Spacer y={1} />
           <Text color="helper">
-            You will need to either request more vCPUs or delete existing instances in order to provision in the region specified. You can request more vCPUs by following these steps:
+            You will need to either request more vCPUs or delete existing
+            instances in order to provision in the region specified. You can
+            request more vCPUs by following these steps:
           </Text>
           <Spacer y={1} />
           <Step number={1}>
             Log into
             <Spacer inline width="5px" />
-            <Link to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account" target="_blank">your AWS account
-            </Link>.
+            <Link
+              to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+              target="_blank"
+            >
+              your AWS account
+            </Link>
+            .
           </Step>
           <Spacer y={1} />
           <Step number={2}>
             Navigate to
             <Spacer inline width="5px" />
-            <Link to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/ec2/quotas" target="_blank">the Amazon Elastic Compute Cloud (Amazon EC2) Service Quotas portal
-            </Link>.
+            <Link
+              to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/ec2/quotas"
+              target="_blank"
+            >
+              the Amazon Elastic Compute Cloud (Amazon EC2) Service Quotas
+              portal
+            </Link>
+            .
           </Step>
           <Spacer y={1} />
-          <Step number={3}>Search for "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances" in the search box and click on the search result.</Step>
+          <Step number={3}>
+            Search for "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z)
+            instances" in the search box and click on the search result.
+          </Step>
           <Spacer y={1} />
-          <Step number={4}>Click on "Request quota increase". In order to provision with Porter, you will need to request at least 10 vCPUs above your current quota limit.</Step>
+          <Step number={4}>
+            Click on "Request quota increase". In order to provision with
+            Porter, you will need to request at least 10 vCPUs above your
+            current quota limit.
+          </Step>
           <Spacer y={1} />
-          <Step number={5}>Once that request is approved, return to Porter and retry the provision.</Step>
+          <Step number={5}>
+            Once that request is approved, return to Porter and retry the
+            provision.
+          </Step>
         </>
-      )
+      );
     default:
-      return null
+      return null;
   }
-}
+};

+ 87 - 0
dashboard/src/components/porter/Tooltip.tsx

@@ -0,0 +1,87 @@
+// Tooltip.tsx
+import React, { useState } from "react";
+import styled from "styled-components";
+
+interface TooltipProps {
+  children: React.ReactNode;
+  content: React.ReactNode;
+  position?: "top" | "right" | "bottom" | "left";
+  hidden?: boolean;
+}
+
+const Tooltip: React.FC<TooltipProps> = ({
+  children,
+  content,
+  position = "top",
+  hidden = false,
+}) => {
+  const [isVisible, setIsVisible] = useState(false);
+
+  const showTooltip = () => setIsVisible(true);
+  const hideTooltip = () => setIsVisible(false);
+
+  if (hidden) {
+    return <>{children}</>;
+  }
+
+  return (
+    <TooltipContainer onMouseEnter={showTooltip} onMouseLeave={hideTooltip}>
+      {isVisible && (
+        <TooltipContent position={position}>{content}</TooltipContent>
+      )}
+      {children}
+    </TooltipContainer>
+  );
+};
+
+export default Tooltip;
+
+const TooltipContainer = styled.div`
+  position: relative;
+  display: inline-flex;
+`;
+
+const TooltipContent = styled.div<{ position: string }>`
+  background-color: #333;
+  color: #fff;
+  padding: 8px;
+  border-radius: 4px;
+  font-size: 14px;
+  position: absolute;
+  z-index: 10;
+  max-width: 200px;
+  text-align: center;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+
+  ${({ position }) => {
+    switch (position) {
+      case "top":
+        return `
+          bottom: 100%;
+          left: 50%;
+          transform: translateX(-50%) translateY(-8px);
+        `;
+      case "right":
+        return `
+          top: 50%;
+          left: 100%;
+          transform: translateY(-50%) translateX(8px);
+        `;
+      case "bottom":
+        return `
+          top: 100%;
+          left: 50%;
+          transform: translateX(-50%) translateY(8px);
+        `;
+      case "left":
+        return `
+          top: 50%;
+          right: 100%;
+          transform: translateY(-50%) translateX(-8px);
+        `;
+      default:
+        return "";
+    }
+  }};
+`;

+ 3 - 2
dashboard/src/main/home/Home.tsx

@@ -28,6 +28,7 @@ import ModalHandler from "./ModalHandler";
 import { NewProjectFC } from "./new-project/NewProject";
 import InfrastructureRouter from "./infrastructure/InfrastructureRouter";
 import { overrideInfraTabEnabled } from "utils/infrastructure";
+import NoClusterPlaceHolder from "components/NoClusterPlaceHolder";
 
 // Guarded components
 const GuardedProjectSettings = fakeGuardedRoute("settings", "", [
@@ -438,7 +439,7 @@ const Home: React.FC<Props> = (props) => {
               } else if (!currentCluster || !currentCluster.name) {
                 return (
                   <DashboardWrapper>
-                    <PageNotFound />
+                    <NoClusterPlaceHolder></NoClusterPlaceHolder>
                   </DashboardWrapper>
                 );
               }
@@ -492,7 +493,7 @@ const ViewWrapper = styled.div`
   flex: 1;
   overflow-y: auto;
   justify-content: center;
-  background: ${props => props.theme.bg};
+  background: ${(props) => props.theme.bg};
   position: relative;
 `;
 

+ 9 - 5
dashboard/src/main/home/cluster-dashboard/DashboardHeader.tsx

@@ -5,6 +5,7 @@ import { Context } from "shared/Context";
 
 import TitleSection from "components/TitleSection";
 import Spacer from "components/porter/Spacer";
+import Tooltip from "components/porter/Tooltip";
 
 type PropsType = {
   image?: any;
@@ -22,7 +23,9 @@ export default class DashboardHeader extends Component<PropsType, StateType> {
     return (
       <>
         <TitleSection
-          capitalize={this.props.capitalize === undefined || this.props.capitalize}
+          capitalize={
+            this.props.capitalize === undefined || this.props.capitalize
+          }
           icon={this.props.image}
           materialIconClass={this.props.materialIconClass}
         >
@@ -34,9 +37,11 @@ export default class DashboardHeader extends Component<PropsType, StateType> {
             <Spacer height="35px" />
             <InfoSection>
               <TopRow>
-                <InfoLabel>
-                  <i className="material-icons">info</i> Info
-                </InfoLabel>
+                <Tooltip content="TestInfo" position="bottom" hidden={true}>
+                  <InfoLabel>
+                    <i className="material-icons">info</i> Info
+                  </InfoLabel>
+                </Tooltip>
               </TopRow>
               <Description>{this.props.description}</Description>
             </InfoSection>
@@ -85,7 +90,6 @@ const InfoLabel = styled.div`
 `;
 
 const InfoSection = styled.div`
-
   font-family: "Work Sans", sans-serif;
   margin-left: 0px;
 `;

+ 131 - 116
dashboard/src/main/home/cluster-dashboard/dashboard/ClusterRevisionSelector.tsx

@@ -14,9 +14,11 @@ import {
   EKS,
   NodeGroupType,
   EnumKubernetesKind,
-  EnumCloudProvider
+  EnumCloudProvider,
 } from "@porter-dev/api-contracts";
 import Spacer from "components/porter/Spacer";
+import { createPortal } from "react-dom";
+import ConfirmOverlay from "components/ConfirmOverlay";
 
 type Props = {
   selectedClusterVersion: any;
@@ -31,6 +33,7 @@ const ClusterRevisionSelector: React.FC<Props> = ({
   setShowProvisionerStatus,
   setProvisionFailureReason,
 }) => {
+  const [showConfirmOverlay, setShowConfirmOverlay] = useState(false);
   const { currentProject, currentCluster } = useContext(Context);
   const [versions, setVersions] = useState<any[]>(null);
   const [selectedId, setSelectedId] = useState(null);
@@ -51,7 +54,9 @@ const ClusterRevisionSelector: React.FC<Props> = ({
 
       if (data[0].condition !== "") {
         setFailedContractId(data[0].id);
-        setProvisionFailureReason(data[0].condition_metadata?.message || data[0].condition);
+        setProvisionFailureReason(
+          data[0].condition_metadata?.message || data[0].condition
+        );
       }
     }
 
@@ -61,7 +66,9 @@ const ClusterRevisionSelector: React.FC<Props> = ({
 
     // Handle active provisioning attempt
     if (activeCandidate) {
-      setSelectedClusterVersion(JSON.parse(atob(activeCandidate.base64_contract)));
+      setSelectedClusterVersion(
+        JSON.parse(atob(activeCandidate.base64_contract))
+      );
       setSelectedId(-1);
       setShowProvisionerStatus(true);
     } else {
@@ -70,14 +77,11 @@ const ClusterRevisionSelector: React.FC<Props> = ({
       setShowProvisionerStatus(false);
     }
     setVersions(successes);
-  }
+  };
 
   const updateContracts = () => {
-    api.getContracts(
-      "<token>",
-      {},
-      { project_id: currentProject.id },
-    )
+    api
+      .getContracts("<token>", {}, { project_id: currentProject.id })
       .then(({ data }) => {
         const filtered_data = data.filter((x: any) => {
           return x.cluster_id === currentCluster.id;
@@ -100,13 +104,11 @@ const ClusterRevisionSelector: React.FC<Props> = ({
 
   const createContract = () => {
     if (false) {
-      api.createContract(
-        "<token>",
-        selectedClusterVersion,
-        { project_id: currentProject.id }
-      )
-        .then(() => {
+      api
+        .createContract("<token>", selectedClusterVersion, {
+          project_id: currentProject.id,
         })
+        .then(() => {})
         .catch((err) => {
           console.log(err);
         });
@@ -114,14 +116,15 @@ const ClusterRevisionSelector: React.FC<Props> = ({
   };
 
   const deleteContract = () => {
-    api.deleteContract(
-      "<token>",
-      {},
-      {
-        project_id: currentProject.id,
-        revision_id: failedContractId,
-      }
-    )
+    api
+      .deleteContract(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          revision_id: failedContractId,
+        }
+      )
       .then(() => {
         updateContracts();
       })
@@ -136,7 +139,9 @@ const ClusterRevisionSelector: React.FC<Props> = ({
         <Tr
           key={i}
           onClick={() => {
-            setSelectedClusterVersion(JSON.parse(atob(version.base64_contract)));
+            setSelectedClusterVersion(
+              JSON.parse(atob(version.base64_contract))
+            );
             setSelectedId(i);
             setShowProvisionerStatus(false);
           }}
@@ -163,34 +168,31 @@ const ClusterRevisionSelector: React.FC<Props> = ({
     return (
       <Tr
         onClick={() => {
-          setSelectedClusterVersion(JSON.parse(atob(pendingContract.base64_contract)));
+          setSelectedClusterVersion(
+            JSON.parse(atob(pendingContract.base64_contract))
+          );
           setSelectedId(-1);
           setShowProvisionerStatus(true);
         }}
         selected={selectedId === -1}
       >
         <Td>
-          {
-            failedContractId ? (
-              <Failed>Update failed</Failed>
-            ) : (
-              <Flex><Img src={loading} /> Updating</Flex>
-            )
-          }
+          {failedContractId ? (
+            <Failed>Update failed</Failed>
+          ) : (
+            <Flex>
+              <Img src={loading} /> Updating
+            </Flex>
+          )}
         </Td>
         <Td>{readableDate(pendingContract.CreatedAt)}</Td>
-        {
-          failedContractId && (
-            <DeleteButton>
-              <i
-                className="material-icons-outlined"
-                onClick={deleteContract}
-              >
-                close
-              </i>
-            </DeleteButton>
-          )
-        }
+        {failedContractId && (
+          <DeleteButton>
+            <div onClick={() => setShowConfirmOverlay(true)}>
+              Clear Revision
+            </div>
+          </DeleteButton>
+        )}
         {/*
         <Td>
           <RollbackButton
@@ -207,68 +209,74 @@ const ClusterRevisionSelector: React.FC<Props> = ({
 
   return (
     <>
-      {
-        hideSelector ? (
-          <></>
-        ) : (
-          <>
-            <StyledClusterRevisionSelector>
-              <ExpandableSection
-                isInitiallyExpanded={false}
-                color={selectedId <= 0 ? "#ffffff66" : "#f5cb42"}
-                Header={(
-                  <>
-                    <Label isCurrent={selectedId <= 0}>
-                      {
-                        selectedId === 0 ? (
-                          "Current version -"
-                        ) : (
-                          selectedId === -1 ? (
-                            failedContractId ? (
-                              ""
-                            ) : (
-                              "In progress -"
-                            )
-                          ) : (
-                            "Previewing version (not deployed) -"
-                          )
-                        )
-                      }
-                    </Label>
-                    {
-                      selectedId === -1 ? (
-                        failedContractId ? (
-                          <><WarningIcon src={warning} /> Last update failed</>
-                        ) : (
-                          <><Img src={loading} /> Updating</>
-                        )
-                      ) : (
-                        `No. ${versions?.length - selectedId}`
-                      )
-                    }
-                  </>
-                )}
-                ExpandedSection={(
-                  <TableWrapper>
-                    <RevisionsTable>
-                      <tbody>
-                        <Tr disableHover={true}>
-                          <Th>Version no.</Th>
-                          <Th>Created</Th>
-                          {/* <Th>Rollback</Th> */}
-                        </Tr>
-                        {(pendingContract || failedContractId) && renderActiveAttempt()}
-                        {renderVersionList()}
-                      </tbody>
-                    </RevisionsTable>
-                  </TableWrapper>
-                )}
-              />
-            </StyledClusterRevisionSelector>
-            <Spacer y={1} />
-          </>
-        )
-      }
+      {hideSelector ? (
+        <></>
+      ) : (
+        <>
+          <StyledClusterRevisionSelector>
+            <ExpandableSection
+              isInitiallyExpanded={false}
+              color={selectedId <= 0 ? "#ffffff66" : "#f5cb42"}
+              Header={
+                <>
+                  <Label isCurrent={selectedId <= 0}>
+                    {selectedId === 0
+                      ? "Current version -"
+                      : selectedId === -1
+                      ? failedContractId
+                        ? ""
+                        : "In progress -"
+                      : "Previewing version (not deployed) -"}
+                  </Label>
+                  {selectedId === -1 ? (
+                    failedContractId ? (
+                      <>
+                        <WarningIcon src={warning} /> Last update failed
+                      </>
+                    ) : (
+                      <>
+                        <Img src={loading} /> Updating
+                      </>
+                    )
+                  ) : (
+                    `No. ${versions?.length - selectedId}`
+                  )}
+                </>
+              }
+              ExpandedSection={
+                <TableWrapper>
+                  <RevisionsTable>
+                    <tbody>
+                      <Tr disableHover={true}>
+                        <Th>Version no.</Th>
+                        <Th>Created</Th>
+                        {/* <Th>Rollback</Th> */}
+                      </Tr>
+                      {(pendingContract || failedContractId) &&
+                        renderActiveAttempt()}
+                      {renderVersionList()}
+                    </tbody>
+                  </RevisionsTable>
+                </TableWrapper>
+              }
+            />
+          </StyledClusterRevisionSelector>
+          <Spacer y={1} />
+        </>
+      )}
+      {showConfirmOverlay &&
+        createPortal(
+          <ConfirmOverlay
+            show={true}
+            message={`Clear the failed revision?`}
+            onYes={() => {
+              deleteContract();
+              setShowConfirmOverlay(false);
+            }}
+            onNo={() => setShowConfirmOverlay(false)}
+          />,
+          document.body
+        )}
     </>
   );
 };
@@ -277,18 +285,26 @@ export default ClusterRevisionSelector;
 
 const DeleteButton = styled.div`
   position: absolute;
-  right: 10px;
+  right: 40px;
   top: 0px;
   height: 100%;
   display: flex;
   align-items: center;
+  justify-content: center;
 
-  > i {
-    font-size: 16px;
+  > div {
+    font-size: 13px;
     padding: 5px;
+    background: #cc3d42;
+    color: white;
+    border-radius: 3px;
+    height: 22px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
     :hover {
-      background: #ffffff22;
-      border-radius: 40px;
+      background: #990205;
     }
   }
 `;
@@ -337,7 +353,7 @@ const RollbackButton = styled.div`
     props.disabled ? "#aaaabbee" : "#616FEEcc"};
   :hover {
     background: ${(props: { disabled: boolean }) =>
-    props.disabled ? "" : "#405eddbb"};
+      props.disabled ? "" : "#405eddbb"};
   }
 `;
 
@@ -351,7 +367,7 @@ const Tr = styled.tr`
     props.selected ? "#ffffff11" : ""};
   :hover {
     background: ${(props: { disableHover?: boolean; selected?: boolean }) =>
-    props.disableHover ? "" : "#ffffff22"};
+      props.disableHover ? "" : "#ffffff22"};
   }
 `;
 
@@ -386,9 +402,8 @@ const TableWrapper = styled.div`
 `;
 
 const Label = styled.div<{ isCurrent?: boolean }>`
-  color: ${props => props.isCurrent ? "#ffffff66" : "#f5cb42"};
+  color: ${(props) => (props.isCurrent ? "#ffffff66" : "#f5cb42")};
   margin-right: 5px;
 `;
 
-const StyledClusterRevisionSelector = styled.div`
-`;
+const StyledClusterRevisionSelector = styled.div``;

+ 70 - 2
dashboard/src/main/home/cluster-dashboard/dashboard/ClusterSettings.tsx

@@ -7,8 +7,16 @@ import { Context } from "shared/Context";
 import api from "shared/api";
 import CheckboxRow from "components/form-components/CheckboxRow";
 import Loading from "components/Loading";
+import CopyToClipboard from "components/CopyToClipboard";
+import { DetailedIngressError } from "shared/types";
+import { RouteComponentProps } from "react-router";
 
-const ClusterSettings: React.FC = () => {
+type Props = RouteComponentProps & {
+  ingressIp: string;
+  ingressError: DetailedIngressError;
+};
+
+const ClusterSettings: React.FC<Props> = (props) => {
   const {
     currentProject,
     currentCluster,
@@ -28,6 +36,7 @@ const ClusterSettings: React.FC = () => {
   const [secretKey, setSecretKey] = useState<string>("");
   const [startRotateCreds, setStartRotateCreds] = useState<boolean>(false);
   const [successfulRotate, setSuccessfulRotate] = useState<boolean>(false);
+
   const [enableAgent, setEnableAgent] = useState(
     currentCluster.agent_integration_enabled
   );
@@ -241,6 +250,41 @@ const ClusterSettings: React.FC = () => {
       </Button>
     </div>
   );
+  let configureUrl = (
+    ingressIp: string | undefined,
+    ingressError: DetailedIngressError
+  ) => {
+    if (typeof ingressIp !== "string") {
+      return <></>;
+    }
+
+    if (!ingressIp.length && ingressError) {
+      return <></>;
+    }
+
+    if (!ingressIp.length) {
+      return <></>;
+    }
+    return (
+      <>
+        <div>
+          <Heading>Configure Custom Domain</Heading>
+          <Helper>
+            To configure custom domains for your apps, add a CNAME record
+            pointing to the following Ingress IP:
+          </Helper>
+          <CopyToClipboard
+            as={Url}
+            text={ingressIp}
+            wrapperProps={{ onClick: (e: any) => e.stopPropagation() }}
+          >
+            <span>{ingressIp}</span>
+            <i className="material-icons-outlined">content_copy</i>
+          </CopyToClipboard>
+        </div>
+      </>
+    );
+  };
 
   let enableAgentIntegration = (
     <div>
@@ -298,6 +342,11 @@ const ClusterSettings: React.FC = () => {
   return (
     <div>
       <StyledSettingsSection>
+        <DarkMatter />
+        {props.ingressIp && (
+          <>{configureUrl(props.ingressIp, props.ingressError)}</>
+        )}
+
         <DarkMatter />
         {enableAgentIntegration}
         <DarkMatter />
@@ -339,7 +388,7 @@ const StyledSettingsSection = styled.div`
   overflow: auto;
   height: 100%;
   border-radius: 5px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   border: 1px solid #494b4f;
 `;
 
@@ -372,3 +421,22 @@ const Warning = styled.div`
     props.highlight ? "#f5cb42" : ""};
   margin-bottom: 20px;
 `;
+const Url = styled.a`
+  font-size: 13px;
+  user-select: text;
+  font-weight: 400;
+  display: flex;
+  align-items: center;
+  margin-left: 20px;
+  cursor: pointer;
+  > i {
+    margin-left: 10px;
+    font-size: 15px;
+  }
+
+  > span {
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+`;

+ 47 - 105
dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx

@@ -1,10 +1,9 @@
 import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import { useLocation } from "react-router";
-import settings from "assets/settings.svg";
+import editIcon from "assets/edit-button.svg";
 
 import api from "shared/api";
-import { DetailedIngressError } from "shared/types";
 import { getQueryParam } from "shared/routing";
 import useAuth from "shared/auth/useAuth";
 import { Context } from "shared/Context";
@@ -20,11 +19,16 @@ import ClusterSettings from "./ClusterSettings";
 import Metrics from "./Metrics";
 import ClusterSettingsModal from "./ClusterSettingsModal";
 
-import CopyToClipboard from "components/CopyToClipboard";
 import Loading from "components/Loading";
 import Spacer from "components/porter/Spacer";
 
-type TabEnum = "nodes" | "settings" | "namespaces" | "metrics" | "incidents" | "configuration";
+type TabEnum =
+  | "nodes"
+  | "settings"
+  | "namespaces"
+  | "metrics"
+  | "incidents"
+  | "configuration";
 
 var tabOptions: {
   label: string;
@@ -46,7 +50,15 @@ export const Dashboard: React.FunctionComponent = () => {
   const renderTab = () => {
     switch (currentTab) {
       case "settings":
-        return <ClusterSettings />;
+        return (
+          <ClusterSettings
+            ingressIp={ingressIp}
+            ingressError={ingressError}
+            history={undefined}
+            location={undefined}
+            match={undefined}
+          />
+        );
       case "metrics":
         return <Metrics />;
       case "namespaces":
@@ -57,8 +69,11 @@ export const Dashboard: React.FunctionComponent = () => {
             <Br />
             <ProvisionerSettings
               selectedClusterVersion={selectedClusterVersion}
+              provisionerError={provisionFailureReason}
               clusterId={context.currentCluster.id}
-              credentialId={context.currentCluster.cloud_provider_credential_identifier}
+              credentialId={
+                context.currentCluster.cloud_provider_credential_identifier
+              }
             />
           </>
         );
@@ -74,9 +89,11 @@ export const Dashboard: React.FunctionComponent = () => {
     ) {
       if (!context.currentProject.capi_provisioner_enabled) {
         tabOptions.unshift({ label: "Namespaces", value: "namespaces" });
+        tabOptions.unshift({ label: "Metrics", value: "metrics" });
+        tabOptions.unshift({ label: "Nodes", value: "nodes" });
       }
-      tabOptions.unshift({ label: "Metrics", value: "metrics" });
-      tabOptions.unshift({ label: "Nodes", value: "nodes" });
+      // tabOptions.unshift({ label: "Metrics", value: "metrics" });
+      // tabOptions.unshift({ label: "Nodes", value: "nodes" });
     }
 
     if (
@@ -115,52 +132,6 @@ export const Dashboard: React.FunctionComponent = () => {
     }
   }, [context.currentCluster]);
 
-  const renderIngressIp = (
-    ingressIp: string | undefined,
-    ingressError: DetailedIngressError
-  ) => {
-    if (typeof ingressIp !== "string") {
-      return (
-        <Url onClick={(e) => e.preventDefault()}>
-          <Loading />
-        </Url>
-      );
-    }
-
-    if (!ingressIp.length && ingressError) {
-      return (
-        <>
-          <Bolded>Ingress IP:</Bolded>
-          <span>{ingressError.message}</span>
-        </>
-      );
-    }
-
-    if (!ingressIp.length) {
-      return (
-        <>
-          <Bolded>Ingress IP:</Bolded>
-          <span>Ingress IP not available</span>
-        </>
-      );
-    }
-
-    return (
-      <>
-        <Bolded>To configure custom domains for your apps, add a CNAME record pointing to the following Ingress IP:</Bolded>
-        <br /><br />
-        <CopyToClipboard
-          as={Url}
-          text={ingressIp}
-          wrapperProps={{ onClick: (e: any) => e.stopPropagation() }}
-        >
-          <span>{ingressIp}</span>
-          <i className="material-icons-outlined">content_copy</i>
-        </CopyToClipboard>
-      </>
-    );
-  };
-
   const updateClusterWithDetailedData = async () => {
     try {
       const res = await api.getCluster(
@@ -176,7 +147,7 @@ export const Dashboard: React.FunctionComponent = () => {
         setIngressIp(ingress_ip);
         setIngressError(ingress_error);
       }
-    } catch (error) { }
+    } catch (error) {}
   };
 
   useEffect(() => {
@@ -193,12 +164,9 @@ export const Dashboard: React.FunctionComponent = () => {
             setShowProvisionerStatus={setShowProvisionerStatus}
             setProvisionFailureReason={setProvisionFailureReason}
           />
-          {(
-            showProvisionerStatus && (
-              context.currentCluster.status === "UPDATING" ||
-              context.currentCluster.status === "UPDATING_UNAVAILABLE"
-            )
-          ) && (
+          {showProvisionerStatus &&
+            (context.currentCluster.status === "UPDATING" ||
+              context.currentCluster.status === "UPDATING_UNAVAILABLE") && (
               <>
                 <ProvisionerStatus
                   provisionFailureReason={provisionFailureReason}
@@ -253,7 +221,7 @@ export const Dashboard: React.FunctionComponent = () => {
                   stroke="white"
                   strokeWidth="1.5"
                   strokeLinecap="round"
-                  strokeLinejoin="round"
+                  stroke-linejoin="round"
                 />
                 <path
                   fillRule="evenodd"
@@ -289,23 +257,22 @@ export const Dashboard: React.FunctionComponent = () => {
                 />
               </svg>
               <Spacer inline />
-              {context.currentCluster.vanity_name || context.currentCluster.name}
+              {context.currentCluster.vanity_name ||
+                context.currentCluster.name}
               <Spacer inline />
             </Flex>
-            <SettingsIcon onClick={() => {
-              context.setCurrentModal(<ClusterSettingsModal />);
-            }}>
-              <img src={settings} />
-            </SettingsIcon>
+            <EditIconStyle
+              onClick={() => {
+                context.setCurrentModal(<ClusterSettingsModal />);
+              }}
+            >
+              <img src={editIcon} />
+            </EditIconStyle>
           </Flex>
         }
-        description={
-          ingressIp ? (
-            <>{renderIngressIp(ingressIp, ingressError)}</>
-          ) : (
-            `Cluster settings and status for ${context.currentCluster.vanity_name || context.currentCluster.name}.`
-          )
-        }
+        description={`Cluster settings and status for ${
+          context.currentCluster.vanity_name || context.currentCluster.name
+        }.`}
         disableLineBreak
         capitalize={false}
       />
@@ -315,16 +282,16 @@ export const Dashboard: React.FunctionComponent = () => {
   );
 };
 
-const SettingsIcon = styled.div`
-  width: 30px;
-  height: 30px;
-  margin-left: 3px;
+const EditIconStyle = styled.div`
+  width: 20px;
+  height: 20px;
+  margin-left: -5px;
   cursor: pointer;
   display: flex;
   justify-content: center;
   align-items: center;
   border-radius: 40px;
-  margin-bottom: -2px;
+  margin-bottom: 3px;
   :hover {
     background: #ffffff18;
   }
@@ -344,28 +311,3 @@ const Br = styled.div`
   width: 100%;
   height: 35px;
 `;
-
-const Url = styled.a`
-  font-size: 13px;
-  user-select: text;
-  font-weight: 400;
-  display: flex;
-  align-items: center;
-  cursor: pointer;
-  > i {
-    margin-left: 10px;
-    font-size: 15px;
-  }
-
-  > span {
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-  }
-`;
-
-const Bolded = styled.span`
-  color: #aaaabb;
-  margin-right: 6px;
-  white-space: nowrap;
-`;