From ba5ce015dc75b0c1c32880069cac063cb7066899 Mon Sep 17 00:00:00 2001 From: Dawson Toth Date: Sun, 11 Oct 2026 02:25:23 -0400 Subject: [PATCH 1/5] fix(applications): show a failed deploy once, and keep a failed import beside its form A failed deploy showed the same failure in two toasts: the deploy mutation's global one, and the caller's own. The import form's only failure display was a toast that faded, for failures the user has to act on and retry. The deploy mutation now opts out of the global toast and keeps its RUM report with a mutation-level console.error. The import form keeps the failure inline above the Import button until the next attempt, with the SSH-keys link the redeploy panel already offered. Redeploy shows a streamed failure in its progress panel, toasting only when the panel is gone, and the buffered path, which relied on the global toast, now says why it failed. Both callers await mutateAsync so a failure that settles after the form unmounts still reaches the user, and every failure display reads the server's reason through describeError rather than axios's generic message. Closes #1833 Co-Authored-By: Claude Opus 5.5 --- .../DeployProgress/DeployFailure.tsx | 48 ++++ .../DeployProgress/DeployProgress.tsx | 38 +-- .../NewApplication/ImportInstructions.tsx | 12 + .../NewApplication/NewApplication.test.tsx | 43 +++- .../components/NewApplication/index.tsx | 3 +- .../useImportApplication.test.ts | 151 ++++++++++++ .../NewApplication/useImportApplication.ts | 221 ++++++++++-------- .../modals/RedeployApplicationModal.test.tsx | 155 ++++++++++++ .../modals/RedeployApplicationModal.tsx | 219 +++++++++-------- .../applications/deployComponent.test.ts | 73 ++++-- .../instance/applications/deployComponent.ts | 6 +- 11 files changed, 709 insertions(+), 260 deletions(-) create mode 100644 src/features/instance/applications/components/DeployProgress/DeployFailure.tsx create mode 100644 src/features/instance/applications/components/NewApplication/useImportApplication.test.ts create mode 100644 src/features/instance/applications/modals/RedeployApplicationModal.test.tsx 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..76c0e2942 --- /dev/null +++ b/src/features/instance/applications/components/DeployProgress/DeployFailure.tsx @@ -0,0 +1,48 @@ +import { buildAbsoluteLinkToPage } from '@/lib/urls/buildAbsoluteLinkToPage'; +import { Link, useParams } from '@tanstack/react-router'; +import { CircleIcon } from 'lucide-react'; + +/** + * 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 only the deploy UI reports the failure: 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; + +/** A failed deploy's message, persistent beside the form that started it. */ +export function DeployFailure({ + title, + message, + onNavigateAway, +}: { + title?: string; + message: 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 + + )} +
+ ); +} 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 && ( + + )} +