diff --git a/app/forms/disk-attach.tsx b/app/forms/disk-attach.tsx
index e4f8268b4..75f319c66 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 23922a4db..516a485b6 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 && (