diff --git a/src/features/instance/applications/DESIGN.md b/src/features/instance/applications/DESIGN.md index e58317fc0..b73a0b2c8 100644 --- a/src/features/instance/applications/DESIGN.md +++ b/src/features/instance/applications/DESIGN.md @@ -55,3 +55,15 @@ tell it from a gateway's. So [`onDeployComponentSubmit`](../../../integrations/a turns those into `SSEInconclusiveError`, which every deploy caller already answers by checking rather than resending. `streamOperation` stays neutral: it puts the `status` on `SSEUnsupportedError` and leaves the decision to the caller, because a read (`get_deployment`) may fall back on anything. + +## Who reports a failed deploy + +Every caller of [`useDeployComponentMutation`](../../../integrations/api/instance/applications/deployComponent.ts) +shows its own failure. The mutation opts out of the global error toast and reports the failure to RUM itself, with a +mutation-level `console.error` that still runs after the caller unmounts. A new caller has to render or toast the +rejection, or the person sees nothing. + +Callers await `mutateAsync` rather than reporting from `mutate()` callbacks, which React Query drops once the caller +unmounts. Following a rollout keeps a deploy pending for up to 30 minutes, so its outcome often lands after the form +or modal that started it is gone. A failure that lands while its progress panel or import form is still on screen is +shown there only; one that lands later goes to a toast, with the deployment record when the failure named one. diff --git a/src/features/instance/applications/components/DeployProgress/DeployFailure.tsx b/src/features/instance/applications/components/DeployProgress/DeployFailure.tsx new file mode 100644 index 000000000..3ae037dca --- /dev/null +++ b/src/features/instance/applications/components/DeployProgress/DeployFailure.tsx @@ -0,0 +1,53 @@ +import { deploymentRecordPath } from '@/features/instance/applications/hooks/useDeploymentRecordAction'; +import { buildAbsoluteLinkToPage } from '@/lib/urls/buildAbsoluteLinkToPage'; +import { Link, useParams } from '@tanstack/react-router'; +import { CircleIcon } from 'lucide-react'; + +// The instance's own SSH keys fix this, so the failure links to them. +const SSH_ACCESS_FAILURE = /Failed to deploy private repository\b|SSH access failed/i; + +export function DeployFailure({ + title, + message, + deploymentId, + onNavigateAway, +}: { + title?: string; + message: string; + /** Only where the host shows no record link of its own. */ + deploymentId?: string; + /** Called when a guidance link navigates away — lets a hosting modal close itself first. */ + onNavigateAway?: () => void; +}) { + const params: { clusterId?: string; instanceId?: string; organizationId?: string } = useParams({ strict: false }); + + return ( +
+
+ +
+ {title && {title}} + {message} +
+
+ {SSH_ACCESS_FAILURE.test(message) && ( + + Manage SSH keys + + )} + {deploymentId && ( + + View this deployment's record + + )} +
+ ); +} diff --git a/src/features/instance/applications/components/DeployProgress/DeployProgress.tsx b/src/features/instance/applications/components/DeployProgress/DeployProgress.tsx index bec4f3674..eacadf535 100644 --- a/src/features/instance/applications/components/DeployProgress/DeployProgress.tsx +++ b/src/features/instance/applications/components/DeployProgress/DeployProgress.tsx @@ -7,18 +7,10 @@ import { CertificationDecision } from '@/integrations/api/instance/applications/ import { RolloutNode, RolloutSnapshot } from '@/integrations/api/instance/applications/restartJob'; import { cn } from '@/lib/cn'; import { errorText } from '@/lib/errorText'; -import { buildAbsoluteLinkToPage } from '@/lib/urls/buildAbsoluteLinkToPage'; import { Link, useParams } from '@tanstack/react-router'; -import { - CheckIcon, - CircleDashedIcon, - CircleIcon, - Loader2Icon, - ShieldCheckIcon, - TriangleAlertIcon, - XIcon, -} from 'lucide-react'; +import { CheckIcon, CircleDashedIcon, Loader2Icon, ShieldCheckIcon, TriangleAlertIcon, XIcon } from 'lucide-react'; import { useEffect, useRef } from 'react'; +import { DeployFailure } from './DeployFailure'; import { describeCertification, describeDecision, @@ -36,16 +28,6 @@ const PHASE_LABELS: Record = { staged: 'Staged', }; -/** - * A deploy that failed because the *instance* couldn't reach a private repository over SSH is - * fixed in Config > SSH Keys (install a key, grant it access to the repo, add the git host to - * `known_hosts`), so link the user there instead of leaving them to find it. Harper's message - * already spells out the remediation, and this UI is now the only place the failure surfaces: - * it's an instance configuration state, not a Studio bug, so `shouldKeepEvent` drops the RUM - * event (which also kept the customer's private repo URL out of Error Tracking). - */ -const SSH_ACCESS_FAILURE = /Failed to deploy private repository\b|SSH access failed/i; - function PhaseIcon({ status, isCurrent }: { status?: PhaseStatus; isCurrent: boolean }) { if (status === 'done') { return ; @@ -164,21 +146,7 @@ export function DeployProgress({ {phasesFailed && state.decision && } {phasesFailed && state.error && !state.decision && ( -
-
- - {state.error} -
- {SSH_ACCESS_FAILURE.test(state.error) && ( - - Manage SSH keys - - )} -
+ )} {state.lifecycle === 'inconclusive' && !state.rollout && ( diff --git a/src/features/instance/applications/components/NewApplication/ImportInstructions.tsx b/src/features/instance/applications/components/NewApplication/ImportInstructions.tsx index a63381c2d..168e481ee 100644 --- a/src/features/instance/applications/components/NewApplication/ImportInstructions.tsx +++ b/src/features/instance/applications/components/NewApplication/ImportInstructions.tsx @@ -10,6 +10,7 @@ import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { DeployFailure } from '@/features/instance/applications/components/DeployProgress/DeployFailure'; import { useStagedDeploymentsAvailable } from '@/features/instance/applications/hooks/useSupportsDeploymentSSE'; import { GitMergeIcon, LinkIcon, PackageIcon, RocketIcon } from 'lucide-react'; import { useCallback } from 'react'; @@ -17,16 +18,19 @@ import { Control, FormState, UseFormSetValue, UseFormWatch } from 'react-hook-fo import { z } from 'zod'; import { ImportAuthorization } from './ImportAuthorization'; import { NewApplicationSchema } from './schema'; +import { ImportFailure } from './useImportApplication'; export function ImportInstructions({ control, formState, + importFailure, isImportingApplication, setValue, watch, }: { control: Control>; formState: FormState>; + importFailure?: ImportFailure; isImportingApplication: boolean; setValue: UseFormSetValue>; watch: UseFormWatch>; @@ -180,6 +184,14 @@ export function ImportInstructions({ + {importFailure && ( + + )} +