ソースを参照

update onboarding flow for new provisioner

Alexander Belanger 4 年 前
コミット
e62c61d9dc

+ 2 - 2
dashboard/src/components/Description.tsx

@@ -1,11 +1,11 @@
 import styled from "styled-components";
 
-const Description = styled.div`
+const Description = styled.div<{ margin?: string }>`
   width: 100%;
   color: white;
   font-size: 13px;
   color: #aaaabb;
-  margin: 20px 0 10px 0;
+  margin: ${(props) => props.margin || "20px 0 10px 0"};
   display: flex;
   align-items: center;
 `;

+ 91 - 53
dashboard/src/components/ProvisionerStatus.tsx

@@ -1,11 +1,7 @@
-import { Steps } from "main/home/onboarding/types";
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useRef, useState } from "react";
 import { integrationList } from "shared/common";
-
-import loading from "assets/loading.gif";
-
 import styled, { keyframes } from "styled-components";
-import { capitalize, readableDate } from "shared/string_utils";
+import { readableDate } from "shared/string_utils";
 import {
   Infrastructure,
   KindMap,
@@ -18,19 +14,14 @@ import {
 import api from "shared/api";
 import Placeholder from "./Placeholder";
 import Loading from "./Loading";
-import ExpandedOperation from "main/home/infrastructure/components/ExpandedOperation";
 import { Context } from "shared/Context";
 import { useWebsockets } from "shared/hooks/useWebsockets";
 import Description from "./Description";
-import Heading from "./form-components/Heading";
-import PorterFormWrapper from "./porter-form/PorterFormWrapper";
-import SaveButton from "./SaveButton";
-import { ProgressPlugin } from "webpack";
 
 type Props = {
   infras: Infrastructure[];
   project_id: number;
-  setInfraStatus: (status: { hasError: boolean; description?: string }) => void;
+  setInfraStatus: (infra: Infrastructure) => void;
   auto_expanded?: boolean;
 };
 
@@ -62,9 +53,9 @@ const ProvisionerStatus: React.FC<Props> = ({
   };
 
   const updateInfraStatus = (infra: Infrastructure) => {
-    setInfraStatus({
-      hasError: infra.status === "errored",
-    });
+    // in order for this to propagate to parent, we check that all tracked infras (including
+    // the reported infra) are in a final state
+    setInfraStatus(infra);
   };
 
   const renderV2Infra = (infra: Infrastructure) => {
@@ -116,6 +107,7 @@ const V1InfraObject: React.FC<V1InfraObjectProps> = ({
         timestampLabel = "Created at";
         break;
       case "deleted":
+      case "destroyed":
         timestampLabel = "Deleted at";
         break;
       case "errored":
@@ -236,6 +228,7 @@ const V2InfraObject: React.FC<V2InfraObjectProps> = ({
   );
   const [fullInfra, setFullInfra] = useState<Infrastructure>(null);
   const [infraState, setInfraState] = useState<TFState>(null);
+
   const [isLoading, setIsLoading] = useState(false);
 
   useEffect(() => {
@@ -269,7 +262,7 @@ const V2InfraObject: React.FC<V2InfraObjectProps> = ({
       });
   };
 
-  const refreshInfra = () => {
+  const refreshInfra = (completed?: boolean, errored?: boolean) => {
     setIsLoading(true);
 
     api
@@ -282,8 +275,18 @@ const V2InfraObject: React.FC<V2InfraObjectProps> = ({
         }
       )
       .then(({ data }) => {
-        setFullInfra(data);
-        updateInfraStatus(data);
+        let infra = data as Infrastructure;
+
+        if (completed && infra.latest_operation) {
+          if (errored) {
+            infra.latest_operation.status = "errored";
+          } else {
+            infra.latest_operation.status = "completed";
+          }
+        }
+
+        setFullInfra(infra);
+        updateInfraStatus(infra);
 
         // re-query for the infra state
         refreshInfraState();
@@ -372,7 +375,7 @@ const V2InfraObject: React.FC<V2InfraObjectProps> = ({
 
 type OperationDetailsProps = {
   infra: Infrastructure;
-  refreshInfra: () => void;
+  refreshInfra: (completed?: boolean, errored?: boolean) => void;
 };
 
 const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
@@ -391,6 +394,9 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
   const [createdResources, setCreatedResources] = useState<TFResourceState[]>(
     []
   );
+  const [deletedResources, setDeletedResources] = useState<TFResourceState[]>(
+    []
+  );
   const [plannedResources, setPlannedResources] = useState<TFResourceState[]>(
     []
   );
@@ -402,7 +408,7 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
 
     if (status == "OPERATION_COMPLETED") {
       // if the operation is completed, call the completed handler
-      refreshInfra();
+      refreshInfra(true, erroredResources.length > 0);
     } else if (status && resource_id) {
       // if the status and resource_id are defined, add this to the infra state
       setInfraState((curr) => {
@@ -413,9 +419,7 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
           resources: { ...curr.resources },
         };
 
-        if (currCopy.resources[resource_id] && status == "deleted") {
-          delete currCopy.resources[resource_id];
-        } else if (currCopy.resources[resource_id]) {
+        if (currCopy.resources[resource_id]) {
           currCopy.resources[resource_id].status = status;
           currCopy.resources[resource_id].error = error;
         } else {
@@ -482,13 +486,16 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
       })
       .catch((err) => {
         console.error(err);
-        setInfraState({
-          last_updated: "",
-          operation_id: infra.latest_operation.id,
-          status: "creating",
-          resources: {},
-        });
-        setInfraStateInitialized(true);
+
+        if (!infraStateInitialized) {
+          setInfraState({
+            last_updated: "",
+            operation_id: infra.latest_operation.id,
+            status: "creating",
+            resources: {},
+          });
+          setInfraStateInitialized(true);
+        }
       });
   }, [currentProject, infra]);
 
@@ -544,10 +551,25 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
           .filter((val) => val)
       );
 
+      setDeletedResources(
+        Object.keys(infraState.resources)
+          .map((key) => {
+            if (infraState.resources[key].status == "deleted") {
+              return infraState.resources[key];
+            }
+
+            return null;
+          })
+          .filter((val) => val)
+      );
+
       setPlannedResources(
         Object.keys(infraState.resources)
           .map((key) => {
-            if (infraState.resources[key].status == "planned_create") {
+            if (
+              infraState.resources[key].status == "planned_create" ||
+              infraState.resources[key].status == "planned_delete"
+            ) {
               return infraState.resources[key];
             }
 
@@ -625,20 +647,40 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
     plannedResourceCount: number
   ) => {
     let width = (100.0 * completedResourceCount) / plannedResourceCount;
-
     let operationKind = "Created";
+    let count = `${completedResourceCount} / ${plannedResourceCount}`;
+
+    if (
+      infra.latest_operation.status == "completed" &&
+      (infra.latest_operation.type == "delete" ||
+        infra.latest_operation.type == "retry_delete")
+    ) {
+      width = 100.0;
+      count = "";
+    } else if (
+      infra.latest_operation.status != "completed" &&
+      plannedResourceCount == 0
+    ) {
+      // in the case when the planned resource count is 0, the state is still being computed, so
+      // render 0 width and "Planning..." message
+      width = 0;
+      operationKind = "Planning...";
+      count = "";
+    }
 
-    switch (infra.latest_operation.type) {
-      case "retry_create":
-      case "create":
-        operationKind = "Created";
-        break;
-      case "update":
-        operationKind = "Updated";
-        break;
-      case "retry_delete":
-      case "delete":
-        operationKind = "Deleted";
+    if (operationKind != "Planning...") {
+      switch (infra.latest_operation.type) {
+        case "retry_create":
+        case "create":
+          operationKind = "Created";
+          break;
+        case "update":
+          operationKind = "Updated";
+          break;
+        case "retry_delete":
+        case "delete":
+          operationKind = "Deleted";
+      }
     }
 
     return (
@@ -646,13 +688,16 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
         <LoadingBar>
           <LoadingFill status="loading" width={width + "%"} />
         </LoadingBar>
-        <ResourceNumber>{`${completedResourceCount} / ${plannedResourceCount} ${operationKind}`}</ResourceNumber>
+        <ResourceNumber>{`${count} ${operationKind}`}</ResourceNumber>
       </StatusContainer>
     );
   };
 
   const renderErrorSection = () => {
-    if (erroredResources.length > 0) {
+    if (
+      erroredResources.length > 0 &&
+      infra?.latest_operation?.status == "errored"
+    ) {
       return (
         <>
           <Description>
@@ -673,7 +718,7 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
   return (
     <StyledCard>
       {renderLoadingBar(
-        createdResources.length,
+        createdResources.length + deletedResources.length,
         createdResources.length +
           erroredResources.length +
           plannedResources.length
@@ -740,13 +785,6 @@ const StatusContainer = styled.div`
   justify-content: space-between;
 `;
 
-const StatusText = styled.div`
-  font-size: 13px;
-  margin-left: 15px;
-  color: #aaaabb;
-  font-weight: 400;
-`;
-
 const ResourceNumber = styled.div`
   font-size: 12px;
   margin-left: 7px;

