Skip to content

Commit 733bcb2

Browse files
committed
a hard one: IpPoolPage, have to handle rows with no ID field
1 parent 6132c6a commit 733bcb2

File tree

2 files changed

+47
-18
lines changed

2 files changed

+47
-18
lines changed

app/pages/system/networking/IpPoolPage.tsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ import { useForm } from 'react-hook-form'
1212
import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
1313

1414
import {
15+
apiq,
1516
apiQueryClient,
17+
getListQFn,
1618
parseIpUtilization,
19+
queryClient,
1720
useApiMutation,
1821
useApiQuery,
1922
useApiQueryClient,
2023
usePrefetchedApiQuery,
24+
usePrefetchedQuery,
2125
type IpPoolRange,
2226
type IpPoolSiloLink,
2327
} from '@oxide/api'
@@ -38,7 +42,7 @@ import { SkeletonCell } from '~/table/cells/EmptyCell'
3842
import { LinkCell } from '~/table/cells/LinkCell'
3943
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
4044
import { Columns } from '~/table/columns/common'
41-
import { PAGE_SIZE, useQueryTable } from '~/table/QueryTable'
45+
import { useQueryTable } from '~/table/QueryTable2'
4246
import { toComboboxItems } from '~/ui/lib/Combobox'
4347
import { CreateButton, CreateLink } from '~/ui/lib/CreateButton'
4448
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
@@ -51,14 +55,16 @@ import { ALL_ISH } from '~/util/consts'
5155
import { docLinks } from '~/util/links'
5256
import { pb } from '~/util/path-builder'
5357

54-
const query = { limit: PAGE_SIZE }
58+
const ipPoolView = (pool: string) => apiq('ipPoolView', { path: { pool } })
59+
const ipPoolSiloList = (pool: string) => getListQFn('ipPoolSiloList', { path: { pool } })
60+
const ipPoolRangeList = (pool: string) => getListQFn('ipPoolRangeList', { path: { pool } })
5561

