Skip to content

Commit 5f338ce

Browse files
authored
Add component for empty MiniTable (#2811)
1 parent e891f96 commit 5f338ce

File tree

6 files changed

+125
-71
lines changed

6 files changed

+125
-71
lines changed

app/components/form/fields/DisksTableField.tsx

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { DiskCreate } from '@oxide/api'
1313
import { AttachDiskModalForm } from '~/forms/disk-attach'
1414
import { CreateDiskSideModalForm } from '~/forms/disk-create'
1515
import type { InstanceCreateInput } from '~/forms/instance-create'
16+
import { EmptyCell } from '~/table/cells/EmptyCell'
1617
import { Badge } from '~/ui/lib/Badge'
1718
import { Button } from '~/ui/lib/Button'
1819
import * as MiniTable from '~/ui/lib/MiniTable'
@@ -45,18 +46,18 @@ export function DisksTableField({
4546

4647
return (
4748
<>
48-
<div className="max-w-lg">
49-
{!!items.length && (
50-
<MiniTable.Table className="mb-4" aria-label="Disks">
51-
<MiniTable.Header>
52-
<MiniTable.HeadCell>Name</MiniTable.HeadCell>
53-
<MiniTable.HeadCell>Type</MiniTable.HeadCell>
54-
<MiniTable.HeadCell>Size</MiniTable.HeadCell>
55-
{/* For remove button */}
56-
<MiniTable.HeadCell className="w-12" />
57-
</MiniTable.Header>
58-
<MiniTable.Body>
59-
{items.map((item, index) => (
49+
<div className="flex max-w-lg flex-col items-end gap-3">
50+
<MiniTable.Table aria-label="Disks">
51+
<MiniTable.Header>
52+
<MiniTable.HeadCell>Name</MiniTable.HeadCell>
53+
<MiniTable.HeadCell>Type</MiniTable.HeadCell>
54+
<MiniTable.HeadCell>Size</MiniTable.HeadCell>
55+
{/* For remove button */}
56+
<MiniTable.HeadCell />
57+
</MiniTable.Header>
58+
<MiniTable.Body>
59+
{items.length ? (
60+
items.map((item, index) => (
6061
<MiniTable.Row
6162
tabIndex={0}
6263
aria-rowindex={index + 1}
@@ -67,15 +68,15 @@ export function DisksTableField({
6768
<Truncate text={item.name} maxLength={35} />
6869
</MiniTable.Cell>
6970
<MiniTable.Cell>
70-
<Badge variant="solid">{item.type}</Badge>
71+
<Badge>{item.type}</Badge>
7172
</MiniTable.Cell>
7273
<MiniTable.Cell>
7374
{item.type === 'attach' ? (
74-
'—'
75+
<EmptyCell />
7576
) : (
7677
<>
7778
<span>{bytesToGiB(item.size)}</span>
78-
<span className="ml-1 inline-block text-accent-secondary">GiB</span>
79+
<span className="ml-1 inline-block text-tertiary">GiB</span>
7980
</>
8081
)}
8182
</MiniTable.Cell>
@@ -84,17 +85,23 @@ export function DisksTableField({
8485
label={`remove disk ${item.name}`}
8586
/>
8687
</MiniTable.Row>
87-
))}
88-
</MiniTable.Body>
89-
</MiniTable.Table>
90-
)}
88+
))
89+
) : (
90+
<MiniTable.EmptyState
91+
title="No disks"
92+
body="Add a disk to see it here"
93+
colSpan={4}
94+
/>
95+
)}
96+
</MiniTable.Body>
97+
</MiniTable.Table>
9198

9299
<div className="space-x-3">
93100
<Button size="sm" onClick={() => setShowDiskCreate(true)} disabled={disabled}>
94101
Create new disk
95102
</Button>
96103
<Button
97-
variant="ghost"
104+
variant="secondary"
98105
size="sm"
99106
onClick={() => setShowDiskAttach(true)}
100107
disabled={disabled}

app/forms/firewall-rules-common.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ const TargetAndHostFilterSubform = ({
235235
key={`${type}|${value}`}
236236
>
237237
<MiniTable.Cell>
238-
<Badge variant="solid">{type}</Badge>
238+
<Badge>{type}</Badge>
239239
</MiniTable.Cell>
240240
<MiniTable.Cell>{value}</MiniTable.Cell>
241241
<MiniTable.RemoveCell

app/forms/instance-create.tsx

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -777,44 +777,51 @@ const AdvancedAccordion = ({
777777
detached from them as needed
778778
</TipIcon>
779779
</h2>
780-
{isFloatingIpAttached && (
781-
<MiniTable.Table>
782-
<MiniTable.Header>
783-
<MiniTable.HeadCell>Name</MiniTable.HeadCell>
784-
<MiniTable.HeadCell>IP</MiniTable.HeadCell>
785-
{/* For remove button */}
786-
<MiniTable.HeadCell className="w-12" />
787-
</MiniTable.Header>
788-
<MiniTable.Body>
789-
{attachedFloatingIpsData.map((item, index) => (
790-
<MiniTable.Row
791-
tabIndex={0}
792-
aria-rowindex={index + 1}
793-
aria-label={`Name: ${item.name}, IP: ${item.ip}`}
794-
key={item.name}
795-
>
796-
<MiniTable.Cell>{item.name}</MiniTable.Cell>
797-
<MiniTable.Cell>{item.ip}</MiniTable.Cell>
798-
<MiniTable.RemoveCell
799-
onClick={() => detachFloatingIp(item.name)}
800-
label={`remove floating IP ${item.name}`}
801-
/>
802-
</MiniTable.Row>
803-
))}
804-
</MiniTable.Body>
805-
</MiniTable.Table>
806-
)}
807780
{floatingIpList.items.length === 0 ? (
808-
<div className="flex max-w-lg items-center justify-center rounded-lg border p-6 border-default">
781+
<div className="flex max-w-lg items-center justify-center rounded-lg border border-default">
809782
<EmptyMessage
810783
icon={<IpGlobal16Icon />}
811784
title="No floating IPs found"
812785
body="Create a floating IP to attach it to this instance"
813786
/>
814787
</div>
815788
) : (
816-
<div>
789+
<div className="flex flex-col items-end gap-3">
790+
<MiniTable.Table>
791+
<MiniTable.Header>
792+
<MiniTable.HeadCell>Name</MiniTable.HeadCell>
793+
<MiniTable.HeadCell>IP</MiniTable.HeadCell>
794+
{/* For remove button */}
795+
<MiniTable.HeadCell className="w-12" />
796+
</MiniTable.Header>
797+
<MiniTable.Body>
798+
{isFloatingIpAttached ? (
799+
attachedFloatingIpsData.map((item, index) => (
800+
<MiniTable.Row
801+
tabIndex={0}
802+
aria-rowindex={index + 1}
803+
aria-label={`Name: ${item.name}, IP: ${item.ip}`}
804+
key={item.name}
805+
>
806+
<MiniTable.Cell>{item.name}</MiniTable.Cell>
807+
<MiniTable.Cell>{item.ip}</MiniTable.Cell>
808+
<MiniTable.RemoveCell
809+
onClick={() => detachFloatingIp(item.name)}
810+
label={`remove floating IP ${item.name}`}
811+
/>
812+
</MiniTable.Row>
813+
))
814+
) : (
815+
<MiniTable.EmptyState
816+
title="No floating IPs attached"
817+
body="Attach a floating IP to see it here"
818+
colSpan={3}
819+
/>
820+
)}
821+
</MiniTable.Body>
822+
</MiniTable.Table>
817823
<Button
824+
variant="secondary"
818825
size="sm"
819826
className="shrink-0"
820827
disabled={availableFloatingIps.length === 0}
@@ -825,7 +832,6 @@ const AdvancedAccordion = ({
825832
</Button>
826833
</div>
827834
)}
828-
829835
<Modal
830836
isOpen={floatingIpModalOpen}
831837
onDismiss={closeFloatingIpModal}

app/ui/lib/MiniTable.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Error16Icon } from '@oxide/design-system/icons/react'
1010
import { classed } from '~/util/classed'
1111

1212
import { Button } from './Button'
13+
import { EmptyMessage } from './EmptyMessage'
1314
import { Table as BigTable } from './Table'
1415

1516
type Children = { children: React.ReactNode }
@@ -36,6 +37,37 @@ export const Cell = ({ children }: Children) => {
3637
)
3738
}
3839

40+
export const EmptyState = (props: { title: string; body: string; colSpan: number }) => (
41+
<Row>
42+
<td colSpan={props.colSpan}>
43+
<div className="!m-0 !w-full !flex-col !border-none !bg-transparent !py-14">
44+
<EmptyMessage title={props.title} body={props.body} />
45+
</div>
46+
</td>
47+
</Row>
48+
)
49+
50+
export const InputCell = ({
51+
colSpan,
52+
defaultValue,
53+
placeholder,
54+
}: {
55+
colSpan?: number
56+
defaultValue: string
57+
placeholder: string
58+
}) => (
59+
<td colSpan={colSpan}>
60+
<div>
61+
<input
62+
type="text"
63+
className="text-sm m-0 w-full bg-transparent p-0 !outline-none text-default placeholder:text-quaternary"
64+
placeholder={placeholder}
65+
defaultValue={defaultValue}
66+
/>
67+
</div>
68+
</td>
69+
)
70+
3971
// followed this for icon in button best practices
4072
// https://www.sarasoueidan.com/blog/accessible-icon-buttons/
4173
export const RemoveCell = ({ onClick, label }: { onClick: () => void; label: string }) => (

app/ui/styles/components/mini-table.css

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,47 +11,54 @@
1111
border-spacing: 0px;
1212
}
1313

14-
& td {
15-
@apply relative px-0 pt-2;
16-
}
17-
14+
/* all rows */
1815
& tr {
16+
@apply bg-default;
1917
@apply relative;
2018
}
2119

20+
/* all cells */
21+
& td {
22+
@apply relative px-0 pt-2;
23+
}
24+
25+
/* a fake left border for all cells that aren't first */
2226
& td + td:before {
23-
@apply absolute bottom-[2px] top-[calc(0.5rem+1px)] block w-[1px] border-l opacity-40 border-accent-tertiary;
27+
@apply absolute bottom-[2px] top-[calc(0.5rem+1px)] block w-[1px] border-l border-secondary;
2428
content: ' ';
2529
}
2630

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

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

35-
& td:last-child > div {
36-
@apply w-12 justify-center pl-0 pr-0;
37-
}
38-
& td:last-child > div > button {
39-
@apply -mx-3 -my-3 flex items-center justify-center px-3 py-3;
40+
/* first cell's div */
41+
& td:first-child > div {
42+
@apply ml-2 rounded-l border-l;
4043
}
41-
& td:last-child > div:has(button:hover, button:focus) {
42-
@apply bg-accent-secondary-hover;
44+
45+
/* second-to-last cell's div */
46+
& td:nth-last-child(2) > div {
47+
@apply rounded-r border-r;
4348
}
4449

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

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

53-
& td:last-child > div {
54-
@apply mr-2 rounded-r border-r;
60+
& tr:last-child td {
61+
@apply pb-2;
5562
}
5663

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

6269
& thead tr:first-of-type th:last-of-type {
6370
border-top-right-radius: var(--border-radius-lg);
64-
@apply border-r;
71+
@apply w-8 border-r;
6572
}
6673

6774
& tbody tr:last-of-type td:first-of-type {

test/e2e/instance-create.e2e.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ test('can’t create a disk with a name that collides with the boot disk name',
257257
await page.fill('input[name=bootDiskName]', 'disk-11')
258258

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

0 commit comments

Comments
 (0)