-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix auto-updating of boot disk size when selected image is larger than current disk size; add tests #1829
Fix auto-updating of boot disk size when selected image is larger than current disk size; add tests #1829
Changes from 1 commit
c250f54
107d226
4f2bec0
6885220
74f5a22
ca5abc2
093d59a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,6 @@ import { | |
genName, | ||
INSTANCE_MAX_CPU, | ||
INSTANCE_MAX_RAM_GiB, | ||
MAX_DISK_SIZE_GiB, | ||
useApiMutation, | ||
useApiQueryClient, | ||
usePrefetchedApiQuery, | ||
|
@@ -140,6 +139,7 @@ export function CreateInstanceForm() { | |
|
||
return ( | ||
<FullPageForm | ||
submitDisabled={allImages.length ? undefined : 'Image required'} | ||
id="create-instance-form" | ||
form={form} | ||
title="Create instance" | ||
|
@@ -151,9 +151,10 @@ export function CreateInstanceForm() { | |
values.presetId === 'custom' | ||
? { memory: values.memory, ncpus: values.ncpus } | ||
: { memory: preset.memory, ncpus: preset.ncpus } | ||
|
||
// we should also never have an image ID that's not in the list | ||
const image = allImages.find((i) => values.image === i.id) | ||
// There should always be an image present, because … | ||
// - The form is disabled unless there are images available. | ||
// - The form defaults to including at least one image. | ||
invariant(image, 'Expected image to be defined') | ||
|
||
const bootDiskName = values.bootDiskName || genName(values.name, image.name) | ||
|
@@ -287,37 +288,51 @@ export function CreateInstanceForm() { | |
<FormDivider /> | ||
|
||
<Form.Heading id="boot-disk">Boot disk</Form.Heading> | ||
<Tabs.Root id="boot-disk-tabs" className="full-width" defaultValue="project"> | ||
<Tabs.Root | ||
id="boot-disk-tabs" | ||
className="full-width" | ||
// default to the project images tab if there are only project images | ||
defaultValue={ | ||
projectImages.length > 0 && siloImages.length === 0 ? 'project' : 'silo' | ||
} | ||
Comment on lines
+295
to
+298
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a nice touch 👍 |
||
> | ||
<Tabs.List aria-describedby="boot-disk"> | ||
<Tabs.Trigger value="project">Project images</Tabs.Trigger> | ||
<Tabs.Trigger value="silo">Silo images</Tabs.Trigger> | ||
<Tabs.Trigger value="project">Project images</Tabs.Trigger> | ||
</Tabs.List> | ||
<Tabs.Content value="project" className="space-y-4"> | ||
{projectImages.length === 0 ? ( | ||
{allImages.length === 0 && ( | ||
<Message | ||
className="mb-8 ml-10 max-w-lg" | ||
variant="notice" | ||
content="Images are required to create a boot disk." | ||
/> | ||
)} | ||
<Tabs.Content value="silo" className="space-y-4"> | ||
{siloImages.length === 0 ? ( | ||
<div className="flex max-w-lg items-center justify-center rounded-lg border p-6 border-default"> | ||
<EmptyMessage | ||
icon={<Images16Icon />} | ||
title="No project images found" | ||
body="An image needs to be uploaded to be seen here" | ||
buttonText="Upload image" | ||
onClick={() => navigate(pb.projectImageNew(projectSelector))} | ||
title="No silo images found" | ||
body="Project images need to be promoted to be seen here" | ||
/> | ||
</div> | ||
) : ( | ||
<ImageSelectField images={projectImages} control={control} /> | ||
<ImageSelectField images={siloImages} control={control} /> | ||
)} | ||
</Tabs.Content> | ||
<Tabs.Content value="silo" className="space-y-4"> | ||
{siloImages.length === 0 ? ( | ||
<Tabs.Content value="project" className="space-y-4"> | ||
{projectImages.length === 0 ? ( | ||
<div className="flex max-w-lg items-center justify-center rounded-lg border p-6 border-default"> | ||
<EmptyMessage | ||
icon={<Images16Icon />} | ||
title="No silo images found" | ||
body="Project images need to be promoted to be seen here" | ||
title="No project images found" | ||
body="An image needs to be uploaded to be seen here" | ||
buttonText="Upload image" | ||
onClick={() => navigate(pb.projectImageNew(projectSelector))} | ||
/> | ||
</div> | ||
) : ( | ||
<ImageSelectField images={siloImages} control={control} /> | ||
<ImageSelectField images={projectImages} control={control} /> | ||
)} | ||
</Tabs.Content> | ||
</Tabs.Root> | ||
|
@@ -328,17 +343,7 @@ export function CreateInstanceForm() { | |
label="Disk size" | ||
name="bootDiskSize" | ||
control={control} | ||
// Imitate API logic: only require that the disk is big enough to fit the image | ||
validate={(diskSizeGiB) => { | ||
if (!image) return true | ||
if (diskSizeGiB < image.size / GiB) { | ||
const minSize = Math.ceil(image.size / GiB) | ||
return `Must be as large as selected image (min. ${minSize} GiB)` | ||
} | ||
if (diskSizeGiB > MAX_DISK_SIZE_GiB) { | ||
return `Can be at most ${MAX_DISK_SIZE_GiB} GiB` | ||
} | ||
}} | ||
imageSize={image?.size ? Math.ceil(image.size / GiB) : undefined} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not quite sure that we should should push the validation logic down into the We'll either continue pushing more context down into the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, even though the logic is currently the same between the two spots, we could model it as a shared There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refactored this after chatting with @david-crespo … several of the validations are standard and will be needed for the DiskSizeField regardless of where we use it. But we also want to have the option of adding one-off validators. So we added an optional |
||
/> | ||
<NameField | ||
name="bootDiskName" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -128,6 +128,43 @@ test('can create an instance with custom hardware', async ({ page }) => { | |
]) | ||
}) | ||
|
||
test('automatically updates disk size when larger image selected', async ({ page }) => { | ||
await page.goto('/projects/mock-project/instances-new') | ||
|
||
const instanceName = 'my-new-instance' | ||
await page.fill('input[name=name]', instanceName) | ||
|
||
// set the disk size larger than it needs to be, to verify it doesn't get reduced | ||
const diskSizeInput = page.getByRole('textbox', { name: 'Disk size (GiB)' }) | ||
await diskSizeInput.fill('5') | ||
|
||
// pick a disk image that's smaller than 5GiB (the first project image works [4GiB]) | ||
await page.getByRole('tab', { name: 'Project images' }).click() | ||
await page.getByRole('button', { name: 'Image' }).click() | ||
await page.getByRole('option', { name: images[0].name }).click() | ||
|
||
// test that it still says 5, as that's larger than the given image | ||
await expect(diskSizeInput).toHaveValue('5') | ||
|
||
// pick a disk image that's larger than 5GiB (the third project image works [6GiB]) | ||
await page.getByRole('button', { name: 'Image' }).click() | ||
await page.getByRole('option', { name: images[2].name }).click() | ||
|
||
// test that it has been automatically increased to next-largest incremement of 10 | ||
await expect(diskSizeInput).toHaveValue('10') | ||
|
||
// pick another image, just to verify that the diskSizeInput stays as it was | ||
await page.getByRole('button', { name: 'Image' }).click() | ||
await page.getByRole('option', { name: images[1].name }).click() | ||
await expect(diskSizeInput).toHaveValue('10') | ||
|
||
const submitButton = page.getByRole('button', { name: 'Create instance' }) | ||
await submitButton.click() | ||
|
||
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`) | ||
await expectVisible(page, [`h1:has-text("${instanceName}")`, 'text=10 GiB']) | ||
}) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Love the test! |
||
test('with disk name already taken', async ({ page }) => { | ||
await page.goto('/projects/mock-project/instances-new') | ||
await page.fill('input[name=name]', 'my-instance') | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing we could do here is just update the
min
input threshold. We'd probably want to include an info icon with some details to indicate why the user can't go below the threshold though.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that’s why we went with two thresholds, the message saying it’s due to the image is pretty helpful. See also #1824