Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions app/forms/disk-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 (
<SideModalForm
Expand All @@ -123,8 +139,8 @@ export function CreateDiskSideModalForm({
name="size"
control={form.control}
validate={(diskSizeGiB: number) => {
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)`
}
}}
/>
Expand All @@ -144,6 +160,7 @@ const DiskSourceField = ({
const {
field: { value, onChange },
} = useController({ control, name: 'diskSource' })
const diskSizeField = useController({ control, name: 'size' }).field

return (
<>
Expand Down Expand Up @@ -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)
}
}}
/>
)}

Expand Down Expand Up @@ -218,6 +243,7 @@ const SnapshotSelectField = ({ control }: { control: Control<DiskCreate> }) => {
const snapshotsQuery = useApiQuery('snapshotList', { query: projectSelector })

const snapshots = snapshotsQuery.data?.items || []
const diskSizeField = useController({ control, name: 'size' }).field

return (
<ListboxField
Expand All @@ -226,22 +252,33 @@ const SnapshotSelectField = ({ control }: { control: Control<DiskCreate> }) => {
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: (
<>
<div>{i.name}</div>
<div className="text-secondary">
<div className="text-tertiary selected:text-accent-secondary">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sneaking in a fix for the selected state not rendering correctly.

Created on {format(i.timeCreated, 'MMM d, yyyy')}
<DiskNameFromId disk={i.diskId} />
<DiskNameFromId disk={i.diskId} />{' '}
<span className="mx-1 text-quinary selected:text-accent-disabled">/</span>{' '}
{formattedSize.value} {formattedSize.unit}
</div>
</>
),
}
})}
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)
}
}}
/>
)
}
13 changes: 12 additions & 1 deletion mock-api/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,24 @@ export const snapshots: Json<Snapshot>[] = [
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<Snapshot> {
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(),
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/snapshots.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down