Browse Source

redesign cloudformation aws input flow (#3248)

* redesign cloudformation aws input flow

* add comment
Feroze Mohideen 2 years ago
parent
commit
98a0038e3e

+ 13 - 1
api/server/handlers/user/update_onboarding_step.go

@@ -66,8 +66,20 @@ func (v *UpdateOnboardingStepHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		}))
 	}
 
+	if request.Step == "aws-login-redirect-success" {
+		v.Config().AnalyticsClient.Track(analytics.AWSLoginRedirectSuccess(&analytics.AWSRedirectOpts{
+			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
+			Email:               user.Email,
+			FirstName:           user.FirstName,
+			LastName:            user.LastName,
+			CompanyName:         user.CompanyName,
+			AccountId:           request.AccountId,
+			LoginURL:            request.LoginURL,
+		}))
+	}
+
 	if request.Step == "aws-cloudformation-redirect-success" {
-		v.Config().AnalyticsClient.Track(analytics.AWSCloudformationRedirectSuccess(&analytics.AWSCloudFormationRedirectOpts{
+		v.Config().AnalyticsClient.Track(analytics.AWSCloudformationRedirectSuccess(&analytics.AWSRedirectOpts{
 			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
 			Email:               user.Email,
 			FirstName:           user.FirstName,

+ 1 - 0
api/types/user.go

@@ -87,4 +87,5 @@ type UpdateOnboardingStepRequest struct {
 	AccountId         string `json:"account_id"`
 	CloudformationURL string `json:"cloudformation_url"`
 	ErrorMessage      string `json:"error_message"`
+	LoginURL          string `json:"login_url"`
 }

+ 193 - 119
dashboard/src/components/CloudFormationForm.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState, useContext, useMemo } from "react";
+import React, { useState, useContext } from "react";
 import styled from "styled-components";
 import { v4 as uuidv4 } from 'uuid';
 
@@ -9,16 +9,13 @@ import { Context } from "shared/Context";
 
 import Text from "./porter/Text";
 import Spacer from "./porter/Spacer";
-import InputRow from "./form-components/InputRow";
-import SaveButton from "./SaveButton";
-import Fieldset from "./porter/Fieldset";
 import Input from "./porter/Input";
 import Button from "./porter/Button";
-import DocsHelper from "./DocsHelper";
 import Error from "./porter/Error";
 import Step from "./porter/Step";
 import Link from "./porter/Link";
 import Container from "./porter/Container";
+import VerticalSteps from "./porter/VerticalSteps";
 
 type Props = {
   goBack: () => void;
@@ -36,6 +33,9 @@ const CloudFormationForm: React.FC<Props> = ({
   const [roleStatus, setRoleStatus] = useState("");
   const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
   const [AWSAccountID, setAWSAccountID] = useState("");
+  const [AWSAccountIDInputError, setAWSAccountIDInputError] = useState<string | undefined>(undefined);
+  const [currentStep, setCurrentStep] = useState<number>(0);
+
   const { currentProject } = useContext(Context);
   const markStepStarted = async (
     {
@@ -43,21 +43,57 @@ const CloudFormationForm: React.FC<Props> = ({
       account_id = "",
       cloudformation_url = "",
       error_message = "",
+      login_url = "",
     }:
       {
         step: string;
         account_id?: string
         cloudformation_url?: string
         error_message?: string
+        login_url?: string
       }
   ) => {
     try {
-      await api.updateOnboardingStep("<token>", { step, account_id, cloudformation_url, error_message }, {});
+      await api.updateOnboardingStep("<token>", { step, account_id, cloudformation_url, error_message, login_url }, {});
     } catch (err) {
       // console.log(err);
     }
   };
 
+  const getAccountIdInputError = (accountId: string) => {
+    const regex = /^\d{12}$/;
+    if (accountId === "") {
+      return undefined;
+    } else if (!regex.test(accountId)) {
+      return 'A valid AWS Account ID must be a 12-digit number.';
+    }
+    return undefined;
+  };
+
+  const handleAWSAccountIDChange = (accountId: string) => {
+    setAWSAccountID(accountId);
+    if (accountId === "open-sesame") {
+      switchToCredentialFlow();
+    }
+    // handle case where user resets the input to empty
+    if (accountId.trim().length === 0) {
+      setCurrentStep(0);
+      setAWSAccountIDInputError(undefined);
+      return;
+    }
+    const accountIdInputError = getAccountIdInputError(accountId);
+    if (accountIdInputError == null) {
+      setCurrentStep(1);
+      if (!hasSentAWSNotif) {
+        setHasSentAWSNotif(true);
+        markStepStarted({ step: "aws-account-id-complete", account_id: accountId });
+      }
+    } else {
+      setCurrentStep(0);
+    }
+    setAWSAccountIDInputError(accountIdInputError);
+  };
+
   const getExternalId = () => {
     let externalId = localStorage.getItem(AWSAccountID)
     if (!externalId) {
@@ -105,130 +141,166 @@ const CloudFormationForm: React.FC<Props> = ({
     }
   };
 
-  const directToCloudFormation = () => {
+  const directToAWSLoginAndProceedStep = () => {
+    const login_url = `https://${AWSAccountID}.signin.aws.amazon.com/console`;
+    markStepStarted({ step: "aws-login-redirect-success", account_id: AWSAccountID, login_url })
+    setCurrentStep(2);
+    window.open(login_url, "_blank")
+  }
+
+  const directToCloudFormationAndProceedStep = () => {
     let externalId = getExternalId();
     let trustArn = process.env.TRUST_ARN ? process.env.TRUST_ARN : "arn:aws:iam::108458755588:role/CAPIManagement";
     const cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-policy.json&stackName=PorterRole&param_ExternalIdParameter=${externalId}&param_TrustArnParameter=${trustArn}`
     markStepStarted({ step: "aws-cloudformation-redirect-success", account_id: AWSAccountID, cloudformation_url })
+    setCurrentStep(3);
     window.open(cloudformation_url, "_blank")
   }
 
   const renderContent = () => {
     return (
       <>
+        <Text>Grant Porter permissions to create infrastructure in your AWS account by following 4 simple steps.</Text>
         <Spacer y={1} />
-        <Fieldset>
-          <Text size={16}>
-            Log in to AWS and "Create stack"
-          </Text>
-          <Spacer height="15px" />
-          <Text color="helper">
-            Provide your AWS account ID to log in and grant Porter access to AWS by clicking 'Grant permissions' below. You will need to select "Create stack" after being redirected to the AWS console.
-          </Text>
-          <Spacer y={1} />
-          <Input
-            label={
-              <Flex>
-                👤 AWS account ID
-                <i
-                  className="material-icons"
-                  onClick={() => {
-                    window.open("https://console.aws.amazon.com/billing/home?region=us-east-1#/account", "_blank")
-                  }}
-                >
-                  help_outline
-                </i>
-              </Flex>
-            }
-            value={AWSAccountID}
-            setValue={(e) => {
-              if (e === "open-sesame") {
-                switchToCredentialFlow();
-              }
-              if (e.trim().length === 12 && !hasSentAWSNotif) {
-                setHasSentAWSNotif(true);
-                markStepStarted({ step: "aws-account-id-complete", account_id: e.trim() });
-              }
-              setGrantPermissionsError("");
-              setAWSAccountID(e.trim());
-            }}
-            placeholder="ex: 915037676314"
-          />
-          <Spacer y={1} />
-          <Button
-            onClick={() => {
-              if (AWSAccountID.length === 12 && !isNaN(Number(AWSAccountID))) {
-                directToCloudFormation();
-              } else {
-                setGrantPermissionsError("Invalid AWS account ID");
-              }
-            }}
-            status={
-              grantPermissionsError && (
-                <Error message={grantPermissionsError} />
-              )
-            }
-            color="#1E2631"
-            withBorder
-          >
-            <ButtonImg src={aws} /> Grant permissions
-          </Button>
-          <Spacer y={1} />
-          <Text color="helper">
-            Make sure that the stack status has changed from "CREATE_IN_PROGRESS" to "CREATE_COMPLETE" before clicking Continue below.
-          </Text>
-        </Fieldset>
-        <Spacer y={1} />
-        <Button
-          onClick={() => {
-            checkIfRoleExists()
-          }}
-          status={
-            errorMessage ? (
-              <Error
-                message={errorMessage}
-                ctaText="Troubleshooting steps"
-                errorModalContents={
+        <VerticalSteps
+          currentStep={currentStep}
+          steps={
+            [
+              <>
+                <Text size={16}>1. Provide your AWS Account ID.</Text>
+                <Spacer y={0.5} />
+                <Input
+                  label={
+                    <Flex>
+                      👤 AWS account ID
+                      <i
+                        className="material-icons"
+                        onClick={() => {
+                          window.open("https://console.aws.amazon.com/billing/home?region=us-east-1#/account", "_blank")
+                        }}
+                      >
+                        help_outline
+                      </i>
+                    </Flex>
+                  }
+                  value={AWSAccountID}
+                  setValue={handleAWSAccountIDChange}
+                  placeholder="ex: 915037676314"
+                  error={AWSAccountIDInputError}
+                />
+              </>,
+              <>
+                <Text size={16}>2. Log in to your AWS Account.</Text>
+                <Spacer y={0.25} />
+                <Text color="helper">Return to Porter after successful log-in.</Text>
+                <Spacer y={0.5} />
+                <AWSButtonContainer>
+                  <ButtonImg src={aws} />
+                  <Button
+                    width={"170px"}
+                    onClick={directToAWSLoginAndProceedStep}
+                    color="#1E2631"
+                    withBorder
+                  >
+                    Log in
+                  </Button>
+                </AWSButtonContainer>
+                {/* escape hatch for dev use only */}
+                {process.env.TRUST_ARN != null && process.env.TRUST_ARN !== "arn:aws:iam::108458755588:role/CAPIManagement" &&
                   <>
-                    <Text size={16}>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:
-                    </Text>
-                    <Spacer y={1} />
-                    <Step number={1}>
-                      <Link to="https://aws.amazon.com/resources/create-account/" target="_blank">
-                        Create an AWS account
-                      </Link>
-                      <Spacer inline width="5px" />
-                      if you don't already have one.
-                    </Step>
-                    <Spacer y={1} />
-                    <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">
-                        copy your account ID
-                      </Link>.
-                    </Step>
-                    <Spacer y={1} />
-                    <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>
-                    <Spacer y={1} />
-                    <Step number={5}>Wait until the stack status has changed from "CREATE_IN_PROGRESS" to "CREATE_COMPLETE".</Step>
-                    <Spacer y={1} />
-                    <Step number={6}>Return to Porter and select "Continue".</Step>
+                    <Spacer y={0.5} />
+                    <Link onClick={() => setCurrentStep(4)} hasunderline>Skip this step</Link>
                   </>
                 }
-              />
-            ) : (
-              roleStatus
-            )
+              </>,
+              <>
+                <Text size={16}>3. Create an AWS Cloudformation Stack.</Text>
+                <Spacer y={0.25} />
+                <Text color="helper">This grants Porter permissions to create infrastructure.</Text>
+                <Spacer y={0.25} />
+                <Text color="helper">
+                  Return to Porter once the stack status has changed from "CREATE_IN_PROGRESS" to "CREATE_COMPLETE".
+                </Text>
+                <Spacer y={0.5} />
+                <AWSButtonContainer>
+                  <ButtonImg src={aws} />
+                  <Button
+                    width={"170px"}
+                    onClick={() => {
+                      if (AWSAccountID.length === 12 && !isNaN(Number(AWSAccountID))) {
+                        directToCloudFormationAndProceedStep();
+                      } else {
+                        setGrantPermissionsError("Invalid AWS account ID");
+                      }
+                    }}
+                    status={
+                      grantPermissionsError && (
+                        <Error message={grantPermissionsError} />
+                      )
+                    }
+                    color="#1E2631"
+                    withBorder
+                  >
+                    Grant permissions
+                  </Button>
+                </AWSButtonContainer>
+              </>,
+              <>
+                <Text size={16}>4. Continue to the provision step.</Text>
+                <Spacer y={0.5} />
+                <Button
+                  width={"200px"}
+                  onClick={checkIfRoleExists}
+                  status={
+                    errorMessage ? (
+                      <Error
+                        message={errorMessage}
+                        ctaText="Troubleshooting steps"
+                        errorModalContents={
+                          <>
+                            <Text size={16}>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:
+                            </Text>
+                            <Spacer y={1} />
+                            <Step number={1}>
+                              <Link to="https://aws.amazon.com/resources/create-account/" target="_blank">
+                                Create an AWS account
+                              </Link>
+                              <Spacer inline width="5px" />
+                              if you don't already have one.
+                            </Step>
+                            <Spacer y={1} />
+                            <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">
+                                copy your account ID
+                              </Link>.
+                            </Step>
+                            <Spacer y={1} />
+                            <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>
+                            <Spacer y={1} />
+                            <Step number={5}>Wait until the stack status has changed from "CREATE_IN_PROGRESS" to "CREATE_COMPLETE".</Step>
+                            <Spacer y={1} />
+                            <Step number={6}>Return to Porter and select "Continue".</Step>
+                          </>
+                        }
+                      />
+                    ) : (
+                      roleStatus
+                    )
+                  }
+                >
+                  Continue
+                </Button>
+              </>
+            ].filter(step => step != null)
           }
-        >
-          Continue
-        </Button>
+        />
       </>
     );
   }
@@ -247,9 +319,6 @@ const CloudFormationForm: React.FC<Props> = ({
         </Text>
       </Container>
       <Spacer y={1} />
-      <Text color="helper">
-        Grant Porter permissions to create infrastructure in your AWS account.
-      </Text>
       {renderContent()}
     </>
   );
@@ -302,4 +371,9 @@ const BackButton = styled.div`
     margin-right: 6px;
     margin-left: -2px;
   }
-`;
+`;
+
+const AWSButtonContainer = styled.div`
+  display: flex;
+  align-items: center;
+  `;

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

@@ -2451,6 +2451,7 @@ const updateOnboardingStep = baseApi<
     account_id?: string;
     cloudformation_url?: string;
     error_message?: string;
+    login_url?: string;
   },
   {}
 >("POST", (pathParams) => {

+ 1 - 0
internal/analytics/track_events.go

@@ -14,6 +14,7 @@ const (
 	PreProvisionCheck           SegmentEvent = "Pre Provision Check Started"
 	AWSInputted                 SegmentEvent = "AWS Account ID Inputted"
 	AWSCloudformationRedirect   SegmentEvent = "AWS Cloudformation Redirect"
+	AWSLoginRedirect            SegmentEvent = "AWS Login Redirect"
 	AWSCreateIntegrationSuccess SegmentEvent = "AWS Create Integration Success"
 	AWSCreateIntegrationFailure SegmentEvent = "AWS Create Integration Failure"
 	ProvisioningAttempted       SegmentEvent = "Provisioning Attempted"

+ 18 - 2
internal/analytics/tracks.go

@@ -205,7 +205,7 @@ func AWSInputTrack(opts *AWSInputTrackOpts) segmentTrack {
 	)
 }
 
-type AWSCloudFormationRedirectOpts struct {
+type AWSRedirectOpts struct {
 	*UserScopedTrackOpts
 
 	Email             string
@@ -214,10 +214,11 @@ type AWSCloudFormationRedirectOpts struct {
 	CompanyName       string
 	AccountId         string
 	CloudformationURL string
+	LoginURL          string
 }
 
 // AWSCloudformationRedirectSuccess returns a track for when a user clicks 'grant permissions' and gets redirected to cloudformation
-func AWSCloudformationRedirectSuccess(opts *AWSCloudFormationRedirectOpts) segmentTrack {
+func AWSCloudformationRedirectSuccess(opts *AWSRedirectOpts) segmentTrack {
 	additionalProps := make(map[string]interface{})
 	additionalProps["email"] = opts.Email
 	additionalProps["name"] = opts.FirstName + " " + opts.LastName
@@ -231,6 +232,21 @@ func AWSCloudformationRedirectSuccess(opts *AWSCloudFormationRedirectOpts) segme
 	)
 }
 
+// AWSLoginRedirectSuccess returns a track for when a user is prompted to login to AWS
+func AWSLoginRedirectSuccess(opts *AWSRedirectOpts) segmentTrack {
+	additionalProps := make(map[string]interface{})
+	additionalProps["email"] = opts.Email
+	additionalProps["name"] = opts.FirstName + " " + opts.LastName
+	additionalProps["company"] = opts.CompanyName
+	additionalProps["account_id"] = opts.AccountId
+	additionalProps["login_url"] = opts.LoginURL
+
+	return getSegmentUserTrack(
+		opts.UserScopedTrackOpts,
+		getDefaultSegmentTrack(additionalProps, AWSLoginRedirect),
+	)
+}
+
 type AWSCreateIntegrationOpts struct {
 	*UserScopedTrackOpts