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
59 changes: 0 additions & 59 deletions app/components/form/fields/DiskSizeField.tsx

This file was deleted.

6 changes: 5 additions & 1 deletion app/components/form/fields/ListboxField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type Control,
type FieldPath,
type FieldValues,
type RegisterOptions,
} from 'react-hook-form'

import { Listbox, type ListboxItem } from '~/ui/lib/Listbox'
Expand All @@ -37,6 +38,8 @@ export type ListboxFieldProps<
isLoading?: boolean
noItemsPlaceholder?: string
hideOptionalTag?: boolean
/** Other fields to re-validate when this one changes */
deps?: RegisterOptions<TFieldValues, TName>['deps']
}

export function ListboxField<
Expand All @@ -57,10 +60,11 @@ export function ListboxField<
noItemsPlaceholder,
hideOptionalTag,
hideLabel,
deps,
}: ListboxFieldProps<TFieldValues, TName>) {
// TODO: recreate this logic
// validate: (v) => (required && !v ? `${name} is required` : undefined),
const { field, fieldState } = useController({ name, control, rules: { required } })
const { field, fieldState } = useController({ name, control, rules: { required, deps } })
return (
<div className={cn('max-w-lg', className)}>
<Listbox
Expand Down
21 changes: 18 additions & 3 deletions app/components/form/fields/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ export function NumberField<
)}
</div>
{/* passing the generated id is very important for a11y */}
<NumberFieldInner name={name} id={id} label={label} required={required} {...props} />
<NumberFieldInner
name={name}
id={id}
label={label}
units={units}
required={required}
{...props}
/>
</div>
)
}
Expand Down Expand Up @@ -72,6 +79,7 @@ export const NumberFieldInner = <
disabled,
max,
min = 0,
units,
}: TextFieldProps<TFieldValues, TName>) => {
const generatedId = useId()
const id = idProp || generatedId
Expand All @@ -85,9 +93,16 @@ export const NumberFieldInner = <
rules: {
required,
deps,
// it seems we need special logic to enforce required on NaN
// RHF's required rule doesn't catch NaN, and its min/max rules don't
// know about units, so we do all three here. The input itself no longer
// clamps, so this is what stops out-of-range values.
validate(value, values) {
if (required && Number.isNaN(value)) return `${label} is required`
if (Number.isNaN(value)) return required ? `${label} is required` : undefined
const suffix = units ? ` ${units}` : ''
if (min !== undefined && value < Number(min))
return `Must be at least ${min}${suffix}`
if (max !== undefined && value > Number(max))
return `Can be at most ${max}${suffix}`
return validate?.(value, values)
},
},
Expand Down
14 changes: 11 additions & 3 deletions app/forms/disk-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ import {

import { CheckboxField } from '~/components/form/fields/CheckboxField'
import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { DiskSizeField } from '~/components/form/fields/DiskSizeField'
import { toImageComboboxItem } from '~/components/form/fields/ImageSelectField'
import { ListboxField } from '~/components/form/fields/ListboxField'
import { NameField } from '~/components/form/fields/NameField'
import { NumberField } from '~/components/form/fields/NumberField'
import { RadioField } from '~/components/form/fields/RadioField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { HL } from '~/components/HL'
Expand Down Expand Up @@ -207,9 +207,12 @@ export function CreateDiskSideModalForm({
}}
/>
<DescriptionField name="description" control={form.control} />
<DiskSizeField
<NumberField
name="size"
control={form.control}
units="GiB"
required
min={1}
// Local disk size is only capped by server capacity
max={match(diskBackend)
.with({ type: 'local' }, () => undefined)
Expand Down Expand Up @@ -242,7 +245,12 @@ const DiskBackendField = ({
}) => {
const {
field: { value: diskBackend, onChange },
} = useController({ control, name: 'diskBackend' })
} = useController({
control,
name: 'diskBackend',
// Switching disk type changes the size limit.
rules: { deps: 'size' },
})
// react-hook-form types onChange as (...event: any[]) => void
// https://github.com/react-hook-form/react-hook-form/issues/10466
const setDiskBackend: (value: DiskBackendForm) => void = onChange
Expand Down
2 changes: 2 additions & 0 deletions app/forms/external-subnet-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export default function CreateExternalSubnetSideModalForm() {
items={pools.items.map(toPoolItem)}
required
description="Subnet pool to allocate from"
// the pool's IP version sets the prefix length max
deps="prefixLength"
/>
<NumberField
name="prefixLength"
Expand Down
5 changes: 3 additions & 2 deletions app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import { DocsPopover } from '~/components/DocsPopover'
import { CheckboxField } from '~/components/form/fields/CheckboxField'
import { ComboboxField } from '~/components/form/fields/ComboboxField'
import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { DiskSizeField } from '~/components/form/fields/DiskSizeField'
import {
DisksTableField,
type DiskTableItem,
Expand Down Expand Up @@ -487,11 +486,13 @@ export default function CreateInstanceForm() {
const bootDiskSizeAndName = (
<>
<div key="divider1" className="my-6! content-['a']" />
<DiskSizeField
<NumberField
key="diskSizeField"
label="Disk size"
name="bootDiskSize"
control={control}
units="GiB"
required
min={imageSizeGiB || 1}
// Max size applies: this disk can only be distributed
max={MAX_DISK_SIZE_GiB}
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/external-subnets.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,14 @@ test('create form prefix length max changes with pool IP version', async ({ page
// Switch back to v4 — value should clamp back to 32
await selectOption(page, 'Subnet pool', v4Pool)
await expect(prefixLen).toHaveValue('32')

// The clamp is display-only: the form still holds 64, so submit must be
// blocked with a message rather than sending an out-of-range prefix
await page.getByRole('textbox', { name: 'Name' }).fill('too-long')
await page.getByRole('button', { name: 'Create external subnet' }).click()
const dialog = page.getByRole('dialog', { name: 'Create external subnet' })
await expect(dialog.getByText('Can be at most 32')).toBeVisible()
await expect(dialog).toBeVisible()
})

test('create form toggles between auto and explicit fields', async ({ page }) => {
Expand Down
Loading