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

cloud select flow + basic credentials screen

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

+ 101 - 0
dashboard/src/components/CredentialsForm.tsx

@@ -0,0 +1,101 @@
+import React, { useEffect, useState, useContext, useMemo } from "react";
+import styled from "styled-components";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+
+import Heading from "components/form-components/Heading";
+import Helper from "./form-components/Helper";
+
+type Props = {
+};
+
+type AWSCredential = {
+  created_at: string;
+  id: number;
+  user_id: number;
+  project_id: number;
+  aws_arn: string;
+};
+
+
+const CredentialsForm: React.FC<Props> = ({
+}) => {
+  const { currentProject } = useContext(Context);
+  const [awsCredentials, setAWSCredentials] = useState<AWSCredential[]>(null);
+  const [isLoading, setIsLoading] = useState(true);
+
+  useEffect(() => {
+    api
+      .getAWSIntegration(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        if (!Array.isArray(data)) {
+          throw Error("Data is not an array");
+        }
+
+        setAWSCredentials(data);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+      });
+  }, [currentProject]);
+
+  return (
+    <StyledCredentialsForm>
+      <Heading isAtTop>
+        AWS credentials
+      </Heading>
+      <Helper>
+        Select your credentials from the list below, or link a new set of credentials:
+      </Helper>
+      {
+        isLoading ? (
+          <>Loading . . .</>
+        ) : (
+          <CredentialList>
+            {
+              awsCredentials.map((cred: AWSCredential, i: number) => {
+                return (
+                  <Credential key={cred.id} isLast={awsCredentials.length - 1 === i}>
+                    {cred.aws_arn || "n/a"}
+                  </Credential>
+                )
+              })
+            }
+          </CredentialList>
+        )
+      }
+    </StyledCredentialsForm>
+  );
+};
+
+export default CredentialsForm;
+
+const Credential = styled.div<{ isLast?: boolean}>`
+  height: 50px;
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  border-bottom: ${props => props.isLast ? "" : "1px solid #aaaabb"};
+`;
+
+const CredentialList = styled.div`
+  width: 100%;
+  border: 1px solid #aaaabb;
+  border-radius: 5px;
+`;
+
+const StyledCredentialsForm = styled.div`
+  padding: 30px;
+  border-radius: 5px;
+  background: #26292e;
+  border: 1px solid #494b4f;
+  font-size: 13px;
+`;

+ 140 - 0
dashboard/src/components/ProvisionerFlow.tsx

@@ -0,0 +1,140 @@
+import React, { useState, useContext, useMemo } from "react";
+import styled from "styled-components";
+
+import { integrationList } from "shared/common";
+import { Context } from "shared/Context";
+
+import CredentialsForm from "components/CredentialsForm";
+import Helper from "components/form-components/Helper";
+
+const providers = ["aws", "gcp", "azure"];
+
+type Props = {
+};
+
+const ProvisionerFlow: React.FC<Props> = ({
+}) => {
+  const { usage, hasBillingEnabled } = useContext(Context);
+  const [currentStep, setCurrentStep] = useState("cloud");
+
+  const isUsageExceeded = useMemo(() => {
+    if (!hasBillingEnabled) {
+      return false;
+    }
+    return usage.current.clusters >= usage.limit.clusters;
+  }, [usage]);
+
+  if (currentStep === "cloud") {
+    return (
+      <StyledProvisionerFlow>
+        <Helper>
+          Select your hosting backend:
+        </Helper>
+        <BlockList>
+          {providers.map((provider: string, i: number) => {
+            let providerInfo = integrationList[provider];
+            return (
+              <Block
+                key={i}
+                disabled={isUsageExceeded || provider === "gcp" || provider === "azure"}
+                onClick={() => {
+                  if (!(isUsageExceeded || provider === "gcp" || provider === "azure")) {
+                    setCurrentStep("credentials");
+                  }
+                }}
+              >
+                <Icon src={providerInfo.icon} />
+                <BlockTitle>{providerInfo.label}</BlockTitle>
+                <BlockDescription>{providerInfo.tagline || "Hosted in your own cloud"}</BlockDescription>
+              </Block>
+            );
+          })}
+        </BlockList>
+      </StyledProvisionerFlow>
+    );
+  } else if (currentStep === "credentials") {
+    return <CredentialsForm />;
+  }
+};
+
+export default ProvisionerFlow;
+
+const BlockList = styled.div`
+  overflow: visible;
+  margin-top: 25px;
+  margin-bottom: 27px;
+  display: grid;
+  grid-column-gap: 25px;
+  grid-row-gap: 25px;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+`;
+
+const Icon = styled.img<{ bw?: boolean }>`
+  height: 42px;
+  margin-top: 30px;
+  margin-bottom: 15px;
+  filter: ${(props) => (props.bw ? "grayscale(1)" : "")};
+`;
+
+const BlockDescription = styled.div`
+  margin-bottom: 12px;
+  color: #ffffff66;
+  text-align: center;
+  font-weight: 400;
+  font-size: 13px;
+  padding: 0px 25px;
+  height: 2.4em;
+  font-size: 12px;
+  display: -webkit-box;
+  overflow: hidden;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+`;
+
+const BlockTitle = styled.div`
+  margin-bottom: 12px;
+  width: 80%;
+  text-align: center;
+  font-size: 14px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const Block = styled.div<{ disabled?: boolean }>`
+  align-items: center;
+  user-select: none;
+  display: flex;
+  font-size: 13px;
+  overflow: hidden;
+  font-weight: 500;
+  padding: 3px 0px 5px;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+  height: 170px;
+  filter: ${({ disabled }) => (disabled ? "brightness(0.8) grayscale(1)" : "")};
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
+  color: #ffffff;
+  position: relative;
+  border-radius: 5px;
+  background: #26292e;
+  border: 1px solid #494b4f;
+  :hover {
+    border: ${(props) => (props.disabled ? "" : "1px solid #7a7b80")};
+  }
+
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const StyledProvisionerFlow = styled.div`
+  margin-top: -24px;
+`;