5662
export async function loader({ params }: LoaderFunctionArgs) {
5763
const { pool } = getIpPoolSelector(params)
5864
await Promise.all([
59-
apiQueryClient.prefetchQuery('ipPoolView', { path: { pool } }),
60-
apiQueryClient.prefetchQuery('ipPoolSiloList', { path: { pool }, query }),
61-
apiQueryClient.prefetchQuery('ipPoolRangeList', { path: { pool }, query }),
65+
queryClient.prefetchQuery(ipPoolView(pool)),
66+
queryClient.prefetchQuery(ipPoolSiloList(pool).optionsFn()),
67+
queryClient.prefetchQuery(ipPoolRangeList(pool).optionsFn()),
6268
apiQueryClient.prefetchQuery('ipPoolUtilizationView', { path: { pool } }),
6369

6470
// fetch silos and preload into RQ cache so fetches by ID in SiloNameFromId
@@ -76,11 +82,10 @@ export async function loader({ params }: LoaderFunctionArgs) {
7682
Component.displayName = 'IpPoolPage'
7783
export function Component() {
7884
const poolSelector = useIpPoolSelector()
79-
const { data: pool } = usePrefetchedApiQuery('ipPoolView', { path: poolSelector })
80-
const { data: ranges } = usePrefetchedApiQuery('ipPoolRangeList', {
81-
path: poolSelector,
82-
query,
83-
})
85+
const { data: pool } = usePrefetchedQuery(ipPoolView(poolSelector.pool))
86+
const { data: ranges } = usePrefetchedQuery(
87+
ipPoolRangeList(poolSelector.pool).optionsFn()
88+
)
8489
const navigate = useNavigate()
8590
const { mutateAsync: deletePool } = useApiMutation('ipPoolDelete', {
8691
onSuccess(_data, variables) {
@@ -190,7 +195,6 @@ const ipRangesStaticCols = [
190195

191196
function IpRangesTable() {
192197
const { pool } = useIpPoolSelector()
193-
const { Table } = useQueryTable('ipPoolRangeList', { path: { pool } })
194198
const queryClient = useApiQueryClient()
195199

196200
const { mutateAsync: removeRange } = useApiMutation('ipPoolRangeRemove', {
@@ -239,13 +243,14 @@ function IpRangesTable() {
239243
[pool, removeRange]
240244
)
241245
const columns = useColsWithActions(ipRangesStaticCols, makeRangeActions)
246+
const { table } = useQueryTable({ query: ipPoolRangeList(pool), columns, emptyState })
242247

243248
return (
244249
<>
245250
<div className="mb-3 flex justify-end">
246251
<CreateLink to={pb.ipPoolRangeAdd({ pool })}>Add range</CreateLink>
247252
</div>
248-
<Table columns={columns} emptyState={emptyState} />
253+
{table}
249254
</>
250255
)
251256
}
@@ -283,7 +288,6 @@ const silosStaticCols = [
283288
function LinkedSilosTable() {
284289
const poolSelector = useIpPoolSelector()
285290
const queryClient = useApiQueryClient()
286-
const { Table } = useQueryTable('ipPoolSiloList', { path: poolSelector })
287291

288292
const { mutateAsync: unlinkSilo } = useApiMutation('ipPoolSiloUnlink', {
289293
onSuccess() {
@@ -335,12 +339,19 @@ function LinkedSilosTable() {
335339
)
336340

337341
const columns = useColsWithActions(silosStaticCols, makeActions)
342+
const { table } = useQueryTable({
343+
query: ipPoolSiloList(poolSelector.pool),
344+
columns,
345+
emptyState,
346+
getId: (link) => link.siloId,
347+
})
348+
338349
return (
339350
<>
340351
<div className="mb-3 flex justify-end">
341352
<CreateButton onClick={() => setShowLinkModal(true)}>Link silo</CreateButton>
342353
</div>
343-
<Table columns={columns} emptyState={emptyState} />
354+
{table}
344355
{showLinkModal && <LinkSiloModal onDismiss={() => setShowLinkModal(false)} />}
345356
</>
346357
)

app/table/QueryTable2.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,18 @@ type QueryTableProps<TItem> = {
2525
// React Table does the same in the type of `columns` on `useReactTable`
2626
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2727
columns: ColumnDef<TItem, any>[]
28-
}
28+
// Require getId if and only if TItem does not have an id field. Something
29+
// to keep in mind for the future: if instead we used the `select` transform
30+
// function on the query to add an ID to every row, we could just require TItem
31+
// to extend `{ id: string }`, and we wouldn't need this `getId` function. The
32+
// difficulty I ran into was propagating the result of `select` through the API
33+
// query options helpers. But I think it can be done.
34+
} & (TItem extends { id: string }
35+
? { getId?: never }
36+
: {
37+
/** Needed if and only if `TItem` has no `id` field */
38+
getId: (row: TItem) => string
39+
})
2940

3041
/**
3142
* Reset scroll to top when clicking * next/prev to change page but not,
@@ -45,11 +56,12 @@ function useScrollReset(triggerDep: string | undefined) {
4556
}
4657

4758
// require ID only so we can use it in getRowId
48-
export function useQueryTable<TItem extends { id: string }>({
59+
export function useQueryTable<TItem>({
4960
query,
5061
rowHeight = 'small',
5162
emptyState,
5263
columns,
64+
getId,
5365
}: QueryTableProps<TItem>) {
5466
const { currentPage, goToNextPage, goToPrevPage, hasPrev } = usePagination()
5567
const queryOptions = query.optionsFn(currentPage)
@@ -59,15 +71,21 @@ export function useQueryTable<TItem extends { id: string }>({
5971
const { data, isPlaceholderData } = queryResult
6072
const tableData = useMemo(() => data?.items || [], [data])
6173

74+
const getRowId = getId
75+
? getId
76+
: // @ts-expect-error we know from the types that getId is only defined when there is no ID
77+
(row: TItem) => row.id as string
78+
6279
// trigger by first item ID and not, e.g., currentPage because currentPage
6380
// changes as soon as you click Next, while the item ID doesn't change until
6481
// the page actually changes.
65-
const requestScrollReset = useScrollReset(tableData.at(0)?.id)
82+
const first = tableData.at(0)
83+
const requestScrollReset = useScrollReset(first ? getRowId(first) : undefined)
6684

6785
const table = useReactTable({
6886
columns,
6987
data: tableData,
70-
getRowId: (row) => row.id,
88+
getRowId,
7189
getCoreRowModel: getCoreRowModel(),
7290
manualPagination: true,
7391
})

0 commit comments

Comments
 (0)