Skip to content

Commit 156c082

Browse files
authored
Proof of concept: Take column logic out of QueryTable (#2110)
* fix IP pool bug by not doing the bad columns thing * take that delay back out! * time created column header * shorter defaultCell * try adding an assert before goBack for safari failures * neater linkCell * convert disks table to useQueryTable2 to test pagination * add useColWithActions to make adding actions column cleaner
1 parent cfdb3aa commit 156c082

File tree

9 files changed

+275
-99
lines changed

9 files changed

+275
-99
lines changed

app/pages/project/disks/DisksPage.tsx

Lines changed: 70 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
import { createColumnHelper } from '@tanstack/react-table'
9+
import { useCallback } from 'react'
810
import { Link, Outlet, type LoaderFunctionArgs } from 'react-router-dom'
911

1012
import {
@@ -21,10 +23,11 @@ import { DiskStatusBadge } from '~/components/StatusBadge'
2123
import { getProjectSelector, useProjectSelector, useToast } from '~/hooks'
2224
import { confirmDelete } from '~/stores/confirm-delete'
2325
import { DateCell } from '~/table/cells/DateCell'
26+
import { defaultCell } from '~/table/cells/DefaultCell'
2427
import { InstanceLinkCell } from '~/table/cells/InstanceLinkCell'
2528
import { SizeCell } from '~/table/cells/SizeCell'
26-
import type { MenuAction } from '~/table/columns/action-col'
27-
import { useQueryTable } from '~/table/QueryTable'
29+
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
30+
import { useQueryTable2 } from '~/table/QueryTable2'
2831
import { buttonStyle } from '~/ui/lib/Button'
2932
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
3033
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
@@ -68,10 +71,31 @@ DisksPage.loader = async ({ params }: LoaderFunctionArgs) => {
6871
return null
6972
}
7073

74+
const colHelper = createColumnHelper<Disk>()
75+
76+
const staticCols = [
77+
colHelper.accessor('name', { cell: defaultCell }),
78+
// sneaky: rather than looking at particular states, just look at
79+
// whether it has an instance field
80+
colHelper.accessor((disk) => ('instance' in disk.state ? disk.state.instance : null), {
81+
header: 'Attached to',
82+
cell: (props) => <InstanceLinkCell value={props.getValue()} />,
83+
}),
84+
colHelper.accessor('size', { cell: (props) => <SizeCell value={props.getValue()} /> }),
85+
colHelper.accessor('state.state', {
86+
header: 'Status',
87+
cell: (props) => <DiskStatusBadge status={props.getValue()} />,
88+
}),
89+
colHelper.accessor('timeCreated', {
90+
header: 'Created',
91+
cell: (props) => <DateCell value={props.getValue()} />,
92+
}),
93+
]
94+
7195
export function DisksPage() {
7296
const queryClient = useApiQueryClient()
73-
const projectSelector = useProjectSelector()
74-
const { Table, Column } = useQueryTable('diskList', { query: projectSelector })
97+
const { project } = useProjectSelector()
98+
const { Table } = useQueryTable2('diskList', { query: { project } })
7599
const addToast = useToast()
76100

77101
const deleteDisk = useApiMutation('diskDelete', {
@@ -94,72 +118,59 @@ export function DisksPage() {
94118
},
95119
})
96120

97-
const makeActions = (disk: Disk): MenuAction[] => [
98-
{
99-
label: 'Snapshot',
100-
onActivate() {
101-
addToast({ title: `Creating snapshot of disk '${disk.name}'` })
102-
createSnapshot.mutate({
103-
query: projectSelector,
104-
body: {
105-
name: genName(disk.name),
106-
disk: disk.name,
107-
description: '',
108-
},
109-
})
121+
const makeActions = useCallback(
122+
(disk: Disk): MenuAction[] => [
123+
{
124+
label: 'Snapshot',
125+
onActivate() {
126+
addToast({ title: `Creating snapshot of disk '${disk.name}'` })
127+
createSnapshot.mutate({
128+
query: { project },
129+
body: {
130+
name: genName(disk.name),
131+
disk: disk.name,
132+
description: '',
133+
},
134+
})
135+
},
136+
disabled: !diskCan.snapshot(disk) && (
137+
<>
138+
Only disks in state {fancifyStates(diskCan.snapshot.states)} can be snapshotted
139+
</>
140+
),
110141
},
111-
disabled: !diskCan.snapshot(disk) && (
112-
<>Only disks in state {fancifyStates(diskCan.snapshot.states)} can be snapshotted</>
113-
),
114-
},
115-
{
116-
label: 'Delete',
117-
onActivate: confirmDelete({
118-
doDelete: () =>
119-
deleteDisk.mutateAsync({ path: { disk: disk.name }, query: projectSelector }),
120-
label: disk.name,
121-
}),
122-
disabled:
123-
!diskCan.delete(disk) &&
124-
(disk.state.state === 'attached' ? (
125-
'Disk must be detached before it can be deleted'
126-
) : (
127-
<>Only disks in state {fancifyStates(diskCan.delete.states)} can be deleted</>
128-
)),
129-
},
130-
]
142+
{
143+
label: 'Delete',
144+
onActivate: confirmDelete({
145+
doDelete: () =>
146+
deleteDisk.mutateAsync({ path: { disk: disk.name }, query: { project } }),
147+
label: disk.name,
148+
}),
149+
disabled:
150+
!diskCan.delete(disk) &&
151+
(disk.state.state === 'attached' ? (
152+
'Disk must be detached before it can be deleted'
153+
) : (
154+
<>Only disks in state {fancifyStates(diskCan.delete.states)} can be deleted</>
155+
)),
156+
},
157+
],
158+
[addToast, createSnapshot, deleteDisk, project]
159+
)
160+
161+
const columns = useColsWithActions(staticCols, makeActions)
131162

132163
return (
133164
<>
134165
<PageHeader>
135166
<PageTitle icon={<Storage24Icon />}>Disks</PageTitle>
136167
</PageHeader>
137168
<TableActions>
138-
<Link to={pb.disksNew(projectSelector)} className={buttonStyle({ size: 'sm' })}>
169+
<Link to={pb.disksNew({ project })} className={buttonStyle({ size: 'sm' })}>
139170
New Disk
140171
</Link>
141172
</TableActions>
142-
<Table emptyState={<EmptyState />} makeActions={makeActions}>
143-
<Column accessor="name" />
144-
{/* TODO: show info about the instance it's attached to */}
145-
<Column
146-
id="attached-to"
147-
header="Attached To"
148-
accessor={(disk) =>
149-
// sneaky: rather than looking at particular states, just look at
150-
// whether it has an instance field
151-
'instance' in disk.state ? disk.state.instance : null
152-
}
153-
cell={InstanceLinkCell}
154-
/>
155-
<Column header="Size" accessor="size" cell={SizeCell} />
156-
<Column
157-
id="status"
158-
accessor={(row) => row.state.state}
159-
cell={({ value }) => <DiskStatusBadge status={value} />}
160-
/>
161-
<Column header="Created" accessor="timeCreated" cell={DateCell} />
162-
</Table>
173+
<Table emptyState={<EmptyState />} columns={columns} />
163174
<Outlet />
164175
</>
165176
)

app/pages/system/networking/IpPoolsTab.tsx

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* Copyright Oxide Computer Company
77
*/
88

9-
import { useMemo } from 'react'
9+
import { createColumnHelper } from '@tanstack/react-table'
10+
import { useCallback, useMemo } from 'react'
1011
import { Link, Outlet, useNavigate } from 'react-router-dom'
1112

1213
import {
@@ -22,10 +23,11 @@ import { IpUtilCell } from '~/components/IpPoolUtilization'
2223
import { useQuickActions } from '~/hooks'
2324
import { confirmDelete } from '~/stores/confirm-delete'
2425
import { DateCell } from '~/table/cells/DateCell'
26+
import { defaultCell } from '~/table/cells/DefaultCell'
2527
import { SkeletonCell } from '~/table/cells/EmptyCell'
26-
import { linkCell } from '~/table/cells/LinkCell'
27-
import type { MenuAction } from '~/table/columns/action-col'
28-
import { useQueryTable } from '~/table/QueryTable'
28+
import { makeLinkCell } from '~/table/cells/LinkCell'
29+
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
30+
import { useQueryTable2 } from '~/table/QueryTable2'
2931
import { buttonStyle } from '~/ui/lib/Button'
3032
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
3133
import { pb } from '~/util/path-builder'
@@ -47,14 +49,30 @@ function UtilizationCell({ pool }: { pool: string }) {
4749
return <IpUtilCell {...data} />
4850
}
4951

52+
const colHelper = createColumnHelper<IpPool>()
53+
54+
const staticColumns = [
55+
colHelper.accessor('name', { cell: makeLinkCell((pool) => pb.ipPool({ pool })) }),
56+
colHelper.accessor('description', { cell: defaultCell }),
57+
colHelper.accessor('name', {
58+
id: 'Utilization',
59+
header: 'Utilization',
60+
cell: (props) => <UtilizationCell pool={props.getValue()} />,
61+
}),
62+
colHelper.accessor('timeCreated', {
63+
header: 'Created',
64+
cell: (props) => <DateCell value={props.getValue()} />,
65+
}),
66+
]
67+
5068
IpPoolsTab.loader = async function () {
5169
await apiQueryClient.prefetchQuery('ipPoolList', { query: { limit: 25 } })
5270
return null
5371
}
5472

5573
export function IpPoolsTab() {
5674
const navigate = useNavigate()
57-
const { Table, Column } = useQueryTable('ipPoolList', {})
75+
const { Table } = useQueryTable2('ipPoolList', {})
5876
const { data: pools } = usePrefetchedApiQuery('ipPoolList', { query: { limit: 25 } })
5977

6078
const deletePool = useApiMutation('ipPoolDelete', {
@@ -63,24 +81,29 @@ export function IpPoolsTab() {
6381
},
6482
})
6583

66-
const makeActions = (pool: IpPool): MenuAction[] => [
67-
{
68-
label: 'Edit',
69-
onActivate: () => {
70-
// the edit view has its own loader, but we can make the modal open
71-
// instantaneously by preloading the fetch result
72-
apiQueryClient.setQueryData('ipPoolView', { path: { pool: pool.name } }, pool)
73-
navigate(pb.ipPoolEdit({ pool: pool.name }))
84+
const makeActions = useCallback(
85+
(pool: IpPool): MenuAction[] => [
86+
{
87+
label: 'Edit',
88+
onActivate: () => {
89+
// the edit view has its own loader, but we can make the modal open
90+
// instantaneously by preloading the fetch result
91+
apiQueryClient.setQueryData('ipPoolView', { path: { pool: pool.name } }, pool)
92+
navigate(pb.ipPoolEdit({ pool: pool.name }))
93+
},
7494
},
75-
},
76-
{
77-
label: 'Delete',
78-
onActivate: confirmDelete({
79-
doDelete: () => deletePool.mutateAsync({ path: { pool: pool.name } }),
80-
label: pool.name,
81-
}),
82-
},
83-
]
95+
{
96+
label: 'Delete',
97+
onActivate: confirmDelete({
98+
doDelete: () => deletePool.mutateAsync({ path: { pool: pool.name } }),
99+
label: pool.name,
100+
}),
101+
},
102+
],
103+
[deletePool, navigate]
104+
)
105+
106+
const columns = useColsWithActions(staticColumns, makeActions)
84107

85108
useQuickActions(
86109
useMemo(
@@ -106,17 +129,7 @@ export function IpPoolsTab() {
106129
New IP Pool
107130
</Link>
108131
</div>
109-
<Table emptyState={<EmptyState />} makeActions={makeActions}>
110-
<Column accessor="name" cell={linkCell((pool) => pb.ipPool({ pool }))} />
111-
<Column accessor="description" />
112-
<Column
113-
accessor="name"
114-
id="Utilization"
115-
header="Utilization"
116-
cell={({ value }) => <UtilizationCell pool={value} />}
117-
/>
118-
<Column accessor="timeCreated" header="Created" cell={DateCell} />
119-
</Table>
132+
<Table emptyState={<EmptyState />} columns={columns} />
120133
<Outlet />
121134
</>
122135
)

0 commit comments

Comments
 (0)