+ 0 - 1
dashboard/src/main/home/dashboard/ClusterList.tsx

@@ -199,7 +199,6 @@ const CodeBlock = styled.span`
 `;
 
 const StyledClusterList = styled.div`
-  margin-top: -7px;
   padding-left: 2px;
   overflow: visible;
 `;

+ 208 - 0
dashboard/src/main/home/dashboard/ClusterSection.tsx

@@ -0,0 +1,208 @@
+import React, { useState, useContext } from "react";
+import styled from "styled-components";
+
+import { Context } from "shared/Context";
+
+import Banner from "components/Banner";
+
+import ProvisionerFlow from "components/ProvisionerFlow";
+import ClusterPlaceholderContainer from "./ClusterPlaceholderContainer";
+
+type Props = {
+};
+
+const ClusterSection = (props: Props) => {
+  const { usage } = useContext(Context);
+
+  const [currentStep, setCurrentStep] = useState("");
+
+  if (currentStep === "cloud") {
+    return (
+      <>
+        <Flex>
+          <BackButton width="87px" onClick={() => setCurrentStep("")}>
+            <i className="material-icons">first_page</i>
+            Back
+          </BackButton>
+          <Title>
+            <Flex>
+            <ClusterIcon>
+              <svg
+                width="19"
+                height="19"
+                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"
+                  strokeLinejoin="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"
+                  strokeLinejoin="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"
+                  strokeLinejoin="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"
+                  strokeLinejoin="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"
+                  strokeLinejoin="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"
+                  strokeLinejoin="round"
+                />
+              </svg>
+            </ClusterIcon>
+            Provision a new cluster
+            </Flex>
+          </Title>
+        </Flex>
+        <Br />
+        <Banner>
+          You have currently provisioned {usage?.current.cluster || "0"} out of {usage?.limit.clusters || "0"} clusters for this project.
+        </Banner>
+        <Br />
+        <ProvisionerFlow />
+      </>
+    );
+  }
+  return (
+    <>
+      <Button onClick={() => setCurrentStep("cloud")}>
+        <i className="material-icons">add</i> Create a cluster
+      </Button>
+      <ClusterPlaceholderContainer />
+    </>
+  );
+};
+
+export default ClusterSection;
+
+const Br = styled.div`
+  width: 100%;
+  height: 30px;
+`;
+
+const ClusterIcon = styled.div`
+  > svg {
+    width: 20px;
+    display: flex;
+    align-items: center;
+    margin-bottom: -1x;
+    margin-right: 10px;
+    color: #ffffff;
+  }
+`;
+
+const Title = styled.div`
+  font-size: 20px;
+  font-weight: 500;
+  font-family: "Work Sans", sans-serif;
+  margin-left: 15px;
+  border-radius: 2px;
+  color: #ffffff;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const BackButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: pointer;
+  font-size: 13px;
+  height: 35px;
+  padding: 5px 13px;
+  padding-right: 15px;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+    margin-left: -2px;
+  }
+`;
+
+const Button = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 5px;
+  font-weight: 500;
+  width: 147px;
+  margin-bottom: 30px;
+  color: white;
+  height: 30px;
+  padding: 0 8px;
+  padding-right: 13px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;

+ 49 - 75
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -6,18 +6,13 @@ import { Context } from "shared/Context";
 import { InfraType } from "shared/types";
 import api from "shared/api";
 
-import ProvisionerSettings from "../provisioner/ProvisionerSettings";
-import ClusterPlaceholderContainer from "./ClusterPlaceholderContainer";
 import { RouteComponentProps, withRouter } from "react-router";
-import TabRegion from "components/TabRegion";
-import Provisioner from "../provisioner/Provisioner";
 import FormDebugger from "components/porter-form/FormDebugger";
 import TitleSection from "components/TitleSection";
+import ClusterSection from "./ClusterSection";
 
