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
17 changes: 14 additions & 3 deletions app/components/form/fields/DisksTableField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ export type DiskTableItem =
* Designed less for reuse, more to encapsulate logic that would otherwise
* clutter the instance create form.
*/
export function DisksTableField({ control }: { control: Control<InstanceCreateInput> }) {
export function DisksTableField({
control,
disabled,
}: {
control: Control<InstanceCreateInput>
disabled: boolean
}) {
const [showDiskCreate, setShowDiskCreate] = useState(false)
const [showDiskAttach, setShowDiskAttach] = useState(false)

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

<div className="space-x-3">
<Button size="sm" onClick={() => setShowDiskCreate(true)}>
<Button size="sm" onClick={() => setShowDiskCreate(true)} disabled={disabled}>
Create new disk
</Button>
<Button variant="ghost" size="sm" onClick={() => setShowDiskAttach(true)}>
<Button
variant="ghost"
size="sm"
onClick={() => setShowDiskAttach(true)}
disabled={disabled}
>
Attach existing disk
</Button>
</div>
Expand Down
4 changes: 3 additions & 1 deletion app/components/form/fields/ImageSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import { ListboxField } from './ListboxField'
type ImageSelectFieldProps = {
images: Image[]
control: Control<InstanceCreateInput>
disabled?: boolean
}

export function ImageSelectField({ images, control }: ImageSelectFieldProps) {
export function ImageSelectField({ images, control, disabled }: ImageSelectFieldProps) {
const diskSizeField = useController({ control, name: 'bootDiskSize' }).field
return (
<ListboxField
disabled={disabled}
control={control}
name="image"
placeholder="Select an image"
Expand Down
3 changes: 3 additions & 0 deletions app/components/form/fields/NetworkInterfaceField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import CreateNetworkInterfaceForm from 'app/forms/network-interface-create'
*/
export function NetworkInterfaceField({
control,
disabled,
}: {
control: Control<InstanceCreateInput>
disabled: boolean
}) {
const [showForm, setShowForm] = useState(false)

Expand Down Expand Up @@ -58,6 +60,7 @@ export function NetworkInterfaceField({
? onChange({ type: newType, params: oldParams })
: onChange({ type: newType })
}}
disabled={disabled}
>
<Radio value="none">None</Radio>
<Radio value="default">Default</Radio>
Expand Down
2 changes: 2 additions & 0 deletions app/components/form/fields/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const NumberFieldInner = <
description,
required,
id: idProp,
disabled,
}: TextFieldProps<TFieldValues, TName>) => {
const generatedId = useId()
const id = idProp || generatedId
Expand All @@ -87,6 +88,7 @@ export const NumberFieldInner = <
[`${id}-help-text`]: !!description,
})}
aria-describedby={description ? `${id}-label-tip` : undefined}
isDisabled={disabled}
{...field}
/>
<ErrorMessage error={error} label={label} />
Expand Down
67 changes: 53 additions & 14 deletions app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*
* Copyright Oxide Computer Company
*/
import { useEffect, useState } from 'react'
import { useWatch } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
import type { SetRequired } from 'type-fest'
Expand Down Expand Up @@ -97,6 +98,7 @@ CreateInstanceForm.loader = async ({ params }: LoaderFunctionArgs) => {
}

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

useEffect(() => {
if (createInstance.error) {
setIsSubmitting(false)
}
}, [createInstance.error])

return (
<FullPageForm
submitDisabled={allImages.length ? undefined : 'Image required'}
Expand All @@ -146,6 +154,7 @@ export function CreateInstanceForm() {
title="Create instance"
icon={<Instances24Icon />}
onSubmit={(values) => {
setIsSubmitting(true)
// we should never have a presetId that's not in the list
const preset = PRESETS.find((option) => option.id === values.presetId)!
const instance =
Expand Down Expand Up @@ -194,9 +203,14 @@ export function CreateInstanceForm() {
loading={createInstance.isPending}
submitError={createInstance.error}
>
<NameField name="name" control={control} />
<DescriptionField name="description" control={control} />
<CheckboxField id="start-instance" name="start" control={control}>
<NameField name="name" control={control} disabled={isSubmitting} />
<DescriptionField name="description" control={control} disabled={isSubmitting} />
<CheckboxField
id="start-instance"
name="start"
control={control}
disabled={isSubmitting}
>
Start Instance
</CheckboxField>

Expand Down Expand Up @@ -224,13 +238,21 @@ export function CreateInstanceForm() {
}}
>
<Tabs.List aria-labelledby="hardware">
<Tabs.Trigger value="general">General Purpose</Tabs.Trigger>
<Tabs.Trigger value="highCPU">High CPU</Tabs.Trigger>
<Tabs.Trigger value="highMemory">High Memory</Tabs.Trigger>
<Tabs.Trigger value="custom">Custom</Tabs.Trigger>
<Tabs.Trigger value="general" disabled={isSubmitting}>
General Purpose
</Tabs.Trigger>
<Tabs.Trigger value="highCPU" disabled={isSubmitting}>
High CPU
</Tabs.Trigger>
<Tabs.Trigger value="highMemory" disabled={isSubmitting}>
High Memory
</Tabs.Trigger>
<Tabs.Trigger value="custom" disabled={isSubmitting}>
Custom
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="general">
<RadioFieldDyn name="presetId" label="" control={control}>
<RadioFieldDyn name="presetId" label="" control={control} disabled={isSubmitting}>
{renderLargeRadioCards('general')}
</RadioFieldDyn>
</Tabs.Content>
Expand Down Expand Up @@ -264,6 +286,7 @@ export function CreateInstanceForm() {
return `CPUs capped to ${INSTANCE_MAX_CPU}`
}
}}
disabled={isSubmitting}
/>
<TextField
units="GiB"
Expand All @@ -282,6 +305,7 @@ export function CreateInstanceForm() {
return `Can be at most ${INSTANCE_MAX_RAM_GiB} GiB`
}
}}
disabled={isSubmitting}
/>
</Tabs.Content>
</Tabs.Root>
Expand All @@ -298,8 +322,12 @@ export function CreateInstanceForm() {
}
>
<Tabs.List aria-describedby="boot-disk">
<Tabs.Trigger value="silo">Silo images</Tabs.Trigger>
<Tabs.Trigger value="project">Project images</Tabs.Trigger>
<Tabs.Trigger value="silo" disabled={isSubmitting}>
Silo images
</Tabs.Trigger>
<Tabs.Trigger value="project" disabled={isSubmitting}>
Project images
</Tabs.Trigger>
</Tabs.List>
{allImages.length === 0 && (
<Message
Expand All @@ -318,7 +346,11 @@ export function CreateInstanceForm() {
/>
</div>
) : (
<ImageSelectField images={siloImages} control={control} />
<ImageSelectField
images={siloImages}
control={control}
disabled={isSubmitting}
/>
)}
</Tabs.Content>
<Tabs.Content value="project" className="space-y-4">
Expand All @@ -333,7 +365,11 @@ export function CreateInstanceForm() {
/>
</div>
) : (
<ImageSelectField images={projectImages} control={control} />
<ImageSelectField
images={projectImages}
control={control}
disabled={isSubmitting}
/>
)}
</Tabs.Content>
</Tabs.Root>
Expand All @@ -349,18 +385,20 @@ export function CreateInstanceForm() {
return `Must be as large as selected image (min. ${imageSize} GiB)`
}
}}
disabled={isSubmitting}
/>
<NameField
name="bootDiskName"
label="Disk name"
description="Will be autogenerated if name not provided"
required={false}
control={control}
disabled={isSubmitting}
/>
<FormDivider />
<Form.Heading id="additional-disks">Additional disks</Form.Heading>

<DisksTableField control={control} />
<DisksTableField control={control} disabled={isSubmitting} />

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

<NetworkInterfaceField control={control} />
<NetworkInterfaceField control={control} disabled={isSubmitting} />

<TextField
name="hostname"
description="Will be generated if not provided"
control={control}
disabled={isSubmitting}
/>

<Form.Actions>
Expand Down
2 changes: 1 addition & 1 deletion libs/ui/lib/radio/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const cardLabelStyles = `
peer-focus:ring-2 peer-focus:ring-accent-secondary
peer-checked:bg-accent-secondary peer-checked:hover:border-accent
peer-checked:border-accent-secondary peer-checked:text-accent peer-checked:[&>*_.text-secondary]:text-accent-secondary
peer-disabled:bg-disabled peer-disabled:text-secondary w-44
peer-disabled:bg-disabled w-44

children:py-3 children:px-3 children:-mx-4 children:border-secondary
first:children:-mt-2 last:children:-mb-2
Expand Down