Skip to content
Merged
81 changes: 27 additions & 54 deletions app/components/form/fields/DisksTableField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ import type { DiskCreate } from '@oxide/api'
import { AttachDiskModalForm } from '~/forms/disk-attach'
import { CreateDiskSideModalForm } from '~/forms/disk-create'
import type { InstanceCreateInput } from '~/forms/instance-create'
import { EmptyCell } from '~/table/cells/EmptyCell'
import { sizeCellInner } from '~/table/columns/common'
import { Badge } from '~/ui/lib/Badge'
import { Button } from '~/ui/lib/Button'
import * as MiniTable from '~/ui/lib/MiniTable'
import { MiniTable } from '~/ui/lib/MiniTable'
import { Truncate } from '~/ui/lib/Truncate'
import { bytesToGiB } from '~/util/units'

export type DiskTableItem =
| (DiskCreate & { type: 'create' })
| { name: string; type: 'attach' }
| { name: string; type: 'attach'; size: number }

/**
* Designed less for reuse, more to encapsulate logic that would otherwise
Expand All @@ -47,54 +46,28 @@ export function DisksTableField({
return (
<>
<div className="flex max-w-lg flex-col items-end gap-3">
<MiniTable.Table aria-label="Disks">
<MiniTable.Header>
<MiniTable.HeadCell>Name</MiniTable.HeadCell>
<MiniTable.HeadCell>Type</MiniTable.HeadCell>
<MiniTable.HeadCell>Size</MiniTable.HeadCell>
{/* For remove button */}
<MiniTable.HeadCell />
</MiniTable.Header>
<MiniTable.Body>
{items.length ? (
items.map((item, index) => (
<MiniTable.Row
tabIndex={0}
aria-rowindex={index + 1}
aria-label={`Name: ${item.name}, Type: ${item.type}`}
key={item.name}
>
<MiniTable.Cell>
<Truncate text={item.name} maxLength={35} />
</MiniTable.Cell>
<MiniTable.Cell>
<Badge>{item.type}</Badge>
</MiniTable.Cell>
<MiniTable.Cell>
{item.type === 'attach' ? (
<EmptyCell />
) : (
<>
<span>{bytesToGiB(item.size)}</span>
<span className="ml-1 inline-block text-tertiary">GiB</span>
</>
)}
</MiniTable.Cell>
<MiniTable.RemoveCell
onClick={() => onChange(items.filter((i) => i.name !== item.name))}
label={`remove disk ${item.name}`}
/>
</MiniTable.Row>
))
) : (
<MiniTable.EmptyState
title="No disks"
body="Add a disk to see it here"
colSpan={4}
/>
)}
</MiniTable.Body>
</MiniTable.Table>
<MiniTable
ariaLabel="Disks"
items={items}
columns={[
{
header: 'Name',
cell: (item) => <Truncate text={item.name} maxLength={35} />,
},
{
header: 'Type',
cell: (item) => <Badge>{item.type}</Badge>,
},
{
header: 'Size',
cell: (item) => sizeCellInner(item.size),
},
]}
rowKey={(item) => item.name}
onRemoveItem={(item) => onChange(items.filter((i) => i.name !== item.name))}
removeLabel={(item) => `Remove disk ${item.name}`}
emptyState={{ title: 'No disks', body: 'Add a disk to see it here' }}
/>

