jnfrati 4 лет назад
Родитель
Сommit
4cb702c417
1 измененных файлов с 390 добавлено и 2 удалено
  1. 390 2
      dashboard/src/main/home/provisioner/GCPFormSection.tsx

+ 390 - 2
dashboard/src/main/home/provisioner/GCPFormSection.tsx

@@ -1,4 +1,4 @@
-import React, { Component } from "react";
+import React, { Component, useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
 import close from "assets/close.png";
 import close from "assets/close.png";
@@ -16,7 +16,12 @@ import Helper from "components/form-components/Helper";
 import Heading from "components/form-components/Heading";
 import Heading from "components/form-components/Heading";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import CheckboxList from "components/form-components/CheckboxList";
 import CheckboxList from "components/form-components/CheckboxList";
-import { RouteComponentProps, withRouter } from "react-router";
+import {
+  RouteComponentProps,
+  useHistory,
+  useLocation,
+  withRouter,
+} from "react-router";
 import Tooltip from "@material-ui/core/Tooltip";
 import Tooltip from "@material-ui/core/Tooltip";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
@@ -457,6 +462,389 @@ GCPFormSection.contextType = Context;
 
 
 export default withRouter(GCPFormSection);
 export default withRouter(GCPFormSection);
 
 
+const GCPFormSectionFC: React.FC<PropsType> = (props) => {
+  const [gcpRegion, setGcpRegion] = useState("us-east1");
+  const [gcpProjectId, setGcpProjectId] = useState("");
+  const [gcpKeyData, setGcpKeyData] = useState("");
+  const [clusterName, setClusterName] = useState("");
+  const [clusterNameSet, setClusterNameSet] = useState(false);
+  const [selectedInfras, setSelectedInfras] = useState([...provisionOptions]);
+  const [buttonStatus, setButtonStatus] = useState("");
+  const [provisionConfirmed, setProvisionConfirmed] = useState(false);
+  // This is added only for tracking purposes
+  // With this prop we will track down if the user has had an intent of filling the formulary
+  const [isFormDirty, setIsFormDirty] = useState(false);
+
+  const context = useContext(Context);
+  const location = useLocation();
+  const history = useHistory();
+
+  useEffect(() => {
+    if (!isFormDirty) {
+      return;
+    }
+
+    window.analytics?.track("provision_form-dirty", {
+      provider: "gcp",
+    });
+  }, [isFormDirty]);
+
+  useEffect(() => {
+    if (props.infras) {
+      // From the dashboard, only uncheck and disable if "creating" or "created"
+      let filtered = selectedInfras;
+      props.infras.forEach((infra: InfraType, i: number) => {
+        let { kind, status } = infra;
+        if (status === "creating" || status === "created") {
+          filtered = filtered.filter((item: any) => {
+            return item.value !== kind;
+          });
+        }
+      });
+      setSelectedInfras(filtered);
+    }
+  }, [props.infras]);
+
+  useEffect(() => {
+    setClusterNameIfNotSet();
+  }, [props.projectName]);
+
+  const setClusterNameIfNotSet = () => {
+    let projectName = props.projectName || context.currentProject?.name;
+
+    if (!clusterNameSet && !clusterName.includes(`${projectName}-cluster`)) {
+      setClusterName(
+        `${projectName}-cluster-${Math.random().toString(36).substring(2, 8)}`
+      );
+    }
+  };
+
+  const checkFormDisabled = () => {
+    if (!provisionConfirmed) {
+      return true;
+    }
+
+    let { projectName } = props;
+    if (projectName || projectName === "") {
+      return (
+        !isAlphanumeric(projectName) ||
+        !(
+          gcpProjectId !== "" &&
+          gcpKeyData !== "" &&
+          gcpRegion !== "" &&
+          clusterName !== ""
+        ) ||
+        selectedInfras.length === 0
+      );
+    } else {
+      return (
+        !(
+          gcpProjectId !== "" &&
+          gcpKeyData !== "" &&
+          gcpRegion !== "" &&
+          clusterName !== ""
+        ) || selectedInfras.length === 0
+      );
+    }
+  };
+
+  const catchError = (err: any) => {
+    console.log(err);
+    props.handleError();
+  };
+
+  // Step 1: Create a project
+  const createProject = (callback?: any) => {
+    let { projectName } = props;
+    let { user, setProjects, setCurrentProject } = context;
+
+    api
+      .createProject("<token>", { name: projectName }, {})
+      .then((res) => {
+        let proj = res.data;
+
+        // Need to set project list for dropdown
+        // TODO: consolidate into ProjectSection (case on exists in list on set)
+        api
+          .getProjects(
+            "<token>",
+            {},
+            {
+              id: user.userId,
+            }
+          )
+          .then((res) => {
+            setProjects(res.data);
+            setCurrentProject(proj, () => callback && callback());
+          })
+          .catch(catchError);
+      })
+      .catch(catchError);
+  };
+
+  const provisionGCR = (id: number, callback?: any) => {
+    console.log("Provisioning GCR");
+    let { currentProject } = context;
+
+    return api
+      .createGCR(
+        "<token>",
+        {
+          gcp_integration_id: id,
+        },
+        { project_id: currentProject.id }
+      )
+      .catch(catchError);
+  };
+
+  const provisionGKE = (id: number) => {
+    console.log("Provisioning GKE");
+    let { currentProject } = context;
+
+    api
+      .createGKE(
+        "<token>",
+        {
+          gke_name: clusterName,
+          gcp_integration_id: id,
+        },
+        { project_id: currentProject.id }
+      )
+      .then((res) =>
+        pushFiltered({ history, location }, "/dashboard", ["project_id"], {
+          tab: "provisioner",
+        })
+      )
+      .catch(catchError);
+  };
+
+  const handleCreateFlow = () => {
+    let { currentProject } = context;
+    api
+      .createGCPIntegration(
+        "<token>",
+        {
+          gcp_region: gcpRegion,
+          gcp_key_data: gcpKeyData,
+          gcp_project_id: gcpProjectId,
+        },
+        { project_id: currentProject.id }
+      )
+      .then((res) => {
+        if (res?.data) {
+          let { id } = res.data;
+
+          if (selectedInfras.length === 2) {
+            // Case: project exists, provision GCR + GKE
+            provisionGCR(id).then(() => provisionGKE(id));
+          } else if (selectedInfras[0].value === "gcr") {
+            // Case: project exists, only provision GCR
+            provisionGCR(id).then(() =>
+              pushFiltered(
+                { location, history },
+                "/dashboard",
+                ["project_id"],
+                {
+                  tab: "provisioner",
+                }
+              )
+            );
+          } else {
+            // Case: project exists, only provision GKE
+            provisionGKE(id);
+          }
+        }
+      })
+      .catch(console.log);
+  };
+
+  const onCreateGCP = () => {
+    props?.trackOnSave();
+    setButtonStatus("loading");
+    let { projectName } = props;
+
+    if (!projectName) {
+      handleCreateFlow();
+    } else {
+      createProject(handleCreateFlow);
+    }
+  };
+
+  const getButtonStatus = () => {
+    if (props.projectName) {
+      if (!isAlphanumeric(props.projectName)) {
+        return "Project name contains illegal characters";
+      }
+    }
+    if (
+      !gcpProjectId ||
+      !gcpKeyData ||
+      !provisionConfirmed ||
+      !clusterName ||
+      props.projectName === ""
+    ) {
+      return "Required fields missing";
+    }
+    return buttonStatus;
+  };
+
+  const renderClusterNameSection = () => {
+    if (
+      selectedInfras.length == 2 ||
+      (selectedInfras.length == 1 && selectedInfras[0].value === "gke")
+    ) {
+      return (
+        <InputRow
+          type="text"
+          value={clusterName}
+          setValue={(x: string) => {
+            setIsFormDirty(true);
+            setClusterName(x);
+            setClusterNameSet(true);
+          }}
+          label="Cluster Name"
+          placeholder="ex: porter-cluster"
+          width="100%"
+          isRequired={true}
+        />
+      );
+    }
+  };
+
+  const goToGuide = () => {
+    window?.analytics?.track("provision_go-to-guide", {
+      hosting: "gcp",
+    });
+
+    window.open("https://docs.getporter.dev/docs/getting-started-on-gcp");
+  };
+
+  return (
+    <StyledGCPFormSection>
+      <FormSection>
+        <CloseButton onClick={() => props.setSelectedProvisioner(null)}>
+          <CloseButtonImg src={close} />
+        </CloseButton>
+        <Heading isAtTop={true}>
+          GCP Credentials
+          <GuideButton onClick={() => goToGuide()}>
+            <i className="material-icons-outlined">help</i>
+            Guide
+          </GuideButton>
+        </Heading>
+        <SelectRow
+          options={regionOptions}
+          width="100%"
+          value={gcpRegion}
+          dropdownMaxHeight="240px"
+          setActiveValue={(x: string) => {
+            setIsFormDirty(true);
+            setGcpRegion(x);
+          }}
+          label="📍 GCP Region"
+        />
+        <InputRow
+          type="text"
+          value={gcpProjectId}
+          setValue={(x: string) => {
+            setIsFormDirty(true);
+            setGcpProjectId(x);
+          }}
+          label="🏷️ GCP Project ID"
+          placeholder="ex: blindfold-ceiling-24601"
+          width="100%"
+          isRequired={true}
+        />
+        <UploadArea
+          setValue={(x: any) => {
+            setIsFormDirty(true);
+            setGcpKeyData(x);
+          }}
+          label="🔒 GCP Key Data (JSON)"
+          placeholder="Choose a file or drag it here."
+          width="100%"
+          height="100%"
+          isRequired={true}
+        />
+
+        <Br />
+        <Heading>GCP Resources</Heading>
+        <Helper>
+          Porter will provision the following GCP resources in your own cloud.
+        </Helper>
+        <CheckboxList
+          options={provisionOptions}
+          selected={selectedInfras}
+          setSelected={(x: { value: string; label: string }[]) => {
+            setIsFormDirty(true);
+            setSelectedInfras(x);
+          }}
+        />
+        {renderClusterNameSection()}
+        <Helper>
+          By default, Porter creates a cluster with three custom-2-4096
+          instances (2 CPU, 4 GB RAM each). Google Cloud will bill you for any
+          provisioned resources. Learn more about GKE pricing
+          <Highlight
+            href="https://cloud.google.com/kubernetes-engine/pricing"
+            target="_blank"
+          >
+            here
+          </Highlight>
+          .
+        </Helper>
+        {/*
+        <Helper>
+          Estimated Cost:{" "}
+          <CostHighlight highlight={this.props.highlightCosts}>
+            $250/Month
+          </CostHighlight>
+          <Tooltip
+            title={
+              <div
+                style={{
+                  fontFamily: "Work Sans, sans-serif",
+                  fontSize: "12px",
+                  fontWeight: "normal",
+                  padding: "5px 6px",
+                }}
+              >
+                GKE cost: ~$70/month <br />
+                Machine (x3) cost: ~$150/month <br />
+                Networking cost: ~$30/month
+              </div>
+            }
+            placement="top"
+          >
+            <StyledInfoTooltip>
+              <i className="material-icons">help_outline</i>
+            </StyledInfoTooltip>
+          </Tooltip>
+        </Helper>
+        */}
+        <CheckboxRow
+          isRequired={true}
+          checked={provisionConfirmed}
+          toggle={() => {
+            setIsFormDirty(true);
+            setProvisionConfirmed(!provisionConfirmed);
+          }}
+          label="I understand and wish to proceed"
+        />
+      </FormSection>
+      {props.children ? props.children : <Padding />}
+      <SaveButton
+        text="Submit"
+        disabled={checkFormDisabled() || buttonStatus === "loading"}
+        onClick={onCreateGCP}
+        makeFlush={true}
+        status={getButtonStatus()}
+        helper="Note: Provisioning can take up to 15 minutes"
+      />
+    </StyledGCPFormSection>
+  );
+};
+
 const Highlight = styled.a`
 const Highlight = styled.a`
   color: #8590ff;
   color: #8590ff;
   cursor: pointer;
   cursor: pointer;