+ 73 - 17
dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx

@@ -17,6 +17,10 @@ import { useSnapshot } from "valtio";
 import { OFState } from "../../state";
 import { provisionResourcesTracks } from "shared/anayltics";
 import DocsHelper from "components/DocsHelper";
+import Description from "components/Description";
+import api from "shared/api";
+import Placeholder from "components/Placeholder";
+import Loading from "components/Loading";
 
 type Props = {};
 
@@ -25,9 +29,12 @@ const ProvisionResources: React.FC<Props> = () => {
   const { step } = useParams<{ step: any }>();
   const [infraStatus, setInfraStatus] = useState<{
     hasError: boolean;
+    errored_infras: number[];
     description?: string;
   }>(null);
 
+  const [isLoading, setIsLoading] = useState(false);
+
   const shouldProvisionRegistry = !!snap.StateHandler.connected_registry?.skip;
   const provider = snap.StateHandler.provision_resources?.provider;
   const project = snap.StateHandler.project;
@@ -52,6 +59,30 @@ const ProvisionResources: React.FC<Props> = () => {
     OFState.actions.nextStep("skip");
   };
 
+  const retryFailedInfras = () => {
+    setIsLoading(true);
+
+    // call API endpoint to retry all failed infras
+    const promises = Promise.all(
+      infraStatus?.errored_infras.map(async (erroredInfraID) => {
+        const res = await api.retryCreateInfra(
+          "<token>",
+          {},
+          {
+            project_id: project.id,
+            infra_id: erroredInfraID,
+          }
+        );
+        return res.data;
+      })
+    );
+
+    promises.then(() => {
+      setInfraStatus(null);
+      setIsLoading(false);
+    });
+  };
+
   const renderSaveButton = () => {
     if (typeof infraStatus?.hasError !== "boolean") {
       return;
@@ -76,19 +107,25 @@ const ProvisionResources: React.FC<Props> = () => {
       return (
         <>
           <Br height="15px" />
-          <SaveButton
-            text="Resolve Errors"
-            status={
-              infraStatus?.description ||
-              "Encountered errors while provisioning."
-            }
-            disabled={false}
-            onClick={() => handleGoBack(infraStatus.description)}
-            makeFlush={true}
-            clearPosition={true}
-            statusPosition="right"
-            saveText=""
-          />
+          <ErrorStateContainer>
+            <StyledBack>
+              <StyledDescription
+                margin="0"
+                onClick={() => handleGoBack(infraStatus.description)}
+              >
+                {"< Back to Settings"}
+              </StyledDescription>
+            </StyledBack>
+            <SaveButton
+              text="Retry"
+              disabled={false}
+              onClick={retryFailedInfras}
+              makeFlush={true}
+              clearPosition={true}
+              statusPosition="right"
+              saveText=""
+            />
+          </ErrorStateContainer>
         </>
       );
     }
@@ -113,6 +150,14 @@ const ProvisionResources: React.FC<Props> = () => {
       case "settings":
         return <FormFlowWrapper currentStep={step} />;
       case "status":
+        if (isLoading) {
+          return (
+            <Placeholder>
+              <Loading />
+            </Placeholder>
+          );
+        }
+
         return (
           <>
             <StatusPage
@@ -194,10 +239,6 @@ const Subtitle = styled.div`
   display: flex;
 `;
 
-const NextStep = styled(SaveButton)`
-  margin-top: 24px;
-`;
-
 const BackButton = styled.div`
   margin-bottom: 24px;
   display: flex;
@@ -222,3 +263,18 @@ const BackButtonImg = styled.img`
   width: 16px;
   opacity: 0.75;
 `;
+
+const ErrorStateContainer = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+`;
+
+const StyledBack = styled.div`
+  margin-left: 14px;
+`;
+
+const StyledDescription = styled(Description)`
+  text-align: right;
+  cursor: pointer;
+`;

+ 67 - 5
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/StatusPage.tsx

@@ -1,18 +1,22 @@
 import Loading from "components/Loading";
 import ProvisionerStatus from "components/ProvisionerStatus";
-import React, { useEffect, useMemo, useRef, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
 import api from "shared/api";
-import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
 import { Infrastructure } from "shared/types";
 import styled from "styled-components";
 
 type Props = {
-  setInfraStatus: (status: { hasError: boolean; description?: string }) => void;
+  setInfraStatus: (status: {
+    hasError: boolean;
+    description?: string;
+    errored_infras: number[];
+  }) => void;
   project_id: number;
   filter: string[];
   auto_expanded?: boolean;
   notFoundText?: string;
   filterLatest?: boolean;
+  retry_count?: number;
 };
 
 export const StatusPage = ({
@@ -22,6 +26,7 @@ export const StatusPage = ({
   notFoundText = "We couldn't find any infra being provisioned.",
   filterLatest,
   auto_expanded,
+  retry_count,
 }: Props) => {
   const isMounted = useRef(false);
   const [isLoading, setIsLoading] = useState(true);
@@ -68,6 +73,63 @@ export const StatusPage = ({
     return Array.from(infraMap.values());
   };
 
+  const updateSingleInfraStatus = (infra: Infrastructure) => {
+    // update the single infra
+    setInfras((infras) => {
+      if (!infras) {
+        return [infra];
+      }
+
+      let newInfras = infras;
+
+      newInfras = newInfras.map((newInfra) => {
+        if (newInfra.id == infra.id) {
+          return infra;
+        }
+
+        return newInfra;
+      });
+
+      // determine if all infras are in a final state, and if so report to parent
+      let inProgressInfras = newInfras.filter((newInfra) => {
+        if (newInfra.latest_operation) {
+          return (
+            newInfra.latest_operation.status != "completed" &&
+            newInfra.latest_operation.status != "errored"
+          );
+        }
+
+        return (
+          newInfra.status == "creating" ||
+          newInfra.status == "deleting" ||
+          newInfra.status == "destroying"
+        );
+      });
+
+      let erroredInfras = newInfras.filter((newInfra) => {
+        if (newInfra.latest_operation) {
+          return newInfra.latest_operation.status == "errored";
+        }
+
+        return newInfra.status == "errored";
+      });
+
+      if (inProgressInfras.length == 0) {
+        setInfraStatus({
+          hasError: erroredInfras.length != 0,
+          errored_infras: erroredInfras.map((infra) => {
+            return infra.id;
+          }),
+        });
+      }
+
+      return [...newInfras];
+    });
+
+    // determine if all tracked infras are in a finalized state, and then report the
+    // infra status to the parent
+  };
+
   useEffect(() => {
     api
       .getInfra<Infrastructure[]>("<token>", {}, { project_id: project_id })
@@ -87,7 +149,7 @@ export const StatusPage = ({
       .catch((err) => {
         console.error(err);
       });
-  }, [project_id]);
+  }, [project_id, retry_count]);
 
   if (isLoading) {
     return (
@@ -106,7 +168,7 @@ export const StatusPage = ({
       infras={infras}
       project_id={project_id}
       auto_expanded={auto_expanded}
-      setInfraStatus={setInfraStatus}
+      setInfraStatus={updateSingleInfraStatus}
     />
   );
 };

BIN
dump.rdb


+ 11 - 0
provisioner/server/handlers/provision/apply.go

@@ -12,6 +12,7 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/random"
 	"github.com/porter-dev/porter/provisioner/integrations/provisioner"
+	"github.com/porter-dev/porter/provisioner/integrations/redis_stream"
 	"github.com/porter-dev/porter/provisioner/server/config"
 	"golang.org/x/crypto/bcrypt"
 
@@ -84,6 +85,16 @@ func (c *ProvisionApplyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		return
 	}
 
+	// push a first message to the operation stream
+	err = redis_stream.PushToOperationStream(c.Config.RedisClient, infra, operation, &ptypes.TFResourceState{
+		Status: "OPERATION_STARTED",
+	})
+
+	if err != nil {
+		apierrors.HandleAPIError(c.Config.Logger, c.Config.Alerter, w, r, apierrors.NewErrInternal(err), true)
+		return
+	}
+
 	// spawn a new provisioning process
 	err = c.Config.Provisioner.Provision(&provisioner.ProvisionOpts{
 		Infra:         infra,