-import { pushFiltered, pushQueryParams } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
-import { StatusPage } from "../onboarding/steps/ProvisionResources/forms/StatusPage";
-import Banner from "components/Banner";
 
 type PropsType = RouteComponentProps &
   WithAuthProps & {
@@ -25,9 +20,6 @@ type PropsType = RouteComponentProps &
     setRefreshClusters: (x: boolean) => void;
   };
 
-// TODO: rethink this list, should be coupled with tabOptions
-const tabOptionStrings = ["overview", "create-cluster", "provisioner"];
-
 type StateType = {
   infras: InfraType[];
   pressingCtrl: boolean;
@@ -93,71 +85,16 @@ class Dashboard extends Component<PropsType, StateType> {
     if (this.props.projectId && prevProps.projectId !== this.props.projectId) {
       this.refreshInfras();
     }
-
-    if (!tabOptionStrings.includes(this.currentTab())) {
-      this.setCurrentTab("overview");
-    }
   }
 
   onShowProjectSettings = () => {
     pushFiltered(this.props, "/project-settings", ["project_id"]);
   };
 
-  currentTab = () => new URLSearchParams(this.props.location.search).get("tab");
-
-  renderTabContents = () => {
-    if (this.currentTab() === "provisioner") {
-      return (
-        <StatusPage
-          filter={[]}
-          project_id={this.props.projectId}
-          setInfraStatus={() => null}
-        />
-      );
-    } else if (this.currentTab() === "create-cluster") {
-      let helperText = "Create a cluster to link to this project";
-      let helperType = "info";
-      if (
-        true
-      ) {
-        helperText =
-          "You need to update your billing to provision or connect a new cluster";
-        helperType = "warning";
-      }
-      return (
-        <>
-          <Banner type={helperType} noMargin>
-            {helperText}
-          </Banner>
-          <Br />
-          <ProvisionerSettings infras={this.state.infras} provisioner={true} />
-        </>
-      );
-    } else {
-      return <ClusterPlaceholderContainer />;
-    }
-  };
-
-  setCurrentTab = (x: string) => {
-    pushQueryParams(this.props, { tab: x });
-  };
-
   render() {
     let { currentProject, capabilities } = this.context;
     let { onShowProjectSettings } = this;
 
-    let tabOptions = [{ label: "Connected clusters", value: "overview" }];
-
-    if (this.props.isAuthorized("cluster", "", ["get", "create"])) {
-      tabOptions.push({ label: "Create a cluster", value: "create-cluster" });
-    }
-
-    tabOptions.push({ label: "Provisioner status", value: "provisioner" });
-
-    if (!capabilities?.provisioner) {
-      tabOptions = [{ label: "Project overview", value: "overview" }];
-    }
-
     return (
       <>
         {currentProject && (
@@ -200,13 +137,7 @@ class Dashboard extends Component<PropsType, StateType> {
                     .
                   </Description>
                 </InfoSection>
-                <TabRegion
-                  currentTab={this.currentTab()}
-                  setCurrentTab={this.setCurrentTab}
-                  options={tabOptions}
-                >
-                  {this.renderTabContents()}
-                </TabRegion>
+                <ClusterSection />
               </>
             )}
           </DashboardWrapper>
@@ -220,6 +151,49 @@ Dashboard.contextType = Context;
 
 export default withRouter(withAuth(Dashboard));
 
+const Button = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 5px;
+  font-weight: 500;
+  width: 147px;
+  margin-bottom: 30px;
+  color: white;
+  height: 30px;
+  padding: 0 8px;
+  padding-right: 13px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;
+
 const Br = styled.div`
   width: 100%;
   height: 1px;
@@ -252,7 +226,7 @@ const TopRow = styled.div`
 `;
 
 const Description = styled.div`
-  color: #8b949f;
+  color: #aaaabb;
   margin-top: 13px;
   margin-left: 2px;
   font-size: 13px;
@@ -263,10 +237,10 @@ const InfoLabel = styled.div`
   height: 20px;
   display: flex;
   align-items: center;
-  color: #8b949f;
+  color: #aaaabb;
   font-size: 13px;
   > i {
-    color: #8b949f;
+    color: #aaaabb;
     font-size: 18px;
     margin-right: 5px;
   }

+ 6 - 4
dashboard/src/shared/common.tsx

@@ -2,6 +2,7 @@ import aws from "../assets/aws.png";
 import digitalOcean from "../assets/do.png";
 import gcp from "../assets/gcp.png";
 import github from "../assets/github.png";
+import azure from "assets/azure.png";
 
 export const infraNames: any = {
   ecr: "Elastic Container Registry (ECR)",
@@ -101,11 +102,12 @@ export const integrationList: any = {
   gcp: {
     icon: gcp,
     label: "GCP",
+    tagline: "Coming soon"
   },
-  gar: {
-    icon:
-      "https://carlossanchez.files.wordpress.com/2019/06/21046548.png?w=640",
-    label: "Google Artifact Registry (GAR)",
+  azure: {
+    icon: azure,
+    label: "Azure",
+    tagline: "Coming soon"
   },
   do: {
     icon: digitalOcean,