Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of concept: Take column logic out of QueryTable #2110

Merged
merged 8 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 70 additions & 59 deletions app/pages/project/disks/DisksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*
* Copyright Oxide Computer Company
*/
import { createColumnHelper } from '@tanstack/react-table'
import { useCallback } from 'react'
import { Link, Outlet, type LoaderFunctionArgs } from 'react-router-dom'

import {
Expand All @@ -21,10 +23,11 @@ import { DiskStatusBadge } from '~/components/StatusBadge'
import { getProjectSelector, useProjectSelector, useToast } from '~/hooks'
import { confirmDelete } from '~/stores/confirm-delete'
import { DateCell } from '~/table/cells/DateCell'
import { defaultCell } from '~/table/cells/DefaultCell'
import { InstanceLinkCell } from '~/table/cells/InstanceLinkCell'
import { SizeCell } from '~/table/cells/SizeCell'
import type { MenuAction } from '~/table/columns/action-col'
import { useQueryTable } from '~/table/QueryTable'
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
import { useQueryTable2 } from '~/table/QueryTable2'
import { buttonStyle } from '~/ui/lib/Button'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
Expand Down Expand Up @@ -68,10 +71,31 @@ DisksPage.loader = async ({ params }: LoaderFunctionArgs) => {
return null
}

const colHelper = createColumnHelper<Disk>()

const staticCols = [
colHelper.accessor('name', { cell: defaultCell }),
// sneaky: rather than looking at particular states, just look at
// whether it has an instance field
colHelper.accessor((disk) => ('instance' in disk.state ? disk.state.instance : null), {
header: 'Attached to',
cell: (props) => <InstanceLinkCell value={props.getValue()} />,
}),
colHelper.accessor('size', { cell: (props) => <SizeCell value={props.getValue()} /> }),
colHelper.accessor('state.state', {
header: 'Status',
cell: (props) => <DiskStatusBadge status={props.getValue()} />,
}),
colHelper.accessor('timeCreated', {
header: 'Created',
cell: (props) => <DateCell value={props.getValue()} />,
}),
]

export function DisksPage() {
const queryClient = useApiQueryClient()
const projectSelector = useProjectSelector()
const { Table, Column } = useQueryTable('diskList', { query: projectSelector })
const { project } = useProjectSelector()
const { Table } = useQueryTable2('diskList', { query: { project } })
const addToast = useToast()

const deleteDisk = useApiMutation('diskDelete', {
Expand All @@ -94,72 +118,59 @@ export function DisksPage() {
},
})

const makeActions = (disk: Disk): MenuAction[] => [
{
label: 'Snapshot',
onActivate() {
addToast({ title: `Creating snapshot of disk '${disk.name}'` })
createSnapshot.mutate({
query: projectSelector,
body: {
name: genName(disk.name),
disk: disk.name,
description: '',
},
})
const makeActions = useCallback(
(disk: Disk): MenuAction[] => [
{
label: 'Snapshot',
onActivate() {
addToast({ title: `Creating snapshot of disk '${disk.name}'` })
createSnapshot.mutate({
query: { project },
body: {
name: genName(disk.name),
disk: disk.name,
description: '',
},
})
},
disabled: !diskCan.snapshot(disk) && (
<>
Only disks in state {fancifyStates(diskCan.snapshot.states)} can be snapshotted
</>
),
},
disabled: !diskCan.snapshot(disk) && (
<>Only disks in state {fancifyStates(diskCan.snapshot.states)} can be snapshotted</>
),
},
{
label: 'Delete',
onActivate: confirmDelete({
doDelete: () =>
deleteDisk.mutateAsync({ path: { disk: disk.name }, query: projectSelector }),
label: disk.name,
}),
disabled:
!diskCan.delete(disk) &&
(disk.state.state === 'attached' ? (
'Disk must be detached before it can be deleted'
) : (
<>Only disks in state {fancifyStates(diskCan.delete.states)} can be deleted</>
)),
},
]
{
label: 'Delete',
onActivate: confirmDelete({
doDelete: () =>
deleteDisk.mutateAsync({ path: { disk: disk.name }, query: { project } }),
label: disk.name,
}),
disabled:
!diskCan.delete(disk) &&
(disk.state.state === 'attached' ? (
'Disk must be detached before it can be deleted'
) : (
<>Only disks in state {fancifyStates(diskCan.delete.states)} can be deleted</>
)),
},
],
[addToast, createSnapshot, deleteDisk, project]
)

const columns = useColsWithActions(staticCols, makeActions)

return (
<>
<PageHeader>
<PageTitle icon={<Storage24Icon />}>Disks</PageTitle>
</PageHeader>
<TableActions>
<Link to={pb.disksNew(projectSelector)} className={buttonStyle({ size: 'sm' })}>
<Link to={pb.disksNew({ project })} className={buttonStyle({ size: 'sm' })}>
New Disk
</Link>
</TableActions>
<Table emptyState={<EmptyState />} makeActions={makeActions}>
<Column accessor="name" />
{/* TODO: show info about the instance it's attached to */}
<Column
id="attached-to"
header="Attached To"
accessor={(disk) =>
// sneaky: rather than looking at particular states, just look at
// whether it has an instance field
'instance' in disk.state ? disk.state.instance : null
}
cell={InstanceLinkCell}
/>
<Column header="Size" accessor="size" cell={SizeCell} />
<Column
id="status"
accessor={(row) => row.state.state}
cell={({ value }) => <DiskStatusBadge status={value} />}
/>
<Column header="Created" accessor="timeCreated" cell={DateCell} />
</Table>
<Table emptyState={<EmptyState />} columns={columns} />
<Outlet />
</>
)
Expand Down
79 changes: 46 additions & 33 deletions app/pages/system/networking/IpPoolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* Copyright Oxide Computer Company
*/

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

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

const colHelper = createColumnHelper<IpPool>()

const staticColumns = [
colHelper.accessor('name', { cell: makeLinkCell((pool) => pb.ipPool({ pool })) }),
colHelper.accessor('description', { cell: defaultCell }),
colHelper.accessor('name', {
id: 'Utilization',
header: 'Utilization',
cell: (props) => <UtilizationCell pool={props.getValue()} />,
}),
colHelper.accessor('timeCreated', {
header: 'Created',
cell: (props) => <DateCell value={props.getValue()} />,
}),
]

IpPoolsTab.loader = async function () {
await apiQueryClient.prefetchQuery('ipPoolList', { query: { limit: 25 } })
return null
}

export function IpPoolsTab() {
const navigate = useNavigate()
const { Table, Column } = useQueryTable('ipPoolList', {})
const { Table } = useQueryTable2('ipPoolList', {})
const { data: pools } = usePrefetchedApiQuery('ipPoolList', { query: { limit: 25 } })

const deletePool = useApiMutation('ipPoolDelete', {
Expand All @@ -63,24 +81,29 @@ export function IpPoolsTab() {
},
})

const makeActions = (pool: IpPool): MenuAction[] => [
{
label: 'Edit',
onActivate: () => {
// the edit view has its own loader, but we can make the modal open
// instantaneously by preloading the fetch result
apiQueryClient.setQueryData('ipPoolView', { path: { pool: pool.name } }, pool)
navigate(pb.ipPoolEdit({ pool: pool.name }))
const makeActions = useCallback(
(pool: IpPool): MenuAction[] => [
{
label: 'Edit',
onActivate: () => {
// the edit view has its own loader, but we can make the modal open
// instantaneously by preloading the fetch result
apiQueryClient.setQueryData('ipPoolView', { path: { pool: pool.name } }, pool)
navigate(pb.ipPoolEdit({ pool: pool.name }))
},
},
},
{
label: 'Delete',
onActivate: confirmDelete({
doDelete: () => deletePool.mutateAsync({ path: { pool: pool.name } }),
label: pool.name,
}),
},
]
{
label: 'Delete',
onActivate: confirmDelete({
doDelete: () => deletePool.mutateAsync({ path: { pool: pool.name } }),
label: pool.name,
}),
},
],
[deletePool, navigate]
)

const columns = useColsWithActions(staticColumns, makeActions)

useQuickActions(
useMemo(
Expand All @@ -106,17 +129,7 @@ export function IpPoolsTab() {
New IP Pool
</Link>
</div>
<Table emptyState={<EmptyState />} makeActions={makeActions}>
<Column accessor="name" cell={linkCell((pool) => pb.ipPool({ pool }))} />
<Column accessor="description" />
<Column
accessor="name"
id="Utilization"
header="Utilization"
cell={({ value }) => <UtilizationCell pool={value} />}
/>
<Column accessor="timeCreated" header="Created" cell={DateCell} />
</Table>
<Table emptyState={<EmptyState />} columns={columns} />
<Outlet />
</>
)
Expand Down
Loading
Loading