-
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
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Worth noting that we do want to support more cases (like creating boot disks from snapshots or existing disks) eventually. See the designs here: https://www.figma.com/file/n8tWb6BCJsSQ3SXdvi7ZeG/Instances?type=design&node-id=2936-69630&mode=design&t=M5GLVRDZUIX5FO8b-4 (I'd defer to @benjaminleonard or @paryhin if updates are required to those). I think changing the orders of the tabs is fine (though we may want to just collapse those down into a single interface at some point). |
// default to the project images tab if there are only project images | ||
defaultValue={ | ||
projectImages.length > 0 && siloImages.length === 0 ? 'project' : 'silo' | ||
} |
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.
This is a nice touch 👍
if (imageSize && diskSizeGiB < imageSize) { | ||
return `Must be as large as selected image (min. ${imageSize} GiB)` | ||
} |
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
app/forms/instance-create.tsx
Outdated
// 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 comment
The 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 DiskSizeField
component. Disks, as a concept, aren't strictly speaking tied to images. Certainly in this case the disk size is semi-dependent on the image size, but that doesn't impact other situations in which a disk size may be specified.
We'll either continue pushing more context down into the DiskSizeField
component or have to find some other way of composing the logic. I think my preference, for now, is to localize the validation logic to where it's used (similar to what we had here). If we need that logic to be more portable perhaps we could just have some reusable helper we could spread into the usage?
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, even though the logic is currently the same between the two spots, we could model it as a shared validate
function that’s passed in in both spots.
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.
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 validate
prop that we can pass in when calling the component; any validations passed in via that prop will get called after the standard validator functions are evaluated.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Love the test!
…with-image-change
…with-image-change
… optionally receive additional validators
return `Can be at most ${MAX_DISK_SIZE_GiB} GiB` | ||
validate={(diskSizeGiB: number) => { | ||
if (imageSize && diskSizeGiB < imageSize) { | ||
return `Must be as large as selected image (min. ${imageSize} GiB)` |
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.
I thought we had also added imageSize
to the DiskSizeField
on the disk create form last week, but it seems we did not. You could do it in this PR since it should only be couple of lines. A slightly tricky bit in that one is that there isn't always an image (and unlike here, that's not a pathological case, because you might not be creating the disk from an image).
@@ -17,12 +22,19 @@ interface DiskSizeProps< | |||
TName extends FieldPath<TFieldValues>, | |||
> extends TextFieldProps<TFieldValues, TName> { | |||
minSize?: number | |||
validate?(diskSizeGiB: number): ValidateResult |
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.
This is more of a FYI about React Hook form, not a suggestion, because I can't bring myself to suggest something so much worse than what you have, and I don't think it's really more typesafe in practice. But if you hover on the validate prop, you can see it is using some helpers from RHF to look up the value of the field using the field name.
If you wanted to match that type on the prop, you could do this:
validate?: Validate<PathValue<TFieldValues, TName>, TFieldValues>
This produces the same result as what you have (the argument is a number, of course) except it also has a second argument, which is all the form values together:
This could be handy in theory because you could validate one field value based on the value of another. But we're not doing that — well, we're doing it, but we're doing it at the call site so this field doesn't have to know about it. So we don't need it, and the resulting code is dramatically harder to understand, plus the second arg is required (lol) so it actually gets mad at you for leaving it out.
All this is to say, I've already typed this so I might as well shoot it into the ether, but it is not a suggestion to use this horrible type.
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.
Ah, that's good to know, about, though! Thanks for writing that all out.
…ze > selected image size
app/forms/disk-create.tsx
Outdated
{value.type === 'image' && ( | ||
<ImageSelectField | ||
control={control} | ||
images={images} | ||
areImagesLoading={areImagesLoading} | ||
/> | ||
)} |
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.
I'd inline this here and save a bunch of lines. It made more sense as a component when it was responsible for fetching the images. I think the idea there was you could avoid fetching them until the image picker was on screen, but we can't do that anymore since we need the size at top level.
{value.type === 'image' && ( | |
<ImageSelectField | |
control={control} | |
images={images} | |
areImagesLoading={areImagesLoading} | |
/> | |
)} | |
{value.type === 'image' && ( | |
<ListboxField | |
control={control} | |
name="diskSource.imageId" | |
label="Source image" | |
placeholder="Select an image" | |
isLoading={areImagesLoading} | |
items={images.map((i) => toListboxItem(i, true))} | |
required | |
/> | |
)} |
This also seems to me an ok spot to do the auto size update thing we're doing in the instance create ImageSelectField
. I think you can use setValue
instead of the controller business in the other one, so pretty nice all around. You have to pass form
instead of control
though to get access to those. Probably better than passing the methods individually. Here is something that seems to work, though it's not ideal — see for example the spot where I'm duplicating the image size lookup.
diff --git a/app/forms/disk-create.tsx b/app/forms/disk-create.tsx
index 4d413aa9..1699991e 100644
--- a/app/forms/disk-create.tsx
+++ b/app/forms/disk-create.tsx
@@ -6,7 +6,7 @@
* Copyright Oxide Computer Company
*/
import { format } from 'date-fns'
-import { useController, type Control } from 'react-hook-form'
+import { useController, type Control, type UseFormReturn } from 'react-hook-form'
import { useNavigate, type NavigateFunction } from 'react-router-dom'
import {
@@ -110,11 +110,7 @@ export function CreateDiskSideModalForm({
<NameField name="name" control={control} />
<DescriptionField name="description" control={control} />
<FormDivider />
- <DiskSourceField
- control={control}
- images={images}
- areImagesLoading={areImagesLoading}
- />
+ <DiskSourceField form={form} images={images} areImagesLoading={areImagesLoading} />
<DiskSizeField
name="size"
control={control}
@@ -129,17 +125,17 @@ export function CreateDiskSideModalForm({
}
const DiskSourceField = ({
- control,
+ form,
images,
areImagesLoading,
}: {
- control: Control<DiskCreate>
+ form: UseFormReturn<DiskCreate>
images: Image[]
areImagesLoading: boolean
}) => {
const {
field: { value, onChange },
- } = useController({ control, name: 'diskSource' })
+ } = useController({ control: form.control, name: 'diskSource' })
return (
<>
@@ -169,7 +165,7 @@ const DiskSourceField = ({
name="diskSource.blockSize"
label="Block size"
units="Bytes"
- control={control}
+ control={form.control}
parseValue={(val) => parseInt(val, 10) as BlockSize}
items={[
{ label: '512', value: 512 },
@@ -179,41 +175,32 @@ const DiskSourceField = ({
/>
)}
{value.type === 'image' && (
- <ImageSelectField
- control={control}
- images={images}
- areImagesLoading={areImagesLoading}
+ <ListboxField
+ control={form.control}
+ name="diskSource.imageId"
+ label="Source image"
+ placeholder="Select an image"
+ isLoading={areImagesLoading}
+ items={images.map((i) => toListboxItem(i, true))}
+ required
+ onChange={(id) => {
+ const diskSizeGiB = form.getValues('size')
+ const size = images.find((image) => image.id === id)?.size
+ const imageSizeGiB = size ? bytesToGiB(size) : undefined
+ if (imageSizeGiB && diskSizeGiB < imageSizeGiB) {
+ const nearest10 = Math.ceil(imageSizeGiB / 10) * 10
+ form.setValue('size', nearest10)
+ }
+ }}
/>
)}
- {value.type === 'snapshot' && <SnapshotSelectField control={control} />}
+ {value.type === 'snapshot' && <SnapshotSelectField control={form.control} />}
</div>
</>
)
}
-const ImageSelectField = ({
- control,
- images,
- areImagesLoading,
-}: {
- control: Control<DiskCreate>
- images: Image[]
- areImagesLoading: boolean
-}) => {
- return (
- <ListboxField
- control={control}
- name="diskSource.imageId"
- label="Source image"
- placeholder="Select an image"
- isLoading={areImagesLoading}
- items={images.map((i) => toListboxItem(i, true))}
- required
- />
- )
-}
-
const DiskNameFromId = ({ disk }: { disk: string }) => {
const { data, isPending, isError } = useApiQuery(
'diskView',
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.
I think you could do this in a followup since it's gnarly!
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, I think adding in a second PR will make it easier to focus in on the specific issue. Thank you for the proof of concept. Will iterate off of that in another branch.
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.
I think it's good!
oxidecomputer/console@ae8218d...1802c28 * [1802c285](oxidecomputer/console@1802c285) oxidecomputer/console#1839 * [ce09b547](oxidecomputer/console@ce09b547) bump postcss-pseudo-classes for fake vuln * [e09b803b](oxidecomputer/console@e09b803b) might as well get vitest 1.0 in there too * [83dd73ee](oxidecomputer/console@83dd73ee) minor bumps for react router, msw, vite, tailwind, recharts * [6ae6beeb](oxidecomputer/console@6ae6beeb) oxidecomputer/console#1829 * [a0bf47aa](oxidecomputer/console@a0bf47aa) oxidecomputer/console#1836 * [6c9420ad](oxidecomputer/console@6c9420ad) oxidecomputer/console#1835 * [64e97b01](oxidecomputer/console@64e97b01) api-diff also takes a commit * [22bef0bb](oxidecomputer/console@22bef0bb) oxidecomputer/console#1833 * [2fe50f51](oxidecomputer/console@2fe50f51) oxidecomputer/console#1810 * [faadb6d3](oxidecomputer/console@faadb6d3) oxidecomputer/console#1832 * [9e82f9ab](oxidecomputer/console@9e82f9ab) oxidecomputer/console#1811 * [5e11fd83](oxidecomputer/console@5e11fd83) tweak api-diff * [dae20577](oxidecomputer/console@dae20577) oxidecomputer/console#1827 * [ed0ef62e](oxidecomputer/console@ed0ef62e) minor tweaks to api-diff script * [1c790d27](oxidecomputer/console@1c790d27) oxidecomputer/console#1819 * [97be7724](oxidecomputer/console@97be7724) oxidecomputer/console#1826 * [87f4d8b8](oxidecomputer/console@87f4d8b8) oxidecomputer/console#1814 * [65ae1212](oxidecomputer/console@65ae1212) oxidecomputer/console#1820 * [5a6dcea7](oxidecomputer/console@5a6dcea7) oxidecomputer/console#1822 * [4e1bbe13](oxidecomputer/console@4e1bbe13) oxidecomputer/console#1821 * [17408f64](oxidecomputer/console@17408f64) oxidecomputer/console#1813
### User-facing changes * [1802c285](oxidecomputer/console@1802c285) oxidecomputer/console#1839 * [6ae6beeb](oxidecomputer/console@6ae6beeb) oxidecomputer/console#1829 * [a0bf47aa](oxidecomputer/console@a0bf47aa) oxidecomputer/console#1836 * [9e82f9ab](oxidecomputer/console@9e82f9ab) oxidecomputer/console#1811 * [5a6dcea7](oxidecomputer/console@5a6dcea7) oxidecomputer/console#1822 ### All changes oxidecomputer/console@ae8218d...1802c28 * [1802c285](oxidecomputer/console@1802c285) oxidecomputer/console#1839 * [ce09b547](oxidecomputer/console@ce09b547) bump postcss-pseudo-classes for fake vuln * [e09b803b](oxidecomputer/console@e09b803b) might as well get vitest 1.0 in there too * [83dd73ee](oxidecomputer/console@83dd73ee) minor bumps for react router, msw, vite, tailwind, recharts * [6ae6beeb](oxidecomputer/console@6ae6beeb) oxidecomputer/console#1829 * [a0bf47aa](oxidecomputer/console@a0bf47aa) oxidecomputer/console#1836 * [6c9420ad](oxidecomputer/console@6c9420ad) oxidecomputer/console#1835 * [64e97b01](oxidecomputer/console@64e97b01) api-diff also takes a commit * [22bef0bb](oxidecomputer/console@22bef0bb) oxidecomputer/console#1833 * [2fe50f51](oxidecomputer/console@2fe50f51) oxidecomputer/console#1810 * [faadb6d3](oxidecomputer/console@faadb6d3) oxidecomputer/console#1832 * [9e82f9ab](oxidecomputer/console@9e82f9ab) oxidecomputer/console#1811 * [5e11fd83](oxidecomputer/console@5e11fd83) tweak api-diff * [dae20577](oxidecomputer/console@dae20577) oxidecomputer/console#1827 * [ed0ef62e](oxidecomputer/console@ed0ef62e) minor tweaks to api-diff script * [1c790d27](oxidecomputer/console@1c790d27) oxidecomputer/console#1819 * [97be7724](oxidecomputer/console@97be7724) oxidecomputer/console#1826 * [87f4d8b8](oxidecomputer/console@87f4d8b8) oxidecomputer/console#1814 * [65ae1212](oxidecomputer/console@65ae1212) oxidecomputer/console#1820 * [5a6dcea7](oxidecomputer/console@5a6dcea7) oxidecomputer/console#1822 * [4e1bbe13](oxidecomputer/console@4e1bbe13) oxidecomputer/console#1821 * [17408f64](oxidecomputer/console@17408f64) oxidecomputer/console#1813
In the instance creation form, when a user selects an existing image, we want the disk size for the new instance to automatically increase so that it's large enough to successfully use that image. This PR updates the code so that it successfully gets increased to the next-largest increment of 10 GiB (relative to the selected image). It also adds a validator message and an e2e test to cover this behavior.
A few small UI updates in this PR as well that are worth mentioning: