From 40cb1f6d3a738d915588ccdc2f94c71869bd6849 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 21 Aug 2026 17:07:56 -0500 Subject: [PATCH] split instanceDiskAttach mutation: attach modal owns its own, create-then-attach chain gets error toast --- app/forms/disk-attach.tsx | 43 +++++++++++++++++++++- app/pages/project/instances/StorageTab.tsx | 33 +++++++---------- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/app/forms/disk-attach.tsx b/app/forms/disk-attach.tsx index e4f8268b4c..75f319c66f 100644 --- a/app/forms/disk-attach.tsx +++ b/app/forms/disk-attach.tsx @@ -9,16 +9,55 @@ import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { useForm } from 'react-hook-form' -import { api, q, type ApiError, type DiskType } from '@oxide/api' +import { + api, + q, + queryClient, + useApiMutation, + type ApiError, + type DiskType, +} from '@oxide/api' import { ComboboxField } from '~/components/form/fields/ComboboxField' import { ModalForm } from '~/components/form/ModalForm' -import { useProjectSelector } from '~/hooks/use-params' +import { HL } from '~/components/HL' +import { useInstanceSelector, useProjectSelector } from '~/hooks/use-params' +import { addToast } from '~/stores/toast' import { toComboboxItems } from '~/ui/lib/Combobox' import { ALL_ISH } from '~/util/consts' const defaultValues = { name: '' } +/** + * Attach modal for the instance storage tab. Owns the attach mutation so its + * loading and error state can't outlive the modal. `AttachDiskModalForm` below + * stays mutation-free because the instance create form also uses it (with a + * setState `onSubmit`) on a route where no instance exists yet. + */ +export function AttachDiskModal({ onDismiss }: { onDismiss: () => void }) { + const { project, instance } = useInstanceSelector() + + const attachDisk = useApiMutation(api.instanceDiskAttach, { + onSuccess(disk) { + queryClient.invalidateEndpoint('instanceDiskList') + onDismiss() + // prettier-ignore + addToast(<>Disk {disk.name} attached) + }, + }) + + return ( + { + attachDisk.mutate({ path: { instance }, query: { project }, body: { disk: name } }) + }} + loading={attachDisk.isPending} + submitError={attachDisk.error} + /> + ) +} + type AttachDiskProps = { /** If defined, this overrides the usual mutation */ onSubmit: (diskAttach: { name: string; size: number; diskType: DiskType }) => void diff --git a/app/pages/project/instances/StorageTab.tsx b/app/pages/project/instances/StorageTab.tsx index 23922a4db3..516a485b6d 100644 --- a/app/pages/project/instances/StorageTab.tsx +++ b/app/pages/project/instances/StorageTab.tsx @@ -25,7 +25,7 @@ import { Storage24Icon } from '@oxide/design-system/icons/react' import { HL } from '~/components/HL' import { DiskStateBadge, DiskTypeBadge, ReadOnlyBadge } from '~/components/StateBadge' -import { AttachDiskModalForm } from '~/forms/disk-attach' +import { AttachDiskModal } from '~/forms/disk-attach' import { CreateDiskSideModalForm } from '~/forms/disk-create' import { getInstanceSelector, useInstanceSelector } from '~/hooks/use-params' import { useQuickActions } from '~/hooks/use-quick-actions' @@ -320,14 +320,22 @@ export default function StorageTab() { ] ) - const attachDisk = useApiMutation(api.instanceDiskAttach, { + // attach step of the create-then-attach flow only; the attach modal owns its + // own mutation. The create modal closes on create success, so this runs in + // the background and failures must surface as a toast. + const attachCreatedDisk = useApiMutation(api.instanceDiskAttach, { onSuccess(disk) { queryClient.invalidateEndpoint('instanceDiskList') - setShowDiskCreate(false) - setShowDiskAttach(false) // prettier-ignore addToast(<>Disk {disk.name} attached) }, + onError(err, variables) { + addToast({ + title: `Failed to attach disk ${variables.body.disk}`, + content: err.message, + variant: 'error', + }) + }, }) const bootDisksTable = useReactTable({ @@ -434,24 +442,11 @@ export default function StorageTab() { onSuccess={({ name }) => { // TODO: this should probably be done with `mutateAsync` and // awaited, but it's a pain, so punt for now - attachDisk.mutate({ ...instancePathQuery, body: { disk: name } }) - }} - /> - )} - {showDiskAttach && ( - { - setShowDiskAttach(false) - // clear API errors on the mutation - attachDisk.reset() - }} - onSubmit={({ name }) => { - attachDisk.mutate({ ...instancePathQuery, body: { disk: name } }) + attachCreatedDisk.mutate({ ...instancePathQuery, body: { disk: name } }) }} - loading={attachDisk.isPending} - submitError={attachDisk.error} /> )} + {showDiskAttach && setShowDiskAttach(false)} />} {selectedDisk && (