diff --git a/app/forms/disk-create.tsx b/app/forms/disk-create.tsx index 06e55ef27c..cf3122f3f8 100644 --- a/app/forms/disk-create.tsx +++ b/app/forms/disk-create.tsx @@ -6,6 +6,7 @@ * Copyright Oxide Computer Company */ import { format } from 'date-fns' +import { filesize } from 'filesize' import { useMemo } from 'react' import { useController, type Control } from 'react-hook-form' import { useNavigate, type NavigateFunction } from 'react-router-dom' @@ -94,9 +95,24 @@ export function CreateDiskSideModalForm({ ) const areImagesLoading = projectImages.isPending || siloImages.isPending - const selectedImageId = form.watch('diskSource.imageId') - const selectedImageSize = images.find((image) => image.id === selectedImageId)?.size - const imageSizeGiB = selectedImageSize ? bytesToGiB(selectedImageSize) : undefined + const snapshotsQuery = useApiQuery('snapshotList', { query: projectSelector }) + const snapshots = snapshotsQuery.data?.items || [] + + // validate disk source size + const diskSource = form.watch('diskSource').type + + let validateSizeGiB: number | undefined = undefined + if (diskSource === 'snapshot') { + const selectedSnapshotId = form.watch('diskSource.snapshotId') + const selectedSnapshotSize = snapshots.find( + (snapshot) => snapshot.id === selectedSnapshotId + )?.size + validateSizeGiB = selectedSnapshotSize ? bytesToGiB(selectedSnapshotSize) : undefined + } else if (diskSource === 'image') { + const selectedImageId = form.watch('diskSource.imageId') + const selectedImageSize = images.find((image) => image.id === selectedImageId)?.size + validateSizeGiB = selectedImageSize ? bytesToGiB(selectedImageSize) : undefined + } return ( { - if (imageSizeGiB && diskSizeGiB < imageSizeGiB) { - return `Must be as large as selected image (min. ${imageSizeGiB} GiB)` + if (validateSizeGiB && diskSizeGiB < validateSizeGiB) { + return `Must be as large as selected ${diskSource} (min. ${validateSizeGiB} GiB)` } }} /> @@ -144,6 +160,7 @@ const DiskSourceField = ({ const { field: { value, onChange }, } = useController({ control, name: 'diskSource' }) + const diskSizeField = useController({ control, name: 'size' }).field return ( <> @@ -191,6 +208,14 @@ const DiskSourceField = ({ isLoading={areImagesLoading} items={images.map((i) => toListboxItem(i, true))} required + onChange={(id) => { + const image = images.find((i) => i.id === id)! // if it's selected, it must be present + const imageSizeGiB = image.size / GiB + if (diskSizeField.value < imageSizeGiB) { + const nearest10 = Math.ceil(imageSizeGiB / 10) * 10 + diskSizeField.onChange(nearest10) + } + }} /> )} @@ -218,6 +243,7 @@ const SnapshotSelectField = ({ control }: { control: Control }) => { const snapshotsQuery = useApiQuery('snapshotList', { query: projectSelector }) const snapshots = snapshotsQuery.data?.items || [] + const diskSizeField = useController({ control, name: 'size' }).field return ( }) => { label="Source snapshot" placeholder="Select a snapshot" items={snapshots.map((i) => { + const formattedSize = filesize(i.size, { base: 2, output: 'object' }) return { value: i.id, labelString: `${i.name}`, label: ( <>
{i.name}
-
+
Created on {format(i.timeCreated, 'MMM d, yyyy')} - + {' '} + /{' '} + {formattedSize.value} {formattedSize.unit}
), @@ -242,6 +271,14 @@ const SnapshotSelectField = ({ control }: { control: Control }) => { })} isLoading={snapshotsQuery.isPending} required + onChange={(id) => { + const snapshot = snapshots.find((i) => i.id === id)! // if it's selected, it must be present + const snapshotSizeGiB = snapshot.size / GiB + if (diskSizeField.value < snapshotSizeGiB) { + const nearest10 = Math.ceil(snapshotSizeGiB / 10) * 10 + diskSizeField.onChange(nearest10) + } + }} /> ) } diff --git a/mock-api/snapshot.ts b/mock-api/snapshot.ts index 56cf8e73db..4a70380f9a 100644 --- a/mock-api/snapshot.ts +++ b/mock-api/snapshot.ts @@ -74,13 +74,24 @@ export const snapshots: Json[] = [ disk_id: 'a6f61e3f-25c1-49b0-a013-ac6a2d98a948', state: 'ready', }, + { + id: '7fc6ca11-452e-d3e4-9e1c-752ff615abea', + name: 'snapshot-heavy', + description: '', + project_id: project.id, + time_created: new Date().toISOString(), + time_modified: new Date().toISOString(), + size: 1024 * 1024 * 1024 * 20, + disk_id: disks[3].id, + state: 'ready', + }, ...generatedSnapshots, ] function generateSnapshot(index: number): Json { return { id: uuid(), - name: `disk-1-snapshot-${index + 5}`, + name: `disk-1-snapshot-${index + 6}`, description: '', project_id: project.id, time_created: new Date().toISOString(), diff --git a/test/e2e/snapshots.e2e.ts b/test/e2e/snapshots.e2e.ts index 3d2c5e32a1..461f832628 100644 --- a/test/e2e/snapshots.e2e.ts +++ b/test/e2e/snapshots.e2e.ts @@ -28,7 +28,7 @@ test('Click through snapshots', async ({ page }) => { test('Confirm delete snapshot', async ({ page }) => { await page.goto('/projects/mock-project/snapshots') - const row = page.getByRole('row', { name: 'disk-1-snapshot-5' }) + const row = page.getByRole('row', { name: 'disk-1-snapshot-6' }) async function clickDelete() { await row.getByRole('button', { name: 'Row actions' }).click()