Skip to content

Commit d583ae7

Browse files
authored
Display full sled policy in table (#2624)
* make sled policy column display the right data * use beautiful column grouping * fix the e2es * make display more like API data
1 parent fe973ed commit d583ae7

File tree

7 files changed

+92
-50
lines changed

7 files changed

+92
-50
lines changed

app/pages/system/UtilizationPage.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ function UsageTab() {
162162
<Table className="w-full">
163163
<Table.Header>
164164
<Table.HeaderRow>
165-
<Table.HeadCell>Silo</Table.HeadCell>
165+
<Table.HeadCell data-test-ignore></Table.HeadCell>
166166
{/* data-test-ignore makes the row asserts work in the e2e tests */}
167167
<Table.HeadCell colSpan={3} data-test-ignore>
168168
Provisioned / Quota
@@ -173,7 +173,7 @@ function UsageTab() {
173173
<Table.HeadCell data-test-ignore></Table.HeadCell>
174174
</Table.HeaderRow>
175175
<Table.HeaderRow>
176-
<Table.HeadCell data-test-ignore></Table.HeadCell>
176+
<Table.HeadCell>Silo</Table.HeadCell>
177177
<Table.HeadCell>CPU</Table.HeadCell>
178178
<Table.HeadCell>Memory</Table.HeadCell>
179179
<Table.HeadCell>Storage</Table.HeadCell>

app/pages/system/inventory/SledsTab.tsx

+41-26
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,18 @@ import {
1616
} from '@oxide/api'
1717
import { Servers24Icon } from '@oxide/design-system/icons/react'
1818

19+
import { EmptyCell } from '~/table/cells/EmptyCell'
1920
import { makeLinkCell } from '~/table/cells/LinkCell'
2021
import { useQueryTable } from '~/table/QueryTable'
2122
import { Badge, type BadgeColor } from '~/ui/lib/Badge'
2223
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
2324
import { pb } from '~/util/path-builder'
2425

25-
const POLICY_KIND_BADGE_COLORS: Record<SledPolicy['kind'], BadgeColor> = {
26-
in_service: 'default',
27-
expunged: 'neutral',
28-
}
29-
3026
const STATE_BADGE_COLORS: Record<SledState, BadgeColor> = {
3127
active: 'default',
3228
decommissioned: 'neutral',
3329
}
3430

35-
const EmptyState = () => {
36-
return (
37-
<EmptyMessage
38-
icon={<Servers24Icon />}
39-
title="Something went wrong"
40-
body="We expected some racks here, but none were found"
41-
/>
42-
)
43-
}
44-
4531
const sledList = getListQFn('sledList', {})
4632

4733
export async function loader() {
@@ -55,16 +41,45 @@ const staticCols = [
5541
cell: makeLinkCell((sledId) => pb.sled({ sledId })),
5642
}),
5743
// TODO: colHelper.accessor('baseboard.serviceAddress', { header: 'service address' }),
58-
colHelper.accessor('baseboard.part', { header: 'part number' }),
59-
colHelper.accessor('baseboard.serial', { header: 'serial number' }),
60-
colHelper.accessor('baseboard.revision', { header: 'revision' }),
61-
colHelper.accessor('policy.kind', {
62-
header: 'policy',
63-
cell: (info) => (
64-
<Badge color={POLICY_KIND_BADGE_COLORS[info.getValue()]}>
65-
{info.getValue().replace(/_/g, ' ')}
66-
</Badge>
67-
),
44+
colHelper.group({
45+
id: 'baseboard',
46+
header: 'Baseboard',
47+
columns: [
48+
colHelper.accessor('baseboard.part', { header: 'part number' }),
49+
colHelper.accessor('baseboard.serial', { header: 'serial number' }),
50+
colHelper.accessor('baseboard.revision', { header: 'revision' }),
51+
],
52+
}),
53+
colHelper.group({
54+
id: 'policy',
55+
header: 'Policy',
56+
columns: [
57+
colHelper.accessor('policy', {
58+
header: 'Kind',
59+
cell: (info) => {
60+
// need to cast because inference is broken inside groups
61+
// https://github.com/TanStack/table/issues/5065
62+
const policy: SledPolicy = info.getValue()
63+
return policy.kind === 'expunged' ? (
64+
<Badge color="neutral">Expunged</Badge>
65+
) : (
66+
<Badge>In service</Badge>
67+
)
68+
},
69+
}),
70+
colHelper.accessor('policy', {
71+
header: 'Provision policy',
72+
cell: (info) => {
73+
const policy: SledPolicy = info.getValue()
74+
if (policy.kind === 'expunged') return <EmptyCell />
75+
return policy.provisionPolicy === 'provisionable' ? (
76+
<Badge>Provisionable</Badge>
77+
) : (
78+
<Badge color="neutral">Not provisionable</Badge>
79+
)
80+
},
81+
}),
82+
],
6883
}),
6984
colHelper.accessor('state', {
7085
cell: (info) => (
@@ -75,7 +90,7 @@ const staticCols = [
7590

7691
Component.displayName = 'SledsTab'
7792
export function Component() {
78-
const emptyState = <EmptyState />
93+
const emptyState = <EmptyMessage icon={<Servers24Icon />} title="No sleds found" />
7994
const { table } = useQueryTable({ query: sledList, columns: staticCols, emptyState })
8095
return table
8196
}

app/table/Table.tsx

+24-12
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,30 @@ export const Table = <TData,>({
2929
}: TableProps<TData>) => (
3030
<UITable {...tableProps}>
3131
<UITable.Header>
32-
{table.getHeaderGroups().map((headerGroup) => (
33-
<UITable.HeaderRow key={headerGroup.id}>
34-
{headerGroup.headers.map((header) => (
35-
<UITable.HeadCell
36-
key={header.id}
37-
className={header.column.columnDef.meta?.thClassName}
38-
>
39-
{flexRender(header.column.columnDef.header, header.getContext())}
40-
</UITable.HeadCell>
41-
))}
42-
</UITable.HeaderRow>
43-
))}
32+
{table.getHeaderGroups().map((headerGroup) => {
33+
console.log(headerGroup)
34+
return (
35+
<UITable.HeaderRow key={headerGroup.id}>
36+
{headerGroup.headers.map((header) => (
37+
<UITable.HeadCell
38+
key={header.id}
39+
className={header.column.columnDef.meta?.thClassName}
40+
colSpan={header.colSpan}
41+
>
42+
{
43+
// Placeholder concept is for when grouped columns are
44+
// combined with regular columns. The regular column only
45+
// needs one entry in the stack of header cells, so the others
46+
// have isPlacholder=true. See sleds table for an example.
47+
header.isPlaceholder
48+
? null
49+
: flexRender(header.column.columnDef.header, header.getContext())
50+
}
51+
</UITable.HeadCell>
52+
))}
53+
</UITable.HeaderRow>
54+
)
55+
})}
4456
</UITable.Header>
4557
<UITable.Body>
4658
{table.getRowModel().rows.map((row) => {

mock-api/sled.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const sleds: Json<Sled[]> = [
3535
rack_id: '759a1c80-4bff-4d0b-97ce-b482ca936724',
3636
policy: {
3737
kind: 'in_service',
38-
provision_policy: 'provisionable',
38+
provision_policy: 'non_provisionable',
3939
},
4040
state: 'active',
4141
baseboard: {

test/e2e/instance-create.e2e.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -474,12 +474,13 @@ test('attaches a floating IP; disables button when no IPs available', async ({ p
474474
await page.getByRole('tab', { name: 'Networking' }).click()
475475

476476
// ensure External IPs table has rows for the Ephemeral IP and the Floating IP
477-
await expectRowVisible(page.getByRole('table'), {
477+
const ipsTable = page.getByRole('table', { name: 'External IPs' })
478+
await expectRowVisible(ipsTable, {
478479
ip: '123.4.56.0',
479480
Kind: 'ephemeral',
480481
name: '—',
481482
})
482-
await expectRowVisible(page.getByRole('table'), {
483+
await expectRowVisible(ipsTable, {
483484
ip: floatingIp.ip,
484485
Kind: 'floating',
485486
name: floatingIp.name,

test/e2e/inventory.e2e.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,40 @@ test('Sled inventory page', async ({ page }) => {
2020
await expect(sledsTab).toHaveClass(/is-selected/)
2121

2222
const sledsTable = page.getByRole('table')
23+
// expectRowVisible currently only looks at the last header row in case of
24+
// grouping, hence the slightly weird column names
25+
await expectRowVisible(sledsTable, {
26+
id: sleds[0].id,
27+
'serial number': sleds[0].baseboard.serial,
28+
Kind: 'In service',
29+
'Provision policy': 'Provisionable',
30+
state: 'active',
31+
})
2332
await expectRowVisible(sledsTable, {
2433
id: sleds[1].id,
2534
'serial number': sleds[1].baseboard.serial,
26-
policy: 'in service',
35+
Kind: 'In service',
36+
'Provision policy': 'Not provisionable',
2737
state: 'active',
2838
})
2939
await expectRowVisible(sledsTable, {
3040
id: sleds[2].id,
3141
'serial number': sleds[2].baseboard.serial,
32-
policy: 'expunged',
42+
Kind: 'Expunged',
43+
'Provision policy': '—',
3344
state: 'active',
3445
})
3546
await expectRowVisible(sledsTable, {
3647
id: sleds[3].id,
3748
'serial number': sleds[3].baseboard.serial,
38-
policy: 'expunged',
49+
Kind: 'Expunged',
50+
'Provision policy': '—',
3951
state: 'decommissioned',
4052
})
4153

4254
// Visit the sled detail page of the first sled
4355
await sledsTable.getByRole('link').first().click()
4456

45-
// TODO: Once sled location is piped through this'll need to be dynamic
4657
await expectVisible(page, ['role=heading[name*="Sled"]'])
4758

4859
const instancesTab = page.getByRole('tab', { name: 'Instances' })

test/e2e/utils.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ export async function expectRowVisible(
6969
) {
7070
// wait for header and rows to avoid flake town
7171
const headerLoc = table.locator('thead >> role=cell')
72-
await headerLoc.locator('nth=0').waitFor() // nth=0 bc error if there's more than 1
72+
await headerLoc.first().waitFor() // nth=0 bc error if there's more than 1
7373

7474
const rowLoc = table.locator('tbody >> role=row')
75-
await rowLoc.locator('nth=0').waitFor()
75+
await rowLoc.first().waitFor()
7676

7777
async function getRows() {
7878
// need to pull header keys every time because the whole page can change
@@ -81,7 +81,10 @@ export async function expectRowVisible(
8181
// filter out data-test-ignore is specifically for making the header cells
8282
// match up with the contents on the double-header utilization table
8383
const headerKeys = await table
84-
.locator('thead >> th:not([data-test-ignore])')
84+
.locator('thead')
85+
.getByRole('row')
86+
.last()
87+
.locator('th:not([data-test-ignore])')
8588
.allTextContents()
8689

8790
const rows = await map(table.locator('tbody >> role=row'), async (row) => {

0 commit comments

Comments
 (0)