Skip to content

Commit 700e270

Browse files
authored
Freeze the Create Instance form when submitting, so the user can't make post-submit edits (#1893)
* Freeze create instance from while submitting * align prop names * cleanup unneeded props * Disable all tabs; more refactoring * Add re-set for when createInstance has an error * Target createInstance.error in useEffect
1 parent 644a45b commit 700e270

File tree

6 files changed

+76
-19
lines changed

6 files changed

+76
-19
lines changed

app/components/form/fields/DisksTableField.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ export type DiskTableItem =
2424
* Designed less for reuse, more to encapsulate logic that would otherwise
2525
* clutter the instance create form.
2626
*/
27-
export function DisksTableField({ control }: { control: Control<InstanceCreateInput> }) {
27+
export function DisksTableField({
28+
control,
29+
disabled,
30+
}: {
31+
control: Control<InstanceCreateInput>
32+
disabled: boolean
33+
}) {
2834
const [showDiskCreate, setShowDiskCreate] = useState(false)
2935
const [showDiskAttach, setShowDiskAttach] = useState(false)
3036

@@ -81,10 +87,15 @@ export function DisksTableField({ control }: { control: Control<InstanceCreateIn
8187
)}
8288

8389
<div className="space-x-3">
84-
<Button size="sm" onClick={() => setShowDiskCreate(true)}>
90+
<Button size="sm" onClick={() => setShowDiskCreate(true)} disabled={disabled}>
8591
Create new disk
8692
</Button>
87-
<Button variant="ghost" size="sm" onClick={() => setShowDiskAttach(true)}>
93+
<Button
94+
variant="ghost"
95+
size="sm"
96+
onClick={() => setShowDiskAttach(true)}
97+
disabled={disabled}
98+
>
8899
Attach existing disk
89100
</Button>
90101
</div>

app/components/form/fields/ImageSelectField.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ import { ListboxField } from './ListboxField'
1818
type ImageSelectFieldProps = {
1919
images: Image[]
2020
control: Control<InstanceCreateInput>
21+
disabled?: boolean
2122
}
2223

23-
export function ImageSelectField({ images, control }: ImageSelectFieldProps) {
24+
export function ImageSelectField({ images, control, disabled }: ImageSelectFieldProps) {
2425
const diskSizeField = useController({ control, name: 'bootDiskSize' }).field
2526
return (
2627
<ListboxField
28+
disabled={disabled}
2729
control={control}
2830
name="image"
2931
placeholder="Select an image"

app/components/form/fields/NetworkInterfaceField.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import CreateNetworkInterfaceForm from 'app/forms/network-interface-create'
2323
*/
2424
export function NetworkInterfaceField({
2525
control,
26+
disabled,
2627
}: {
2728
control: Control<InstanceCreateInput>
29+
disabled: boolean
2830
}) {
2931
const [showForm, setShowForm] = useState(false)
3032

@@ -58,6 +60,7 @@ export function NetworkInterfaceField({
5860
? onChange({ type: newType, params: oldParams })
5961
: onChange({ type: newType })
6062
}}
63+
disabled={disabled}
6164
>
6265
<Radio value="none">None</Radio>
6366
<Radio value="default">Default</Radio>

app/components/form/fields/NumberField.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const NumberFieldInner = <
6868
description,
6969
required,
7070
id: idProp,
71+
disabled,
7172
}: TextFieldProps<TFieldValues, TName>) => {
7273
const generatedId = useId()
7374
const id = idProp || generatedId
@@ -87,6 +88,7 @@ export const NumberFieldInner = <
8788
[`${id}-help-text`]: !!description,
8889
})}
8990
aria-describedby={description ? `${id}-label-tip` : undefined}
91+
isDisabled={disabled}
9092
{...field}
9193
/>
9294
<ErrorMessage error={error} label={label} />

app/forms/instance-create.tsx

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
import { useEffect, useState } from 'react'
89
import { useWatch } from 'react-hook-form'
910
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
1011
import type { SetRequired } from 'type-fest'
@@ -97,6 +98,7 @@ CreateInstanceForm.loader = async ({ params }: LoaderFunctionArgs) => {
9798
}
9899

99100
export function CreateInstanceForm() {
101+
const [isSubmitting, setIsSubmitting] = useState(false)
100102
const queryClient = useApiQueryClient()
101103
const addToast = useToast()
102104
const projectSelector = useProjectSelector()
@@ -138,6 +140,12 @@ export function CreateInstanceForm() {
138140
const image = allImages.find((i) => i.id === imageInput)
139141
const imageSize = image?.size ? Math.ceil(image.size / GiB) : undefined
140142

143+
useEffect(() => {
144+
if (createInstance.error) {
145+
setIsSubmitting(false)
146+
}
147+
}, [createInstance.error])
148+
141149
return (
142150
<FullPageForm
143151
submitDisabled={allImages.length ? undefined : 'Image required'}
@@ -146,6 +154,7 @@ export function CreateInstanceForm() {
146154
title="Create instance"
147155
icon={<Instances24Icon />}
148156
onSubmit={(values) => {
157+
setIsSubmitting(true)
149158
// we should never have a presetId that's not in the list
150159
const preset = PRESETS.find((option) => option.id === values.presetId)!
151160
const instance =
@@ -194,9 +203,14 @@ export function CreateInstanceForm() {
194203
loading={createInstance.isPending}
195204
submitError={createInstance.error}
196205
>
197-
<NameField name="name" control={control} />
198-
<DescriptionField name="description" control={control} />
199-
<CheckboxField id="start-instance" name="start" control={control}>
206+
<NameField name="name" control={control} disabled={isSubmitting} />
207+
<DescriptionField name="description" control={control} disabled={isSubmitting} />
208+
<CheckboxField
209+
id="start-instance"
210+
name="start"
211+
control={control}
212+
disabled={isSubmitting}
213+
>
200214
Start Instance
201215
</CheckboxField>
202216

@@ -224,13 +238,21 @@ export function CreateInstanceForm() {
224238
}}
225239
>
226240
<Tabs.List aria-labelledby="hardware">
227-
<Tabs.Trigger value="general">General Purpose</Tabs.Trigger>
228-
<Tabs.Trigger value="highCPU">High CPU</Tabs.Trigger>
229-
<Tabs.Trigger value="highMemory">High Memory</Tabs.Trigger>
230-
<Tabs.Trigger value="custom">Custom</Tabs.Trigger>
241+
<Tabs.Trigger value="general" disabled={isSubmitting}>
242+
General Purpose
243+
</Tabs.Trigger>
244+
<Tabs.Trigger value="highCPU" disabled={isSubmitting}>
245+
High CPU
246+
</Tabs.Trigger>
247+
<Tabs.Trigger value="highMemory" disabled={isSubmitting}>
248+
High Memory
249+
</Tabs.Trigger>
250+
<Tabs.Trigger value="custom" disabled={isSubmitting}>
251+
Custom
252+
</Tabs.Trigger>
231253
</Tabs.List>
232254
<Tabs.Content value="general">
233-
<RadioFieldDyn name="presetId" label="" control={control}>
255+
<RadioFieldDyn name="presetId" label="" control={control} disabled={isSubmitting}>
234256
{renderLargeRadioCards('general')}
235257
</RadioFieldDyn>
236258
</Tabs.Content>
@@ -264,6 +286,7 @@ export function CreateInstanceForm() {
264286
return `CPUs capped to ${INSTANCE_MAX_CPU}`
265287
}
266288
}}
289+
disabled={isSubmitting}
267290
/>
268291
<TextField
269292
units="GiB"
@@ -282,6 +305,7 @@ export function CreateInstanceForm() {
282305
return `Can be at most ${INSTANCE_MAX_RAM_GiB} GiB`
283306
}
284307
}}
308+
disabled={isSubmitting}
285309
/>
286310
</Tabs.Content>
287311
</Tabs.Root>
@@ -298,8 +322,12 @@ export function CreateInstanceForm() {
298322
}
299323
>
300324
<Tabs.List aria-describedby="boot-disk">
301-
<Tabs.Trigger value="silo">Silo images</Tabs.Trigger>
302-
<Tabs.Trigger value="project">Project images</Tabs.Trigger>
325+
<Tabs.Trigger value="silo" disabled={isSubmitting}>
326+
Silo images
327+
</Tabs.Trigger>
328+
<Tabs.Trigger value="project" disabled={isSubmitting}>
329+
Project images
330+
</Tabs.Trigger>
303331
</Tabs.List>
304332
{allImages.length === 0 && (
305333
<Message
@@ -318,7 +346,11 @@ export function CreateInstanceForm() {
318346
/>
319347
</div>
320348
) : (
321-
<ImageSelectField images={siloImages} control={control} />
349+
<ImageSelectField
350+
images={siloImages}
351+
control={control}
352+
disabled={isSubmitting}
353+
/>
322354
)}
323355
</Tabs.Content>
324356
<Tabs.Content value="project" className="space-y-4">
@@ -333,7 +365,11 @@ export function CreateInstanceForm() {
333365
/>
334366
</div>
335367
) : (
336-
<ImageSelectField images={projectImages} control={control} />
368+
<ImageSelectField
369+
images={projectImages}
370+
control={control}
371+
disabled={isSubmitting}
372+
/>
337373
)}
338374
</Tabs.Content>
339375
</Tabs.Root>
@@ -349,18 +385,20 @@ export function CreateInstanceForm() {
349385
return `Must be as large as selected image (min. ${imageSize} GiB)`
350386
}
351387
}}
388+
disabled={isSubmitting}
352389
/>
353390
<NameField
354391
name="bootDiskName"
355392
label="Disk name"
356393
description="Will be autogenerated if name not provided"
357394
required={false}
358395
control={control}
396+
disabled={isSubmitting}
359397
/>
360398
<FormDivider />
361399
<Form.Heading id="additional-disks">Additional disks</Form.Heading>
362400

363-
<DisksTableField control={control} />
401+
<DisksTableField control={control} disabled={isSubmitting} />
364402

365403
<FormDivider />
366404
<Form.Heading id="authentication">Authentication</Form.Heading>
@@ -370,12 +408,13 @@ export function CreateInstanceForm() {
370408
<FormDivider />
371409
<Form.Heading id="networking">Networking</Form.Heading>
372410

373-
<NetworkInterfaceField control={control} />
411+
<NetworkInterfaceField control={control} disabled={isSubmitting} />
374412

375413
<TextField
376414
name="hostname"
377415
description="Will be generated if not provided"
378416
control={control}
417+
disabled={isSubmitting}
379418
/>
380419

381420
<Form.Actions>

libs/ui/lib/radio/Radio.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const cardLabelStyles = `
4343
peer-focus:ring-2 peer-focus:ring-accent-secondary
4444
peer-checked:bg-accent-secondary peer-checked:hover:border-accent
4545
peer-checked:border-accent-secondary peer-checked:text-accent peer-checked:[&>*_.text-secondary]:text-accent-secondary
46-
peer-disabled:bg-disabled peer-disabled:text-secondary w-44
46+
peer-disabled:bg-disabled w-44
4747
4848
children:py-3 children:px-3 children:-mx-4 children:border-secondary
4949
first:children:-mt-2 last:children:-mb-2

0 commit comments

Comments
 (0)