<div className="space-x-3">
<Button size="sm" onClick={() => setShowDiskCreate(true)} disabled={disabled}>
Expand Down Expand Up @@ -124,8 +97,8 @@ export function DisksTableField({
{showDiskAttach && (
<AttachDiskModalForm
onDismiss={() => setShowDiskAttach(false)}
onSubmit={(values) => {
onChange([...items, { type: 'attach', ...values }])
onSubmit={({ name, size }: { name: string; size: number }) => {
onChange([...items, { type: 'attach', name, size } satisfies DiskTableItem])
setShowDiskAttach(false)
}}
diskNamesToExclude={items.filter((i) => i.type === 'attach').map((i) => i.name)}
Expand Down
54 changes: 19 additions & 35 deletions app/components/form/fields/NetworkInterfaceField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { InstanceCreateInput } from '~/forms/instance-create'
import { CreateNetworkInterfaceForm } from '~/forms/network-interface-create'
import { Button } from '~/ui/lib/Button'
import { FieldLabel } from '~/ui/lib/FieldLabel'
import * as MiniTable from '~/ui/lib/MiniTable'
import { MiniTable } from '~/ui/lib/MiniTable'
import { Radio } from '~/ui/lib/Radio'
import { RadioGroup } from '~/ui/lib/RadioGroup'

Expand Down Expand Up @@ -75,40 +75,24 @@ export function NetworkInterfaceField({
</RadioGroup>
{value.type === 'create' && (
<>
{value.params.length > 0 && (
<MiniTable.Table className="pt-2">
<MiniTable.Header>
<MiniTable.HeadCell>Name</MiniTable.HeadCell>
<MiniTable.HeadCell>VPC</MiniTable.HeadCell>
<MiniTable.HeadCell>Subnet</MiniTable.HeadCell>
{/* For remove button */}
<MiniTable.HeadCell className="w-12" />
</MiniTable.Header>
<MiniTable.Body>
{value.params.map((item, index) => (
<MiniTable.Row
tabIndex={0}
aria-rowindex={index + 1}
aria-label={`Name: ${item.name}, Vpc: ${item.vpcName}, Subnet: ${item.subnetName}`}
key={item.name}
>
<MiniTable.Cell>{item.name}</MiniTable.Cell>
<MiniTable.Cell>{item.vpcName}</MiniTable.Cell>
<MiniTable.Cell>{item.subnetName}</MiniTable.Cell>
<MiniTable.RemoveCell
onClick={() =>
onChange({
type: 'create',
params: value.params.filter((i) => i.name !== item.name),
})
}
label={`remove network interface ${item.name}`}
/>
</MiniTable.Row>
))}
</MiniTable.Body>
</MiniTable.Table>
)}
<MiniTable
className="pt-2"
ariaLabel="Network Interfaces"
items={value.params}
columns={[
{ header: 'Name', cell: (item) => item.name },
{ header: 'VPC', cell: (item) => item.vpcName },
{ header: 'Subnet', cell: (item) => item.subnetName },
]}
rowKey={(item) => item.name}
onRemoveItem={(item) =>
onChange({
type: 'create',
params: value.params.filter((i) => i.name !== item.name),
})
}
removeLabel={(item) => `remove network interface ${item.name}`}
/>

{showForm && (
<CreateNetworkInterfaceForm
Expand Down
36 changes: 10 additions & 26 deletions app/components/form/fields/TlsCertsField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { CertificateCreate } from '@oxide/api'
import type { SiloCreateFormValues } from '~/forms/silo-create'
import { Button } from '~/ui/lib/Button'
import { FieldLabel } from '~/ui/lib/FieldLabel'
import * as MiniTable from '~/ui/lib/MiniTable'
import { MiniTable } from '~/ui/lib/MiniTable'
import { Modal } from '~/ui/lib/Modal'

import { DescriptionField } from './DescriptionField'
Expand Down Expand Up @@ -46,31 +46,15 @@ export function TlsCertsField({ control }: { control: Control<SiloCreateFormValu
<FieldLabel id="tls-certificates-label" className="mb-3">
TLS Certificates
</FieldLabel>
{!!items.length && (
<MiniTable.Table className="mb-4">
<MiniTable.Header>
<MiniTable.HeadCell>Name</MiniTable.HeadCell>
{/* For remove button */}
<MiniTable.HeadCell className="w-12" />
</MiniTable.Header>
<MiniTable.Body>
{items.map((item, index) => (
<MiniTable.Row
tabIndex={0}
aria-rowindex={index + 1}
aria-label={`Name: ${item.name}, Description: ${item.description}`}
key={item.name}
>
<MiniTable.Cell>{item.name}</MiniTable.Cell>
<MiniTable.RemoveCell
onClick={() => onChange(items.filter((i) => i.name !== item.name))}
label={`remove cert ${item.name}`}
/>
</MiniTable.Row>
))}
</MiniTable.Body>
</MiniTable.Table>
)}
<MiniTable
className="mb-4"
ariaLabel="TLS Certificates"
items={items}
columns={[{ header: 'Name', cell: (item) => item.name }]}
rowKey={(item) => item.name}
onRemoveItem={(item) => onChange(items.filter((i) => i.name !== item.name))}
removeLabel={(item) => `remove cert ${item.name}`}
/>

{/* ref on button element allows scrollTo to work when the form has a "missing TLS cert" error */}
<Button size="sm" onClick={() => setShowAddCert(true)} ref={ref}>
Expand Down
10 changes: 8 additions & 2 deletions app/forms/disk-attach.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const defaultValues = { name: '' }

type AttachDiskProps = {
/** If defined, this overrides the usual mutation */
onSubmit: (diskAttach: { name: string }) => void
onSubmit: (diskAttach: { name: string; size: number }) => void
onDismiss: () => void
diskNamesToExclude?: string[]
loading?: boolean
Expand Down Expand Up @@ -64,7 +64,13 @@ export function AttachDiskModalForm({
submitError={submitError}
loading={loading}
title="Attach disk"
onSubmit={onSubmit}
onSubmit={({ name }) => {
// because the ComboboxField is required and does not allow arbitrary
// values (values not in the list of disks), we can only get here if the
// disk is defined and in the list
const disk = data!.items.find((d) => d.name === name)!
onSubmit({ name, size: disk.size })
}}
>
<ComboboxField
label="Disk name"
Expand Down
92 changes: 31 additions & 61 deletions app/forms/firewall-rules-common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { toComboboxItems } from '~/ui/lib/Combobox'
import { FormDivider } from '~/ui/lib/Divider'
import { FieldLabel } from '~/ui/lib/FieldLabel'
import { Message } from '~/ui/lib/Message'
import * as MiniTable from '~/ui/lib/MiniTable'
import { ClearAndAddButtons, MiniTable } from '~/ui/lib/MiniTable'
import { SideModal } from '~/ui/lib/SideModal'
import { TextInputHint } from '~/ui/lib/TextInput'
import { KEYS } from '~/ui/util/keys'
Expand Down Expand Up @@ -148,11 +148,12 @@ const TargetAndHostFilterSubform = ({
subform.setValue('value', value)
}

const noun = sectionType === 'target' ? 'target' : 'host filter'
const nounTitle = capitalize(noun) + 's'

return (
<>
<SideModal.Heading>
{sectionType === 'target' ? 'Targets' : 'Host filters'}
</SideModal.Heading>
<SideModal.Heading>{nounTitle}</SideModal.Heading>

<Message variant="info" content={messageContent} />
<ListboxField
Expand Down Expand Up @@ -209,48 +210,25 @@ const TargetAndHostFilterSubform = ({
}
/>
)}
<MiniTable.ClearAndAddButtons
addButtonCopy={`Add ${sectionType === 'host' ? 'host filter' : 'target'}`}
<ClearAndAddButtons
addButtonCopy={`Add ${noun}`}
disabled={!value}
onClear={() => subform.reset()}
onSubmit={submitSubform}
/>
{field.value.length > 0 && (
<MiniTable.Table
className="mb-4"
aria-label={sectionType === 'target' ? 'Targets' : 'Host filters'}
>
<MiniTable.Header>
<MiniTable.HeadCell>Type</MiniTable.HeadCell>
<MiniTable.HeadCell>Value</MiniTable.HeadCell>
{/* For remove button */}
<MiniTable.HeadCell className="w-12" />
</MiniTable.Header>
<MiniTable.Body>
{field.value.map(({ type, value }, index) => (
<MiniTable.Row
tabIndex={0}
aria-rowindex={index + 1}
aria-label={`Name: ${value}, Type: ${type}`}
key={`${type}|${value}`}
>
<MiniTable.Cell>
<Badge>{type}</Badge>
</MiniTable.Cell>
<MiniTable.Cell>{value}</MiniTable.Cell>
<MiniTable.RemoveCell
onClick={() =>
field.onChange(
field.value.filter((i) => !(i.value === value && i.type === type))
)
}
label={`remove ${sectionType} ${value}`}
/>
</MiniTable.Row>
))}
</MiniTable.Body>
</MiniTable.Table>
)}
<MiniTable
className="mb-4"
ariaLabel={nounTitle}
items={field.value}
columns={[
{ header: 'Type', cell: (item) => <Badge>{item.type}</Badge> },
{ header: 'Value', cell: (item) => item.value },
]}
rowKey={({ type, value }) => `${type}|${value}`}
onRemoveItem={({ type, value }) => {
field.onChange(field.value.filter((i) => !(i.value === value && i.type === type)))
}}
/>
</>
)
}
Expand Down Expand Up @@ -450,32 +428,24 @@ export const CommonFields = ({ control, nameTaken, error }: CommonFieldsProps) =
}}
/>
</div>
<MiniTable.ClearAndAddButtons
<ClearAndAddButtons
addButtonCopy="Add port filter"
disabled={!portValue}
onClear={() => portRangeForm.reset()}
onSubmit={submitPortRange}
/>
</div>
{ports.value.length > 0 && (
<MiniTable.Table className="mb-4" aria-label="Port filters">
<MiniTable.Header>
<MiniTable.HeadCell>Port ranges</MiniTable.HeadCell>
{/* For remove button */}
<MiniTable.HeadCell className="w-12" />
</MiniTable.Header>
<MiniTable.Body>
{ports.value.map((p) => (
<MiniTable.Row tabIndex={0} aria-label={p} key={p}>
<MiniTable.Cell>{p}</MiniTable.Cell>
<MiniTable.RemoveCell
onClick={() => ports.onChange(ports.value.filter((p1) => p1 !== p))}
label={`remove port ${p}`}
/>
</MiniTable.Row>
))}
</MiniTable.Body>
</MiniTable.Table>
<MiniTable
className="mb-4"
ariaLabel="Port filters"
items={ports.value}
columns={[{ header: 'Port ranges', cell: (p) => p }]}
rowKey={(port) => port}
emptyState={{ title: 'No ports', body: 'Add a port to see it here' }}
onRemoveItem={(p) => ports.onChange(ports.value.filter((p1) => p1 !== p))}
removeLabel={(port) => `remove port ${port}`}
/>
)}

<fieldset className="space-y-0.5">
Expand Down
Loading
Loading