jnfrati 4 лет назад
Родитель
Сommit
597eaafa32
1 измененных файлов с 266 добавлено и 344 удалено
  1. 266 344
      dashboard/src/main/home/provisioner/AWSFormSection.tsx

+ 266 - 344
dashboard/src/main/home/provisioner/AWSFormSection.tsx

@@ -1,4 +1,4 @@
-import React, { Component } from "react";
+import React, { Component, useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 
 import close from "assets/close.png";
@@ -15,10 +15,9 @@ import Helper from "components/form-components/Helper";
 import Heading from "components/form-components/Heading";
 import SaveButton from "components/SaveButton";
 import CheckboxList from "components/form-components/CheckboxList";
-import { RouteComponentProps, withRouter } from "react-router";
-import Tooltip from "@material-ui/core/Tooltip";
+import { useHistory, useLocation } from "react-router";
 
-type PropsType = RouteComponentProps & {
+type PropsType = {
   setSelectedProvisioner: (x: string | null) => void;
   handleError: () => void;
   projectName: string;
@@ -27,18 +26,6 @@ type PropsType = RouteComponentProps & {
   trackOnSave: () => void;
 };
 
-type StateType = {
-  awsRegion: string;
-  awsMachineType: string;
-  awsAccessId: string;
-  awsSecretKey: string;
-  clusterName: string;
-  clusterNameSet: boolean;
-  selectedInfras: { value: string; label: string }[];
-  buttonStatus: string;
-  provisionConfirmed: boolean;
-};
-
 const provisionOptions = [
   { value: "ecr", label: "Elastic Container Registry (ECR)" },
   { value: "eks", label: "Elastic Kubernetes Service (EKS)" },
@@ -85,29 +72,30 @@ const costMapping: Record<string, number> = {
   "t3.2xlarge": 240,
 };
 
-// TODO: Consolidate across forms w/ HOC
-class AWSFormSection extends Component<PropsType, StateType> {
-  state = {
-    awsRegion: "us-east-1",
-    awsMachineType: "t2.medium",
-    awsAccessId: "",
-    awsSecretKey: "",
-    clusterName: "",
-    clusterNameSet: false,
-    selectedInfras: [...provisionOptions],
-    buttonStatus: "",
-    provisionConfirmed: false,
-  };
-
-  componentDidMount = () => {
-    let { infras } = this.props;
-    let { selectedInfras } = this.state;
-    this.setClusterNameIfNotSet();
-
-    if (infras) {
+const AWSFormSectionFC: React.FC<PropsType> = (props) => {
+  const [awsRegion, setAwsRegion] = useState("us-east-1");
+  const [awsMachineType, setAwsMachineType] = useState("t2.medium");
+  const [awsAccessId, setAwsAccessId] = useState("");
+  const [awsSecretKey, setAwsSecretKey] = 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 (props.infras) {
       // From the dashboard, only uncheck and disable if "creating" or "created"
       let filtered = selectedInfras;
-      infras.forEach((infra: InfraType, i: number) => {
+      props.infras.forEach((infra: InfraType, i: number) => {
         let { kind, status } = infra;
         if (status === "creating" || status === "created") {
           filtered = filtered.filter((item: any) => {
@@ -115,45 +103,30 @@ class AWSFormSection extends Component<PropsType, StateType> {
           });
         }
       });
-      this.setState({ selectedInfras: filtered });
+      setSelectedInfras(filtered);
     }
-  };
+  }, [props.infras]);
 
-  componentDidUpdate = (prevProps: PropsType, prevState: StateType) => {
-    if (prevProps.projectName != this.props.projectName) {
-      this.setClusterNameIfNotSet();
-    }
-  };
+  useEffect(() => {
+    setClusterNameIfNotSet();
+  }, [props.projectName]);
 
-  setClusterNameIfNotSet = () => {
-    let projectName =
-      this.props.projectName || this.context.currentProject?.name;
+  const setClusterNameIfNotSet = () => {
+    let projectName = props.projectName || context.currentProject?.name;
 
-    if (
-      !this.state.clusterNameSet &&
-      !this.state.clusterName.includes(`${projectName}-cluster`)
-    ) {
-      this.setState({
-        clusterName: `${projectName}-cluster-${Math.random()
-          .toString(36)
-          .substring(2, 8)}`,
-      });
+    if (!clusterNameSet && !clusterName.includes(`${projectName}-cluster`)) {
+      setClusterName(
+        `${projectName}-cluster-${Math.random().toString(36).substring(2, 8)}`
+      );
     }
   };
 
-  checkFormDisabled = () => {
-    if (!this.state.provisionConfirmed) {
+  const checkFormDisabled = () => {
+    if (!provisionConfirmed) {
       return true;
     }
 
-    let {
-      awsRegion,
-      awsAccessId,
-      awsSecretKey,
-      selectedInfras,
-      clusterName,
-    } = this.state;
-    let { projectName } = this.props;
+    const { projectName } = props;
     if (projectName || projectName === "") {
       return (
         !isAlphanumeric(projectName) ||
@@ -177,173 +150,144 @@ class AWSFormSection extends Component<PropsType, StateType> {
     }
   };
 
-  catchError = (err: any) => {
+  const catchError = (err: any) => {
     console.log(err);
-    this.props.handleError();
+    props.handleError();
   };
 
   // Step 1: Create a project
   // TODO: promisify this function
-  createProject = (callback?: any) => {
-    let { projectName } = this.props;
-    let { user, setProjects, setCurrentProject } = this.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(this.catchError);
-      })
-      .catch(this.catchError);
+  const createProject = async () => {
+    const { projectName } = props;
+    const { user, setProjects, setCurrentProject } = context;
+    try {
+      const project = await api
+        .createProject("<token>", { name: projectName }, {})
+        .then((res) => res.data);
+
+      // Need to set project list for dropdown
+      // TODO: consolidate into ProjectSection (case on exists in list on set)
+      const projectList = await api
+        .getProjects(
+          "<token>",
+          {},
+          {
+            id: user.userId,
+          }
+        )
+        .then((res) => res.data);
+      setProjects(projectList);
+      setCurrentProject(project);
+    } catch (error) {
+      catchError(error);
+    }
   };
 
-  provisionECR = () => {
-    console.log("Provisioning ECR");
-    let { awsAccessId, awsSecretKey, awsRegion } = this.state;
-    let { currentProject } = this.context;
-
-    return api
-      .createAWSIntegration(
+  const getAwsIntegrationId = async () => {
+    const { currentProject } = context;
+    try {
+      const res = await api.createAWSIntegration(
         "<token>",
         {
           aws_region: awsRegion,
           aws_access_key_id: awsAccessId,
           aws_secret_access_key: awsSecretKey,
+          aws_cluster_id: clusterName,
         },
         { id: currentProject.id }
-      )
-      .then((res) =>
-        api.provisionECR(
-          "<token>",
-          {
-            aws_integration_id: res.data.id,
-            ecr_name: `${currentProject.name}-registry`,
-          },
-          { id: currentProject.id }
-        )
-      )
-      .catch(this.catchError);
+      );
+      return res.data;
+    } catch (error) {
+      catchError(error);
+    }
   };
 
-  provisionEKS = () => {
-    let {
-      awsAccessId,
-      awsSecretKey,
-      awsRegion,
-      awsMachineType,
-      clusterName,
-    } = this.state;
-    let { currentProject } = this.context;
-
-    api
-      .createAWSIntegration(
+  const provisionECR = async (awsIntegrationId: string) => {
+    console.log("Started provision ECR");
+    const { currentProject } = context;
+    try {
+      await api.provisionECR(
         "<token>",
         {
-          aws_region: awsRegion,
-          aws_access_key_id: awsAccessId,
-          aws_secret_access_key: awsSecretKey,
-          aws_cluster_id: clusterName,
+          aws_integration_id: awsIntegrationId,
+          ecr_name: `${currentProject.name}-registry`,
         },
         { id: currentProject.id }
-      )
-      .then((res) =>
-        api.provisionEKS(
-          "<token>",
-          {
-            aws_integration_id: res.data.id,
-            eks_name: clusterName,
-            machine_type: awsMachineType,
-          },
-          { id: currentProject.id }
-        )
-      )
-      .then(() =>
-        pushFiltered(this.props, "/dashboard", ["project_id"], {
-          tab: "provisioner",
-        })
-      )
-      .catch(this.catchError);
+      );
+    } catch (error) {
+      catchError(error);
+    }
+  };
+
+  const provisionEKS = async (awsIntegrationId: string) => {
+    const { currentProject } = context;
+    try {
+      await api.provisionEKS(
+        "<token>",
+        {
+          aws_integration_id: awsIntegrationId,
+          eks_name: clusterName,
+          machine_type: awsMachineType,
+        },
+        { id: currentProject.id }
+      );
+    } catch (error) {
+      catchError(error);
+    }
   };
 
   // TODO: handle generically (with > 2 steps)
-  onCreateAWS = () => {
-    this.props?.trackOnSave();
-    this.setState({ buttonStatus: "loading" });
-    let { projectName } = this.props;
-    let { selectedInfras } = this.state;
-
-    if (!projectName) {
-      if (selectedInfras.length === 2) {
-        // Case: project exists, provision ECR + EKS
-        this.provisionECR().then(this.provisionEKS);
-      } else if (selectedInfras[0].value === "ecr") {
-        // Case: project exists, only provision ECR
-        this.provisionECR().then(() =>
-          pushFiltered(this.props, "/dashboard", ["project_id"], {
-            tab: "provisioner",
-          })
-        );
-      } else {
-        // Case: project exists, only provision EKS
-        this.provisionEKS();
-      }
-    } else {
-      if (selectedInfras.length === 2) {
-        // Case: project DNE, provision ECR + EKS
-        this.createProject(() => this.provisionECR().then(this.provisionEKS));
-      } else if (selectedInfras[0].value === "ecr") {
-        // Case: project DNE, only provision ECR
-        this.createProject(() =>
-          this.provisionECR().then(() =>
-            pushFiltered(this.props, "/dashboard", ["project_id"], {
-              tab: "provisioner",
-            })
-          )
-        );
-      } else {
-        // Case: project DNE, only provision EKS
-        this.createProject(this.provisionEKS);
-      }
+  const onCreateAWS = async () => {
+    // Track to segment the intent of provision cluster
+    props?.trackOnSave();
+    setButtonStatus("loading");
+    const { projectName } = props;
+
+    if (projectName) {
+      await createProject();
     }
+
+    const awsIntegrationId = await getAwsIntegrationId();
+
+    const filterNonAWSInfras = (infra: any) =>
+      ["ecr", "eks"].includes(infra.value);
+
+    const infraCreationRequests = selectedInfras
+      // Check that we don't include any other key into the infra creation than ecr and eks
+      .filter(filterNonAWSInfras)
+      .map((infra) => {
+        if (infra.value === "ecr") {
+          return provisionECR(awsIntegrationId);
+        }
+        return provisionEKS(awsIntegrationId);
+      });
+    // Wait for all promises to be completed (could be just one)
+    await Promise.all(infraCreationRequests);
+
+    pushFiltered({ history, location }, "/dashboard", ["project_id"], {
+      tab: "provisioner",
+    });
   };
 
-  getButtonStatus = () => {
-    if (this.props.projectName) {
-      if (!isAlphanumeric(this.props.projectName)) {
+  const getButtonStatus = () => {
+    if (props.projectName) {
+      if (!isAlphanumeric(props.projectName)) {
         return "Project name contains illegal characters";
       }
     }
     if (
-      !this.state.awsAccessId ||
-      !this.state.awsSecretKey ||
-      !this.state.provisionConfirmed ||
-      !this.state.clusterName ||
-      this.props.projectName === ""
+      !awsAccessId ||
+      !awsSecretKey ||
+      !provisionConfirmed ||
+      !clusterName ||
+      props.projectName === ""
     ) {
       return "Required fields missing";
     }
-    return this.state.buttonStatus;
+    return buttonStatus;
   };
 
-  renderClusterNameSection = () => {
-    let { selectedInfras, clusterName } = this.state;
-
+  const renderClusterNameSection = () => {
     if (
       selectedInfras.length == 2 ||
       (selectedInfras.length == 1 && selectedInfras[0].value === "eks")
@@ -352,9 +296,10 @@ class AWSFormSection extends Component<PropsType, StateType> {
         <InputRow
           type="text"
           value={clusterName}
-          setValue={(x: string) =>
-            this.setState({ clusterName: x, clusterNameSet: true })
-          }
+          setValue={(x: string) => {
+            setClusterName(x);
+            setClusterNameSet(true);
+          }}
           label="Cluster Name"
           placeholder="ex: porter-cluster"
           width="100%"
@@ -364,7 +309,7 @@ class AWSFormSection extends Component<PropsType, StateType> {
     }
   };
 
-  goToGuide = () => {
+  const goToGuide = () => {
     window?.analytics?.track("provision_go-to-guide", {
       hosting: "aws",
     });
@@ -373,151 +318,128 @@ class AWSFormSection extends Component<PropsType, StateType> {
       "https://docs.getporter.dev/docs/getting-started-with-porter-on-aws"
     );
   };
-
-  render() {
-    let { setSelectedProvisioner } = this.props;
-    let {
-      awsRegion,
-      awsMachineType,
-      awsAccessId,
-      awsSecretKey,
-      selectedInfras,
-    } = this.state;
-
-    return (
-      <StyledAWSFormSection>
-        <FormSection>
-          <CloseButton onClick={() => setSelectedProvisioner(null)}>
-            <CloseButtonImg src={close} />
-          </CloseButton>
-          <Heading isAtTop={true}>
-            AWS Credentials
-            <GuideButton onClick={() => this.goToGuide()}>
-              <i className="material-icons-outlined">help</i>
-              Guide
-            </GuideButton>
-          </Heading>
-          <SelectRow
-            options={regionOptions}
-            width="100%"
-            value={awsRegion}
-            dropdownMaxHeight="240px"
-            setActiveValue={(x: string) => this.setState({ awsRegion: x })}
-            label="📍 AWS Region"
-          />
-          <SelectRow
-            options={machineTypeOptions}
-            width="100%"
-            value={awsMachineType}
-            dropdownMaxHeight="240px"
-            setActiveValue={(x: string) => this.setState({ awsMachineType: x })}
-            label="⚙️ AWS Machine Type"
-          />
-          {/*
-          <Helper>
-            Estimated Cost:{" "}
-            <CostHighlight highlight={this.props.highlightCosts}>
-              {`\$${
-                70 + 3 * costMapping[this.state.awsMachineType] + 30
-              }/Month`}
-            </CostHighlight>
-            <Tooltip
-              title={
-                <div
-                  style={{
-                    fontFamily: "Work Sans, sans-serif",
-                    fontSize: "12px",
-                    fontWeight: "normal",
-                    padding: "5px 6px",
-                  }}
-                >
-                  EKS cost: ~$70/month <br />
-                  Machine (x3) cost: ~$
-                  {`${3 * costMapping[this.state.awsMachineType]}`}/month <br />
-                  Networking cost: ~$30/month
-                </div>
-              }
-              placement="top"
-            >
-              <StyledInfoTooltip>
-                <i className="material-icons">help_outline</i>
-              </StyledInfoTooltip>
-            </Tooltip>
-          </Helper>
-          */}
-          <InputRow
-            type="text"
-            value={awsAccessId}
-            setValue={(x: string) => this.setState({ awsAccessId: x })}
-            label="👤 AWS Access ID"
-            placeholder="ex: AKIAIOSFODNN7EXAMPLE"
-            width="100%"
-            isRequired={true}
-          />
-          <InputRow
-            type="password"
-            value={awsSecretKey}
-            setValue={(x: string) => this.setState({ awsSecretKey: x })}
-            label="🔒 AWS Secret Key"
-            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
-            width="100%"
-            isRequired={true}
-          />
-          <Br />
-          <Heading>AWS Resources</Heading>
-          <Helper>
-            Porter will provision the following AWS resources in your own cloud.
-          </Helper>
-          <CheckboxList
-            options={provisionOptions}
-            selected={selectedInfras}
-            setSelected={(x: { value: string; label: string }[]) => {
-              this.setState({ selectedInfras: x });
-            }}
-          />
-          {this.renderClusterNameSection()}
-          <Helper>
-            By default, Porter creates a cluster with three t2.medium instances
-            (2vCPUs and 4GB RAM each). AWS will bill you for any provisioned
-            resources. Learn more about EKS pricing
-            <Highlight
-              href="https://aws.amazon.com/eks/pricing/"
-              target="_blank"
-            >
-              here
-            </Highlight>
-            .
-          </Helper>
-          <CheckboxRow
-            isRequired={true}
-            checked={this.state.provisionConfirmed}
-            toggle={() =>
-              this.setState({
-                provisionConfirmed: !this.state.provisionConfirmed,
-              })
+  return (
+    <StyledAWSFormSection>
+      <FormSection>
+        <CloseButton onClick={() => props.setSelectedProvisioner(null)}>
+          <CloseButtonImg src={close} />
+        </CloseButton>
+        <Heading isAtTop={true}>
+          AWS Credentials
+          <GuideButton onClick={() => goToGuide()}>
+            <i className="material-icons-outlined">help</i>
+            Guide
+          </GuideButton>
+        </Heading>
+        <SelectRow
+          options={regionOptions}
+          width="100%"
+          value={awsRegion}
+          dropdownMaxHeight="240px"
+          setActiveValue={(x: string) => setAwsRegion(x)}
+          label="📍 AWS Region"
+        />
+        <SelectRow
+          options={machineTypeOptions}
+          width="100%"
+          value={awsMachineType}
+          dropdownMaxHeight="240px"
+          setActiveValue={(x: string) => setAwsMachineType(x)}
+          label="⚙️ AWS Machine Type"
+        />
+        {/*
+        <Helper>
+          Estimated Cost:{" "}
+          <CostHighlight highlight={this.props.highlightCosts}>
+            {`\$${
+              70 + 3 * costMapping[this.state.awsMachineType] + 30
+            }/Month`}
+          </CostHighlight>
+          <Tooltip
+            title={
+              <div
+                style={{
+                  fontFamily: "Work Sans, sans-serif",
+                  fontSize: "12px",
+                  fontWeight: "normal",
+                  padding: "5px 6px",
+                }}
+              >
+                EKS cost: ~$70/month <br />
+                Machine (x3) cost: ~$
+                {`${3 * costMapping[this.state.awsMachineType]}`}/month <br />
+                Networking cost: ~$30/month
+              </div>
             }
-            label="I understand and wish to proceed"
-          />
-        </FormSection>
-        {this.props.children ? this.props.children : <Padding />}
-        <SaveButton
-          text="Submit"
-          disabled={
-            this.checkFormDisabled() || this.state.buttonStatus === "loading"
-          }
-          onClick={this.onCreateAWS}
-          makeFlush={true}
-          status={this.getButtonStatus()}
-          helper="Note: Provisioning can take up to 15 minutes"
+            placement="top"
+          >
+            <StyledInfoTooltip>
+              <i className="material-icons">help_outline</i>
+            </StyledInfoTooltip>
+          </Tooltip>
+        </Helper>
+        */}
+        <InputRow
+          type="text"
+          value={awsAccessId}
+          setValue={(x: string) => setAwsAccessId(x)}
+          label="👤 AWS Access ID"
+          placeholder="ex: AKIAIOSFODNN7EXAMPLE"
+          width="100%"
+          isRequired={true}
         />
-      </StyledAWSFormSection>
-    );
-  }
-}
-
-AWSFormSection.contextType = Context;
+        <InputRow
+          type="password"
+          value={awsSecretKey}
+          setValue={(x: string) => setAwsSecretKey(x)}
+          label="🔒 AWS Secret Key"
+          placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+          width="100%"
+          isRequired={true}
+        />
+        <Br />
+        <Heading>AWS Resources</Heading>
+        <Helper>
+          Porter will provision the following AWS resources in your own cloud.
+        </Helper>
+        <CheckboxList
+          options={provisionOptions}
+          selected={selectedInfras}
+          setSelected={(x: { value: string; label: string }[]) => {
+            setSelectedInfras(x);
+          }}
+        />
+        {renderClusterNameSection()}
+        <Helper>
+          By default, Porter creates a cluster with three t2.medium instances
+          (2vCPUs and 4GB RAM each). AWS will bill you for any provisioned
+          resources. Learn more about EKS pricing
+          <Highlight href="https://aws.amazon.com/eks/pricing/" target="_blank">
+            here
+          </Highlight>
+          .
+        </Helper>
+        <CheckboxRow
+          isRequired={true}
+          checked={provisionConfirmed}
+          toggle={() => setProvisionConfirmed(!provisionConfirmed)}
+          label="I understand and wish to proceed"
+        />
+      </FormSection>
+      {props.children ? props.children : <Padding />}
+      <SaveButton
+        text="Submit"
+        disabled={checkFormDisabled() || buttonStatus === "loading"}
+        onClick={onCreateAWS}
+        makeFlush={true}
+        status={getButtonStatus()}
+        helper="Note: Provisioning can take up to 15 minutes"
+      />
+    </StyledAWSFormSection>
+  );
+};
 
-export default withRouter(AWSFormSection);
+export default AWSFormSectionFC;
 
 const Highlight = styled.a`
   color: #8590ff;