|
|
@@ -1,38 +1,39 @@
|
|
|
import { Steps } from "main/home/onboarding/types";
|
|
|
-import React, { useState } from "react";
|
|
|
+import React, { useContext, useEffect, 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 {
|
|
|
+ Infrastructure,
|
|
|
+ KindMap,
|
|
|
+ Operation,
|
|
|
+ OperationStatus,
|
|
|
+ OperationType,
|
|
|
+ TFResourceState,
|
|
|
+ TFState,
|
|
|
+} from "shared/types";
|
|
|
+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 = {
|
|
|
- modules: TFModule[];
|
|
|
+ infras: Infrastructure[];
|
|
|
+ project_id: number;
|
|
|
+ setInfraStatus: (status: { hasError: boolean; description?: string }) => void;
|
|
|
+ auto_expanded?: boolean;
|
|
|
};
|
|
|
|
|
|
-export interface TFModule {
|
|
|
- id: number;
|
|
|
- kind: string;
|
|
|
- status: string;
|
|
|
- created_at: string;
|
|
|
- updated_at: string;
|
|
|
- global_errors?: TFResourceError[];
|
|
|
- got_desired: boolean;
|
|
|
- // optional resources, if not created
|
|
|
- resources?: TFResource[];
|
|
|
-}
|
|
|
-
|
|
|
-export interface TFResourceError {
|
|
|
- errored_out?: boolean;
|
|
|
- error_context?: string;
|
|
|
-}
|
|
|
-
|
|
|
-export interface TFResource {
|
|
|
- addr: string;
|
|
|
- provisioned: boolean;
|
|
|
- errored: TFResourceError;
|
|
|
-}
|
|
|
-
|
|
|
const nameMap: { [key: string]: string } = {
|
|
|
eks: "Elastic Kubernetes Service (EKS)",
|
|
|
ecr: "Elastic Container Registry (ECR)",
|
|
|
@@ -40,130 +41,562 @@ const nameMap: { [key: string]: string } = {
|
|
|
docr: "DigitalOcean Container Registry (DOCR)",
|
|
|
gke: "Google Kubernetes Engine (GKE)",
|
|
|
gcr: "Google Container Registry (GCR)",
|
|
|
+ rds: "Amazon Relational Database (RDS)",
|
|
|
};
|
|
|
|
|
|
-const ProvisionerStatus: React.FC<Props> = ({ modules }) => {
|
|
|
- const renderStatus = (status: string) => {
|
|
|
- if (status === "successful") {
|
|
|
- return (
|
|
|
- <StatusIcon successful={true}>
|
|
|
- <i className="material-icons">done</i>
|
|
|
- </StatusIcon>
|
|
|
- );
|
|
|
- } else if (status === "loading") {
|
|
|
- return (
|
|
|
- <StatusIcon>
|
|
|
- <LoadingGif src={loading} />
|
|
|
- </StatusIcon>
|
|
|
- );
|
|
|
- } else if (status === "error") {
|
|
|
- return (
|
|
|
- <StatusIcon>
|
|
|
- <i className="material-icons">error_outline</i>
|
|
|
- </StatusIcon>
|
|
|
- );
|
|
|
+const ProvisionerStatus: React.FC<Props> = ({
|
|
|
+ infras,
|
|
|
+ project_id,
|
|
|
+ auto_expanded,
|
|
|
+ setInfraStatus,
|
|
|
+}) => {
|
|
|
+ const renderV1Infra = (infra: Infrastructure) => {
|
|
|
+ let errors: string[] = [];
|
|
|
+
|
|
|
+ if (infra.status == "destroyed" || infra.status == "deleted") {
|
|
|
+ errors.push("This infrastructure was destroyed.");
|
|
|
+ }
|
|
|
+
|
|
|
+ let error = null;
|
|
|
+
|
|
|
+ if (errors.length > 0) {
|
|
|
+ error = errors.map((error, index) => {
|
|
|
+ return <ExpandedError key={index}>{error}</ExpandedError>;
|
|
|
+ });
|
|
|
}
|
|
|
+
|
|
|
+ return (
|
|
|
+ <StyledInfraObject key={infra.id}>
|
|
|
+ <InfraHeader is_clickable={!auto_expanded}>
|
|
|
+ <Flex>
|
|
|
+ {integrationList[infra.kind] && (
|
|
|
+ <Icon src={integrationList[infra.kind].icon} />
|
|
|
+ )}
|
|
|
+ {KindMap[infra.kind]?.provider_name}
|
|
|
+ </Flex>
|
|
|
+ <Timestamp>Started {readableDate(infra.created_at)}</Timestamp>
|
|
|
+ </InfraHeader>
|
|
|
+ <ErrorWrapper>{error}</ErrorWrapper>
|
|
|
+ </StyledInfraObject>
|
|
|
+ );
|
|
|
};
|
|
|
|
|
|
- const readableDate = (s: string) => {
|
|
|
- const ts = new Date(s);
|
|
|
- const date = ts.toLocaleDateString();
|
|
|
- const time = ts.toLocaleTimeString([], {
|
|
|
- hour: "numeric",
|
|
|
- minute: "2-digit",
|
|
|
+ const updateInfraStatus = (infra: Infrastructure) => {
|
|
|
+ setInfraStatus({
|
|
|
+ hasError: infra.status === "errored",
|
|
|
});
|
|
|
- return `${time} on ${date}`;
|
|
|
};
|
|
|
|
|
|
- const renderModules = () => {
|
|
|
- return modules.map((val) => {
|
|
|
- const totalResources = val.resources?.length;
|
|
|
- const provisionedResources = val.resources?.filter((resource) => {
|
|
|
- return resource.provisioned;
|
|
|
- }).length;
|
|
|
-
|
|
|
- let errors: string[] = [];
|
|
|
+ const renderV2Infra = (infra: Infrastructure) => {
|
|
|
+ return (
|
|
|
+ <InfraObject
|
|
|
+ key={infra.id}
|
|
|
+ project_id={project_id}
|
|
|
+ infra={infra}
|
|
|
+ is_expanded={auto_expanded}
|
|
|
+ is_collapsible={!auto_expanded}
|
|
|
+ updateInfraStatus={updateInfraStatus}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
|
|
|
- if (val.status == "destroyed") {
|
|
|
- errors.push("Note: this infrastructure was automatically destroyed.");
|
|
|
+ const renderInfras = () => {
|
|
|
+ return infras.map((infra) => {
|
|
|
+ if (infra.api_version == "" || infra.api_version == "v1") {
|
|
|
+ return renderV1Infra(infra);
|
|
|
}
|
|
|
|
|
|
- let hasError =
|
|
|
- val.resources?.filter((resource) => {
|
|
|
- if (resource.errored?.errored_out) {
|
|
|
- errors.push(resource.errored?.error_context);
|
|
|
+ return renderV2Infra(infra);
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ return <StyledProvisionerStatus>{renderInfras()}</StyledProvisionerStatus>;
|
|
|
+};
|
|
|
+
|
|
|
+export default ProvisionerStatus;
|
|
|
+
|
|
|
+type InfraObjectProps = {
|
|
|
+ infra: Infrastructure;
|
|
|
+ project_id: number;
|
|
|
+ is_expanded: boolean;
|
|
|
+ is_collapsible: boolean;
|
|
|
+ updateInfraStatus: (infra: Infrastructure) => void;
|
|
|
+};
|
|
|
+
|
|
|
+const InfraObject: React.FC<InfraObjectProps> = ({
|
|
|
+ infra,
|
|
|
+ project_id,
|
|
|
+ is_expanded,
|
|
|
+ is_collapsible,
|
|
|
+ updateInfraStatus,
|
|
|
+}) => {
|
|
|
+ const [isExpanded, setIsExpanded] = useState(is_expanded);
|
|
|
+ const [isInProgress, setIsInProgress] = useState(
|
|
|
+ infra.status == "creating" ||
|
|
|
+ infra.status == "updating" ||
|
|
|
+ infra.status == "deleting"
|
|
|
+ );
|
|
|
+ const [fullInfra, setFullInfra] = useState<Infrastructure>(null);
|
|
|
+ const [infraState, setInfraState] = useState<TFState>(null);
|
|
|
+ const [isLoading, setIsLoading] = useState(false);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if ((isExpanded || isInProgress) && !fullInfra) {
|
|
|
+ refreshInfra();
|
|
|
+ }
|
|
|
+ }, [infra, project_id, isExpanded, isInProgress]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if ((isExpanded || isInProgress) && !infraState) {
|
|
|
+ refreshInfraState();
|
|
|
+ }
|
|
|
+ }, [infra, project_id, isExpanded, isInProgress]);
|
|
|
+
|
|
|
+ const refreshInfraState = () => {
|
|
|
+ api
|
|
|
+ .getInfraState(
|
|
|
+ "<token>",
|
|
|
+ {},
|
|
|
+ {
|
|
|
+ project_id: project_id,
|
|
|
+ infra_id: infra.id,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ .then(({ data }) => {
|
|
|
+ setInfraState(data);
|
|
|
+ setIsLoading(false);
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ console.error(err);
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const refreshInfra = () => {
|
|
|
+ setIsLoading(true);
|
|
|
+
|
|
|
+ api
|
|
|
+ .getInfraByID(
|
|
|
+ "<token>",
|
|
|
+ {},
|
|
|
+ {
|
|
|
+ project_id: project_id,
|
|
|
+ infra_id: infra.id,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ .then(({ data }) => {
|
|
|
+ setFullInfra(data);
|
|
|
+ updateInfraStatus(data);
|
|
|
+
|
|
|
+ // re-query for the infra state
|
|
|
+ refreshInfraState();
|
|
|
+
|
|
|
+ setIsLoading(false);
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ console.error(err);
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderExpandedContentsCreated = () => {
|
|
|
+ return <OperationDetails infra={fullInfra} refreshInfra={refreshInfra} />;
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderExpandedContents = () => {
|
|
|
+ if (!isExpanded) {
|
|
|
+ return null;
|
|
|
+ } else if (fullInfra) {
|
|
|
+ return renderExpandedContentsCreated();
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <ErrorWrapper>
|
|
|
+ <Placeholder>
|
|
|
+ <Loading />{" "}
|
|
|
+ </Placeholder>
|
|
|
+ </ErrorWrapper>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderTimestampSection = () => {
|
|
|
+ let timestampLabel = "Started at";
|
|
|
+
|
|
|
+ switch (infra.status) {
|
|
|
+ case "created":
|
|
|
+ timestampLabel = "Created at";
|
|
|
+ break;
|
|
|
+ case "deleted":
|
|
|
+ timestampLabel = "Deleted at";
|
|
|
+ break;
|
|
|
+ case "errored":
|
|
|
+ timestampLabel = "Errored at";
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Timestamp>
|
|
|
+ {timestampLabel} {readableDate(infra.updated_at)}
|
|
|
+ </Timestamp>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <StyledInfraObject key={infra.id}>
|
|
|
+ <InfraHeader
|
|
|
+ is_clickable={is_collapsible}
|
|
|
+ onClick={() => {
|
|
|
+ if (is_collapsible) {
|
|
|
+ setIsExpanded((val) => {
|
|
|
+ setIsLoading(true);
|
|
|
+ return !val;
|
|
|
+ });
|
|
|
}
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Flex>
|
|
|
+ {integrationList[infra.kind] && (
|
|
|
+ <Icon src={integrationList[infra.kind].icon} />
|
|
|
+ )}
|
|
|
+ {KindMap[infra.kind]?.provider_name}
|
|
|
+ </Flex>
|
|
|
+ <Flex>
|
|
|
+ {renderTimestampSection()}
|
|
|
+ <ExpandIconContainer hidden={!is_collapsible}>
|
|
|
+ <i className="material-icons expand-icon">
|
|
|
+ {isExpanded ? "expand_less" : "expand_more"}
|
|
|
+ </i>
|
|
|
+ </ExpandIconContainer>
|
|
|
+ </Flex>
|
|
|
+ </InfraHeader>
|
|
|
+ {renderExpandedContents()}
|
|
|
+ </StyledInfraObject>
|
|
|
+ );
|
|
|
+};
|
|
|
|
|
|
- return resource.errored?.errored_out;
|
|
|
- }).length > 0;
|
|
|
+type OperationDetailsProps = {
|
|
|
+ infra: Infrastructure;
|
|
|
+ refreshInfra: () => void;
|
|
|
+};
|
|
|
|
|
|
- if (val.global_errors) {
|
|
|
- for (let globalErr of val.global_errors) {
|
|
|
- errors.push(globalErr.error_context);
|
|
|
- hasError = true;
|
|
|
+const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
|
|
|
+ infra,
|
|
|
+ refreshInfra,
|
|
|
+}) => {
|
|
|
+ const [isLoading, setIsLoading] = useState(true);
|
|
|
+ const [hasError, setHasError] = useState(false);
|
|
|
+ const [operation, setOperation] = useState<Operation>(null);
|
|
|
+ const [infraState, setInfraState] = useState<TFState>(null);
|
|
|
+ const [infraStateInitialized, setInfraStateInitialized] = useState(false);
|
|
|
+ const { currentProject, setCurrentError } = useContext(Context);
|
|
|
+ const [erroredResources, setErroredResources] = useState<TFResourceState[]>(
|
|
|
+ []
|
|
|
+ );
|
|
|
+ const [createdResources, setCreatedResources] = useState<TFResourceState[]>(
|
|
|
+ []
|
|
|
+ );
|
|
|
+ const [plannedResources, setPlannedResources] = useState<TFResourceState[]>(
|
|
|
+ []
|
|
|
+ );
|
|
|
+
|
|
|
+ const { newWebsocket, openWebsocket, closeWebsocket } = useWebsockets();
|
|
|
+
|
|
|
+ const parseOperationWebsocketEvent = (evt: MessageEvent) => {
|
|
|
+ let { status, resource_id, error } = JSON.parse(evt.data);
|
|
|
+
|
|
|
+ if (status == "OPERATION_COMPLETED") {
|
|
|
+ // if the operation is completed, call the completed handler
|
|
|
+ refreshInfra();
|
|
|
+ } else if (status && resource_id) {
|
|
|
+ // if the status and resource_id are defined, add this to the infra state
|
|
|
+ setInfraState((curr) => {
|
|
|
+ let currCopy: TFState = {
|
|
|
+ last_updated: curr.last_updated,
|
|
|
+ operation_id: curr.operation_id,
|
|
|
+ status: curr.status,
|
|
|
+ resources: { ...curr.resources },
|
|
|
+ };
|
|
|
+
|
|
|
+ if (currCopy.resources[resource_id] && status == "deleted") {
|
|
|
+ delete currCopy.resources[resource_id];
|
|
|
+ } else if (currCopy.resources[resource_id]) {
|
|
|
+ currCopy.resources[resource_id].status = status;
|
|
|
+ currCopy.resources[resource_id].error = error;
|
|
|
+ } else {
|
|
|
+ currCopy.resources[resource_id] = {
|
|
|
+ id: resource_id,
|
|
|
+ status: status,
|
|
|
+ error: error,
|
|
|
+ };
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- // remove duplicate errors
|
|
|
- errors = errors.filter(
|
|
|
- (error, index, self) =>
|
|
|
- index ===
|
|
|
- self.findIndex((e) => {
|
|
|
- if (e && error) {
|
|
|
- return e === error || e.includes(error) || error.includes(e);
|
|
|
+ return currCopy;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const setupOperationWebsocket = (websocketID: string) => {
|
|
|
+ let apiPath = `/api/projects/${currentProject.id}/infras/${infra.id}/operations/${infra.latest_operation.id}/state`;
|
|
|
+
|
|
|
+ const wsConfig = {
|
|
|
+ onopen: () => {
|
|
|
+ console.log(`connected to websocket:`, websocketID);
|
|
|
+ },
|
|
|
+ onmessage: parseOperationWebsocketEvent,
|
|
|
+ onclose: () => {
|
|
|
+ console.log(`closing websocket:`, websocketID);
|
|
|
+ },
|
|
|
+ onerror: (err: ErrorEvent) => {
|
|
|
+ console.log(err);
|
|
|
+ closeWebsocket(websocketID);
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ newWebsocket(websocketID, apiPath, wsConfig);
|
|
|
+ openWebsocket(websocketID);
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ // if the latest_operation is in progress, open a websocket
|
|
|
+ if (infraStateInitialized && infra.latest_operation.status === "starting") {
|
|
|
+ const websocketID = infra.latest_operation.id;
|
|
|
+
|
|
|
+ setupOperationWebsocket(websocketID);
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ closeWebsocket(websocketID);
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }, [infraStateInitialized]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ api
|
|
|
+ .getInfraState(
|
|
|
+ "<token>",
|
|
|
+ {},
|
|
|
+ {
|
|
|
+ project_id: currentProject.id,
|
|
|
+ infra_id: infra.id,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ .then(({ data }) => {
|
|
|
+ setInfraState(data);
|
|
|
+ setIsLoading(false);
|
|
|
+ setInfraStateInitialized(true);
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ console.error(err);
|
|
|
+ setInfraState({
|
|
|
+ last_updated: "",
|
|
|
+ operation_id: infra.latest_operation.id,
|
|
|
+ status: "creating",
|
|
|
+ resources: {},
|
|
|
+ });
|
|
|
+ setInfraStateInitialized(true);
|
|
|
+ });
|
|
|
+ }, [currentProject, infra]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ api
|
|
|
+ .getOperation(
|
|
|
+ "<token>",
|
|
|
+ {},
|
|
|
+ {
|
|
|
+ project_id: currentProject.id,
|
|
|
+ infra_id: infra.id,
|
|
|
+ operation_id: infra.latest_operation.id,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ .then(({ data }) => {
|
|
|
+ setOperation(data);
|
|
|
+ setIsLoading(false);
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ console.error(err);
|
|
|
+ setHasError(true);
|
|
|
+ setCurrentError(err.response?.data?.error);
|
|
|
+ setIsLoading(false);
|
|
|
+ });
|
|
|
+ }, [currentProject, infra]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (infraState && infraState.resources) {
|
|
|
+ setErroredResources(
|
|
|
+ Object.keys(infraState.resources)
|
|
|
+ .map((key) => {
|
|
|
+ if (
|
|
|
+ infraState.resources[key].error &&
|
|
|
+ infraState.resources[key].error != null
|
|
|
+ ) {
|
|
|
+ return infraState.resources[key];
|
|
|
}
|
|
|
+
|
|
|
+ return null;
|
|
|
})
|
|
|
+ .filter((val) => val)
|
|
|
);
|
|
|
|
|
|
- const width =
|
|
|
- val.status == "created"
|
|
|
- ? 100
|
|
|
- : 100 * (provisionedResources / (totalResources * 1.0)) || 0;
|
|
|
+ setCreatedResources(
|
|
|
+ Object.keys(infraState.resources)
|
|
|
+ .map((key) => {
|
|
|
+ if (infraState.resources[key].status == "created") {
|
|
|
+ return infraState.resources[key];
|
|
|
+ }
|
|
|
|
|
|
- let error = null;
|
|
|
+ return null;
|
|
|
+ })
|
|
|
+ .filter((val) => val)
|
|
|
+ );
|
|
|
|
|
|
- if (hasError) {
|
|
|
- error = errors.map((error, index) => {
|
|
|
- return <ExpandedError key={index}>{error}</ExpandedError>;
|
|
|
- });
|
|
|
- }
|
|
|
- let loadingFill;
|
|
|
- let status;
|
|
|
-
|
|
|
- if (hasError || val.status == "destroyed") {
|
|
|
- loadingFill = <LoadingFill status="error" width={width + "%"} />;
|
|
|
- status = renderStatus("error");
|
|
|
- } else if (width == 100) {
|
|
|
- loadingFill = <LoadingFill status="successful" width={width + "%"} />;
|
|
|
- status = renderStatus("successful");
|
|
|
- } else {
|
|
|
- loadingFill = <LoadingFill status="loading" width={width + "%"} />;
|
|
|
- status = renderStatus("loading");
|
|
|
- }
|
|
|
+ setPlannedResources(
|
|
|
+ Object.keys(infraState.resources)
|
|
|
+ .map((key) => {
|
|
|
+ if (infraState.resources[key].status == "planned_create") {
|
|
|
+ return infraState.resources[key];
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ })
|
|
|
+ .filter((val) => val)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }, [infraState]);
|
|
|
+
|
|
|
+ if (isLoading || !infraState) {
|
|
|
+ return (
|
|
|
+ <Placeholder>
|
|
|
+ <Loading />
|
|
|
+ </Placeholder>
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
+ if (hasError) {
|
|
|
+ return <Placeholder>Error</Placeholder>;
|
|
|
+ }
|
|
|
+
|
|
|
+ const getOperationDescription = (
|
|
|
+ type: OperationType,
|
|
|
+ status: OperationStatus,
|
|
|
+ time: string
|
|
|
+ ): string => {
|
|
|
+ switch (type) {
|
|
|
+ case "retry_create":
|
|
|
+ case "create":
|
|
|
+ if (status == "starting") {
|
|
|
+ return (
|
|
|
+ "Status: infrastructure creation in progress, started at " +
|
|
|
+ readableDate(time)
|
|
|
+ );
|
|
|
+ } else if (status == "completed") {
|
|
|
+ return (
|
|
|
+ "Status: infrastructure creation completed at " + readableDate(time)
|
|
|
+ );
|
|
|
+ } else if (status == "errored") {
|
|
|
+ return "Status: this infrastructure encountered an error while creating.";
|
|
|
+ }
|
|
|
+ case "update":
|
|
|
+ if (status == "starting") {
|
|
|
+ return (
|
|
|
+ "Status: infrastructure update in progress, started at " +
|
|
|
+ readableDate(time)
|
|
|
+ );
|
|
|
+ } else if (status == "completed") {
|
|
|
+ return (
|
|
|
+ "Status: infrastructure update completed at " + readableDate(time)
|
|
|
+ );
|
|
|
+ } else if (status == "errored") {
|
|
|
+ return "Status: this infrastructure encountered an error while updating.";
|
|
|
+ }
|
|
|
+ case "retry_delete":
|
|
|
+ case "delete":
|
|
|
+ if (status == "starting") {
|
|
|
+ return (
|
|
|
+ "Status: infrastructure deletion in progress, started at " +
|
|
|
+ readableDate(time)
|
|
|
+ );
|
|
|
+ } else if (status == "completed") {
|
|
|
+ return (
|
|
|
+ "Status: infrastructure deletion completed at " + readableDate(time)
|
|
|
+ );
|
|
|
+ } else if (status == "errored") {
|
|
|
+ return "Status: this infrastructure encountered an error while deleting.";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderLoadingBar = (
|
|
|
+ completedResourceCount: number,
|
|
|
+ plannedResourceCount: number
|
|
|
+ ) => {
|
|
|
+ let width = (100.0 * completedResourceCount) / plannedResourceCount;
|
|
|
+
|
|
|
+ let operationKind = "Created";
|
|
|
+
|
|
|
+ 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 (
|
|
|
+ <StatusContainer>
|
|
|
+ <LoadingBar>
|
|
|
+ <LoadingFill status="loading" width={width + "%"} />
|
|
|
+ </LoadingBar>
|
|
|
+ <ResourceNumber>{`${completedResourceCount} / ${plannedResourceCount} ${operationKind}`}</ResourceNumber>
|
|
|
+ </StatusContainer>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderErrorSection = () => {
|
|
|
+ if (erroredResources.length > 0) {
|
|
|
return (
|
|
|
- <InfraObject key={val.id}>
|
|
|
- <InfraHeader>
|
|
|
- <Flex>
|
|
|
- {status}
|
|
|
- {integrationList[val.kind] && (
|
|
|
- <Icon src={integrationList[val.kind].icon} />
|
|
|
- )}
|
|
|
- {nameMap[val.kind]}
|
|
|
- </Flex>
|
|
|
- <Timestamp>Started {readableDate(val.created_at)}</Timestamp>
|
|
|
- </InfraHeader>
|
|
|
- <LoadingBar>{loadingFill}</LoadingBar>
|
|
|
- <ErrorWrapper>{error}</ErrorWrapper>
|
|
|
- </InfraObject>
|
|
|
+ <>
|
|
|
+ <Description>
|
|
|
+ Encountered the following errors while provisioning:
|
|
|
+ </Description>
|
|
|
+ <ErrorWrapper>
|
|
|
+ {erroredResources.map((resource, index) => {
|
|
|
+ return (
|
|
|
+ <ExpandedError key={index}>{resource.error}</ExpandedError>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </ErrorWrapper>
|
|
|
+ </>
|
|
|
);
|
|
|
- });
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
- return <StyledProvisionerStatus>{renderModules()}</StyledProvisionerStatus>;
|
|
|
+ return (
|
|
|
+ <StyledCard>
|
|
|
+ {renderLoadingBar(
|
|
|
+ createdResources.length,
|
|
|
+ createdResources.length +
|
|
|
+ erroredResources.length +
|
|
|
+ plannedResources.length
|
|
|
+ )}
|
|
|
+ <Description>
|
|
|
+ {getOperationDescription(
|
|
|
+ operation.type,
|
|
|
+ operation.status,
|
|
|
+ operation.last_updated
|
|
|
+ )}
|
|
|
+ </Description>
|
|
|
+ {renderErrorSection()}
|
|
|
+ </StyledCard>
|
|
|
+ );
|
|
|
};
|
|
|
|
|
|
-export default ProvisionerStatus;
|
|
|
+const StyledCard = styled.div`
|
|
|
+ padding: 12px 20px;
|
|
|
+ max-height: 300px;
|
|
|
+ overflow-y: auto;
|
|
|
+`;
|
|
|
|
|
|
const Flex = styled.div`
|
|
|
display: flex;
|
|
|
@@ -182,7 +615,6 @@ const Icon = styled.img`
|
|
|
`;
|
|
|
|
|
|
const ErrorWrapper = styled.div`
|
|
|
- max-height: 150px;
|
|
|
margin-top: 20px;
|
|
|
overflow-y: auto;
|
|
|
user-select: text;
|
|
|
@@ -200,6 +632,27 @@ const ExpandedError = styled.div`
|
|
|
padding-bottom: 17px;
|
|
|
`;
|
|
|
|
|
|
+const StatusContainer = styled.div`
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ 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;
|
|
|
+ min-width: 100px;
|
|
|
+ text-align: right;
|
|
|
+ color: #aaaabb;
|
|
|
+`;
|
|
|
+
|
|
|
const movingGradient = keyframes`
|
|
|
0% {
|
|
|
background-position: left bottom;
|
|
|
@@ -210,74 +663,68 @@ const movingGradient = keyframes`
|
|
|
}
|
|
|
`;
|
|
|
|
|
|
-const LoadingFill = styled.div<{ width: string; status: string }>`
|
|
|
- width: ${(props) => props.width};
|
|
|
- background: ${(props) =>
|
|
|
- props.status === "successful"
|
|
|
- ? "rgb(56, 168, 138)"
|
|
|
- : props.status === "error"
|
|
|
- ? "#fcba03"
|
|
|
- : "linear-gradient(to right, #8ce1ff, #616FEE)"};
|
|
|
- height: 100%;
|
|
|
- background-size: 250% 100%;
|
|
|
- animation: ${movingGradient} 2s infinite;
|
|
|
- animation-timing-function: ease-in-out;
|
|
|
- animation-direction: alternate;
|
|
|
+const StyledProvisionerStatus = styled.div`
|
|
|
+ margin-top: 25px;
|
|
|
`;
|
|
|
|
|
|
-const StatusIcon = styled.div<{ successful?: boolean }>`
|
|
|
+const StyledInfraObject = styled.div`
|
|
|
+ background: #ffffff1a;
|
|
|
+ border: 1px solid #aaaabb;
|
|
|
+ border-radius: 5px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ position: relative;
|
|
|
+`;
|
|
|
+
|
|
|
+const InfraHeader = styled.div<{ is_clickable: boolean }>`
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 15px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- font-family: "Work Sans", sans-serif;
|
|
|
- font-size: 13px;
|
|
|
- color: #ffffff55;
|
|
|
- max-width: 500px;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
+ cursor: ${(props) => (props.is_clickable ? "pointer" : "default")};
|
|
|
+ height: 50px;
|
|
|
|
|
|
- > i {
|
|
|
- font-size: 18px;
|
|
|
- margin-right: 10px;
|
|
|
- float: left;
|
|
|
- color: ${(props) => (props.successful ? "rgb(56, 168, 138)" : "#fcba03")};
|
|
|
+ :hover {
|
|
|
+ background: ${(props) => (props.is_clickable ? "#ffffff12" : "none")};
|
|
|
}
|
|
|
-`;
|
|
|
|
|
|
-const LoadingGif = styled.img`
|
|
|
- width: 15px;
|
|
|
- height: 15px;
|
|
|
- margin-right: 9px;
|
|
|
- margin-bottom: 0px;
|
|
|
-`;
|
|
|
+ .expand-icon {
|
|
|
+ display: none;
|
|
|
+ color: #ffffff55;
|
|
|
+ }
|
|
|
|
|
|
-const StyledProvisionerStatus = styled.div`
|
|
|
- margin-top: 25px;
|
|
|
+ :hover .expand-icon {
|
|
|
+ display: inline-block;
|
|
|
+ }
|
|
|
`;
|
|
|
|
|
|
const LoadingBar = styled.div`
|
|
|
- width: calc(100% - 30px);
|
|
|
background: #ffffff22;
|
|
|
- border: 100px;
|
|
|
- margin: 15px 15px 0;
|
|
|
- height: 18px;
|
|
|
+ width: 100%;
|
|
|
+ height: 8px;
|
|
|
overflow: hidden;
|
|
|
border-radius: 100px;
|
|
|
`;
|
|
|
|
|
|
-const InfraObject = styled.div`
|
|
|
- background: #ffffff22;
|
|
|
- padding: 15px 0 0;
|
|
|
- border: 1px solid #aaaabb;
|
|
|
- border-radius: 5px;
|
|
|
- margin-bottom: 10px;
|
|
|
- position: relative;
|
|
|
+const LoadingFill = styled.div<{ width: string; status: string }>`
|
|
|
+ width: ${(props) => props.width};
|
|
|
+ background: ${(props) =>
|
|
|
+ props.status === "successful"
|
|
|
+ ? "rgb(56, 168, 138)"
|
|
|
+ : props.status === "error"
|
|
|
+ ? "#fcba03"
|
|
|
+ : "linear-gradient(to right, #8ce1ff, #616FEE)"};
|
|
|
+ height: 100%;
|
|
|
+ background-size: 250% 100%;
|
|
|
+ animation: ${movingGradient} 2s infinite;
|
|
|
+ animation-timing-function: ease-in-out;
|
|
|
+ animation-direction: alternate;
|
|
|
`;
|
|
|
|
|
|
-const InfraHeader = styled.div`
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 500;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 0 15px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
+const ExpandIconContainer = styled.div<{ hidden: boolean }>`
|
|
|
+ width: 30px;
|
|
|
+ margin-left: 10px;
|
|
|
+ padding-top: 2px;
|
|
|
+ display: ${(props) => (props.hidden ? "none" : "inline")};
|
|
|
`;
|