diff --git a/dashboard/src/lib/hooks/useCluster.ts b/dashboard/src/lib/hooks/useCluster.ts index 0eaa54c77f..538b9695ac 100644 --- a/dashboard/src/lib/hooks/useCluster.ts +++ b/dashboard/src/lib/hooks/useCluster.ts @@ -337,7 +337,8 @@ export const useClusterState = ({ type TUseUpdateCluster = { updateCluster: ( clientContract: ClientClusterContract, - baseContract: Contract + baseContract: Contract, + skipPreflightChecks?: boolean ) => Promise; isHandlingPreflightChecks: boolean; isCreatingContract: boolean; @@ -353,7 +354,8 @@ export const useUpdateCluster = ({ const updateCluster = async ( clientContract: ClientClusterContract, - baseContract: Contract + baseContract: Contract, + skipPreflightChecks: boolean = false ): Promise => { if (!projectId) { throw new Error("Project ID is missing"); @@ -369,88 +371,90 @@ export const useUpdateCluster = ({ ), }); - setIsHandlingPreflightChecks(true); - try { - let preflightCheckResp; - if ( - clientContract.cluster.cloudProvider === "AWS" || - clientContract.cluster.cloudProvider === "Azure" - ) { - preflightCheckResp = await api.cloudContractPreflightCheck( - "", - newContract, - { - project_id: projectId, - } - ); - } else { - preflightCheckResp = await api.legacyPreflightCheck( - "", - new PreflightCheckRequest({ - contract: newContract, - }), - { - id: projectId, - } - ); - } + if (!skipPreflightChecks) { + setIsHandlingPreflightChecks(true); + try { + let preflightCheckResp; + if ( + clientContract.cluster.cloudProvider === "AWS" || + clientContract.cluster.cloudProvider === "Azure" + ) { + preflightCheckResp = await api.cloudContractPreflightCheck( + "", + newContract, + { + project_id: projectId, + } + ); + } else { + preflightCheckResp = await api.legacyPreflightCheck( + "", + new PreflightCheckRequest({ + contract: newContract, + }), + { + id: projectId, + } + ); + } - const parsed = await preflightCheckValidator.parseAsync( - preflightCheckResp.data - ); + const parsed = await preflightCheckValidator.parseAsync( + preflightCheckResp.data + ); - if (parsed.errors.length > 0) { - const cloudProviderSpecificChecks = match( - clientContract.cluster.cloudProvider - ) - .with("AWS", () => CloudProviderAWS.preflightChecks) - .with("GCP", () => CloudProviderGCP.preflightChecks) - .with("Azure", () => CloudProviderAzure.preflightChecks) - .otherwise(() => []); - - const clientPreflightChecks: ClientPreflightCheck[] = parsed.errors - .map((e) => { - const preflightCheckMatch = cloudProviderSpecificChecks.find( - (cloudProviderCheck) => e.name === cloudProviderCheck.name - ); - if (!preflightCheckMatch) { + if (parsed.errors.length > 0) { + const cloudProviderSpecificChecks = match( + clientContract.cluster.cloudProvider + ) + .with("AWS", () => CloudProviderAWS.preflightChecks) + .with("GCP", () => CloudProviderGCP.preflightChecks) + .with("Azure", () => CloudProviderAzure.preflightChecks) + .otherwise(() => []); + + const clientPreflightChecks: ClientPreflightCheck[] = parsed.errors + .map((e) => { + const preflightCheckMatch = cloudProviderSpecificChecks.find( + (cloudProviderCheck) => e.name === cloudProviderCheck.name + ); + if (!preflightCheckMatch) { + return { + title: "Unknown preflight check", + status: "failure" as const, + error: { + detail: + "Your cloud provider returned an unknown error. Please reach out to Porter support.", + metadata: {}, + }, + }; + } return { - title: "Unknown preflight check", + title: preflightCheckMatch.displayName, status: "failure" as const, error: { - detail: - "Your cloud provider returned an unknown error. Please reach out to Porter support.", - metadata: {}, + detail: e.error.message, + metadata: e.error.metadata, + resolution: preflightCheckMatch.resolution, }, }; - } - return { - title: preflightCheckMatch.displayName, - status: "failure" as const, - error: { - detail: e.error.message, - metadata: e.error.metadata, - resolution: preflightCheckMatch.resolution, - }, - }; - }) - .filter(valueExists); + }) + .filter(valueExists); - return { - preflightChecks: clientPreflightChecks, - }; + return { + preflightChecks: clientPreflightChecks, + }; + } + // otherwise, continue to create the contract + } catch (err) { + throw new Error( + getErrorMessageFromNetworkCall( + err, + "Cluster preflight checks", + preflightCheckErrorReplacements + ) + ); + } finally { + setIsHandlingPreflightChecks(false); } - // otherwise, continue to create the contract - } catch (err) { - throw new Error( - getErrorMessageFromNetworkCall( - err, - "Cluster preflight checks", - preflightCheckErrorReplacements - ) - ); - } finally { - setIsHandlingPreflightChecks(false); } setIsCreatingContract(true); diff --git a/dashboard/src/main/home/infrastructure-dashboard/ClusterFormContextProvider.tsx b/dashboard/src/main/home/infrastructure-dashboard/ClusterFormContextProvider.tsx index 7d6bdafe5d..5fa2f7f295 100644 --- a/dashboard/src/main/home/infrastructure-dashboard/ClusterFormContextProvider.tsx +++ b/dashboard/src/main/home/infrastructure-dashboard/ClusterFormContextProvider.tsx @@ -33,6 +33,7 @@ type ClusterFormContextType = { showFailedPreflightChecksModal: boolean; updateClusterButtonProps: UpdateClusterButtonProps; setCurrentContract: (contract: Contract) => void; + submitSkippingPreflightChecks: () => Promise; }; const ClusterFormContext = createContext(null); @@ -133,14 +134,21 @@ const ClusterFormContextProvider: React.FC = ({ errors, ]); - const onSubmit = handleSubmit(async (data) => { + const handleClusterUpdate = async ( + data: ClientClusterContract, + skipPreflightChecks?: boolean + ): Promise => { setUpdateClusterResponse(undefined); setUpdateClusterError(""); if (!currentContract?.cluster || !projectId) { return; } try { - const response = await updateCluster(data, currentContract); + const response = await updateCluster( + data, + currentContract, + skipPreflightChecks + ); setUpdateClusterResponse(response); if (response.preflightChecks) { void reportToAnalytics({ @@ -194,8 +202,25 @@ const ClusterFormContextProvider: React.FC = ({ }); } } + }; + + const onSubmit = handleSubmit(async (data) => { + await handleClusterUpdate(data); }); + const submitSkippingPreflightChecks = async (): Promise => { + if (clusterForm.formState.isSubmitting) { + return; + } + if (!currentContract?.cluster) { + return; + } + const fullValuesWithDefaults = clusterContractValidator.parse( + clusterForm.getValues() + ); + await handleClusterUpdate(fullValuesWithDefaults, true); + }; + return ( = ({ updateClusterButtonProps, isAdvancedSettingsEnabled, isMultiClusterEnabled, + submitSkippingPreflightChecks, }} > diff --git a/dashboard/src/main/home/infrastructure-dashboard/modals/PreflightChecksModal.tsx b/dashboard/src/main/home/infrastructure-dashboard/modals/PreflightChecksModal.tsx index 8a10737636..aa9ae345bf 100644 --- a/dashboard/src/main/home/infrastructure-dashboard/modals/PreflightChecksModal.tsx +++ b/dashboard/src/main/home/infrastructure-dashboard/modals/PreflightChecksModal.tsx @@ -1,8 +1,10 @@ -import React from "react"; +import React, { useContext } from "react"; import styled from "styled-components"; import { match } from "ts-pattern"; import Loading from "components/Loading"; +import Button from "components/porter/Button"; +import Container from "components/porter/Container"; import { Error as ErrorComponent } from "components/porter/Error"; import Expandable from "components/porter/Expandable"; import Modal from "components/porter/Modal"; @@ -12,6 +14,9 @@ import StatusDot from "components/porter/StatusDot"; import Text from "components/porter/Text"; import { type ClientPreflightCheck } from "lib/clusters/types"; +import { Context } from "shared/Context"; + +import { useClusterFormContext } from "../ClusterFormContextProvider"; import ResolutionStepsModalContents from "./help/preflight/ResolutionStepsModalContents"; type ItemProps = { @@ -83,6 +88,9 @@ const PreflightChecksModal: React.FC = ({ onClose, preflightChecks, }) => { + const { user } = useContext(Context); + const { submitSkippingPreflightChecks } = useClusterFormContext(); + return ( @@ -104,11 +112,25 @@ const PreflightChecksModal: React.FC = ({ ))} - - Talk to support - + + + Talk to support + + {user.email?.endsWith("@porter.run") && ( + <> + + + )} + );