Skip to content
47 changes: 27 additions & 20 deletions app/components/form/fields/DisksTableField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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 { Badge } from '~/ui/lib/Badge'
import { Button } from '~/ui/lib/Button'
import * as MiniTable from '~/ui/lib/MiniTable'
Expand Down Expand Up @@ -45,18 +46,18 @@ export function DisksTableField({

return (
<>
<div className="max-w-lg">
{!!items.length && (
<MiniTable.Table className="mb-4" 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 className="w-12" />
</MiniTable.Header>
<MiniTable.Body>
{items.map((item, index) => (
<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}
Expand All @@ -67,15 +68,15 @@ export function DisksTableField({
<Truncate text={item.name} maxLength={35} />
</MiniTable.Cell>
<MiniTable.Cell>
<Badge variant="solid">{item.type}</Badge>
<Badge>{item.type}</Badge>
</MiniTable.Cell>
<MiniTable.Cell>
{item.type === 'attach' ? (
'—'
<EmptyCell />
) : (
<>
<span>{bytesToGiB(item.size)}</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a change we need to make now, but should we make a size component that ensures the item and the unit are handled consistently / spacing and text colour.

<span className="ml-1 inline-block text-accent-secondary">GiB</span>
<span className="ml-1 inline-block text-tertiary">GiB</span>
</>
)}
</MiniTable.Cell>
Expand All @@ -84,17 +85,23 @@ export function DisksTableField({
label={`remove disk ${item.name}`}
/>
</MiniTable.Row>
))}
</MiniTable.Body>
</MiniTable.Table>
)}
))
) : (
<MiniTable.EmptyState
title="No disks"
body="Add a disk to see it here"
colSpan={4}
/>
)}
</MiniTable.Body>
</MiniTable.Table>

<div className="space-x-3">
<Button size="sm" onClick={() => setShowDiskCreate(true)} disabled={disabled}>
Create new disk
</Button>
<Button
variant="ghost"
variant="secondary"
size="sm"
onClick={() => setShowDiskAttach(true)}
disabled={disabled}
Expand Down
2 changes: 1 addition & 1 deletion app/forms/firewall-rules-common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ const TargetAndHostFilterSubform = ({
key={`${type}|${value}`}
>
<MiniTable.Cell>
<Badge variant="solid">{type}</Badge>
<Badge>{type}</Badge>
</MiniTable.Cell>
<MiniTable.Cell>{value}</MiniTable.Cell>
<MiniTable.RemoveCell
Expand Down
66 changes: 36 additions & 30 deletions app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -777,44 +777,51 @@ const AdvancedAccordion = ({
detached from them as needed
</TipIcon>
</h2>
{isFloatingIpAttached && (
<MiniTable.Table>
<MiniTable.Header>
<MiniTable.HeadCell>Name</MiniTable.HeadCell>
<MiniTable.HeadCell>IP</MiniTable.HeadCell>
{/* For remove button */}
<MiniTable.HeadCell className="w-12" />
</MiniTable.Header>
<MiniTable.Body>
{attachedFloatingIpsData.map((item, index) => (
<MiniTable.Row
tabIndex={0}
aria-rowindex={index + 1}
aria-label={`Name: ${item.name}, IP: ${item.ip}`}
key={item.name}
>
<MiniTable.Cell>{item.name}</MiniTable.Cell>
<MiniTable.Cell>{item.ip}</MiniTable.Cell>
<MiniTable.RemoveCell
onClick={() => detachFloatingIp(item.name)}
label={`remove floating IP ${item.name}`}
/>
</MiniTable.Row>
))}
</MiniTable.Body>
</MiniTable.Table>
)}
{floatingIpList.items.length === 0 ? (
<div className="flex max-w-lg items-center justify-center rounded-lg border p-6 border-default">
<div className="flex max-w-lg items-center justify-center rounded-lg border border-default">
<EmptyMessage
icon={<IpGlobal16Icon />}
title="No floating IPs found"
body="Create a floating IP to attach it to this instance"
/>
</div>
) : (
<div>
<div className="flex flex-col items-end gap-3">
<MiniTable.Table>
<MiniTable.Header>
<MiniTable.HeadCell>Name</MiniTable.HeadCell>
<MiniTable.HeadCell>IP</MiniTable.HeadCell>
{/* For remove button */}
<MiniTable.HeadCell className="w-12" />
</MiniTable.Header>
<MiniTable.Body>
{isFloatingIpAttached ? (
attachedFloatingIpsData.map((item, index) => (
<MiniTable.Row
tabIndex={0}
aria-rowindex={index + 1}
aria-label={`Name: ${item.name}, IP: ${item.ip}`}
key={item.name}
>
<MiniTable.Cell>{item.name}</MiniTable.Cell>
<MiniTable.Cell>{item.ip}</MiniTable.Cell>
<MiniTable.RemoveCell
onClick={() => detachFloatingIp(item.name)}
label={`remove floating IP ${item.name}`}
/>
</MiniTable.Row>
))
) : (
<MiniTable.EmptyState
title="No floating IPs attached"
body="Attach a floating IP to see it here"
colSpan={3}
/>
)}
</MiniTable.Body>
</MiniTable.Table>
<Button
variant="secondary"
size="sm"
className="shrink-0"
disabled={availableFloatingIps.length === 0}
Expand All @@ -825,7 +832,6 @@ const AdvancedAccordion = ({
</Button>
</div>
)}

<Modal
isOpen={floatingIpModalOpen}
onDismiss={closeFloatingIpModal}
Expand Down
32 changes: 32 additions & 0 deletions app/ui/lib/MiniTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Error16Icon } from '@oxide/design-system/icons/react'
import { classed } from '~/util/classed'

import { Button } from './Button'
import { EmptyMessage } from './EmptyMessage'
import { Table as BigTable } from './Table'

type Children = { children: React.ReactNode }
Expand All @@ -36,6 +37,37 @@ export const Cell = ({ children }: Children) => {
)
}

export const EmptyState = (props: { title: string; body: string; colSpan: number }) => (
<Row>
<td colSpan={props.colSpan}>
<div className="!m-0 !w-full !flex-col !border-none !bg-transparent !py-14">
<EmptyMessage title={props.title} body={props.body} />
</div>
</td>
</Row>
)

export const InputCell = ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not used in this branch, but will be used in follow-up

colSpan,
defaultValue,
placeholder,
}: {
colSpan?: number
defaultValue: string
placeholder: string
}) => (
<td colSpan={colSpan}>
<div>
<input
type="text"
className="text-sm m-0 w-full bg-transparent p-0 !outline-none text-default placeholder:text-quaternary"
placeholder={placeholder}
defaultValue={defaultValue}
/>
</div>
</td>
)

// followed this for icon in button best practices
// https://www.sarasoueidan.com/blog/accessible-icon-buttons/
export const RemoveCell = ({ onClick, label }: { onClick: () => void; label: string }) => (
Expand Down
47 changes: 27 additions & 20 deletions app/ui/styles/components/mini-table.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,54 @@
border-spacing: 0px;
}

& td {
@apply relative px-0 pt-2;
}

/* all rows */
& tr {
@apply bg-default;
@apply relative;
}

/* all cells */
& td {
@apply relative px-0 pt-2;
}

/* a fake left border for all cells that aren't first */
& td + td:before {
@apply absolute bottom-[2px] top-[calc(0.5rem+1px)] block w-[1px] border-l opacity-40 border-accent-tertiary;
@apply absolute bottom-[2px] top-[calc(0.5rem+1px)] block w-[1px] border-l border-secondary;
content: ' ';
}

& tr:last-child td + td:before {
@apply bottom-[calc(0.5rem+2px)];
}

/* all divs */
& td > div {
@apply flex h-11 items-center border-y py-3 pl-3 pr-6 text-accent bg-accent-secondary border-accent-tertiary;
@apply flex h-9 items-center border border-y border-r-0 py-3 pl-3 pr-6 border-default;
}

& td:last-child > div {
@apply w-12 justify-center pl-0 pr-0;
}
& td:last-child > div > button {
@apply -mx-3 -my-3 flex items-center justify-center px-3 py-3;
/* first cell's div */
& td:first-child > div {
@apply ml-2 rounded-l border-l;
}
& td:last-child > div:has(button:hover, button:focus) {
@apply bg-accent-secondary-hover;

/* second-to-last cell's div */
& td:nth-last-child(2) > div {
@apply rounded-r border-r;
}

& tr:last-child td {
@apply pb-2;
/* last cell's div (the div for the delete button) */
& td:last-child > div {
@apply flex w-8 items-center justify-center border-none px-5;
}

& td:first-child > div {
@apply ml-2 rounded-l border-l;
/* the delete button */
& td:last-child > div > button {
@apply -m-2 flex items-center justify-center p-2 text-tertiary hover:text-secondary focus:text-secondary;
}

& td:last-child > div {
@apply mr-2 rounded-r border-r;
& tr:last-child td {
@apply pb-2;
}

& thead tr:first-of-type th:first-of-type {
Expand All @@ -61,7 +68,7 @@

& thead tr:first-of-type th:last-of-type {
border-top-right-radius: var(--border-radius-lg);
@apply border-r;
@apply w-8 border-r;
}

& tbody tr:last-of-type td:first-of-type {
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/instance-create.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ test('can’t create a disk with a name that collides with the boot disk name',
await page.fill('input[name=bootDiskName]', 'disk-11')

// Attempt to create a disk with the same name
await expect(page.getByText('No disks')).toBeVisible()
await page.getByRole('button', { name: 'Create new disk' }).click()
const dialog = page.getByRole('dialog')
await dialog.getByRole('textbox', { name: 'name' }).fill('disk-11')
Expand All @@ -268,6 +269,7 @@ test('can’t create a disk with a name that collides with the boot disk name',
await dialog.getByRole('button', { name: 'Create disk' }).click()
// The disk has been "created" (is in the list of Additional Disks)
await expectVisible(page, ['text=disk-12'])
await expect(page.getByText('No disks')).toBeHidden()
// Create the instance
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL('/projects/mock-project/instances/another-instance/storage')
Expand Down
Loading