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

Add read-only UI for internet gateways #2488

Merged
merged 80 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
a4447af
Update OMICRON_VERSION
charliepark Oct 4, 2024
07d9697
update msw handlers
charliepark Oct 4, 2024
9547005
Other changes from initial branch
charliepark Oct 4, 2024
f7c4b10
IP Pools tab in place
charliepark Oct 5, 2024
aa2f363
IP Pool ID is copyable from IP Pools tab
charliepark Oct 6, 2024
302cd19
Add content to IP Addresses tab and root page
charliepark Oct 6, 2024
c038387
Update OMICRON_VERSION to sha with internet gateways
charliepark Oct 7, 2024
d778b70
small tweaks
charliepark Oct 7, 2024
0a62bef
remove commented-out code
charliepark Oct 7, 2024
f3d8d41
Use proper truncate component
charliepark Oct 7, 2024
bc1a103
adjust path-builder spec
charliepark Oct 7, 2024
7863939
Add breadcrumb nav for internet gateways
charliepark Oct 7, 2024
721a900
Update default internet gateway IP pool to reflect actual default values
charliepark Oct 7, 2024
b72ae0e
update unrelated test
charliepark Oct 7, 2024
a6625e5
Merge branch 'main' into internet-gateways-2
charliepark Oct 9, 2024
cdbd4a2
Remove n+1 query on IpPools
charliepark Oct 9, 2024
a53479b
Add IpPoolCell
charliepark Oct 9, 2024
55e49bc
Remove code that we'll add separately
charliepark Oct 9, 2024
89a6eab
Upgrade OMICRON_VERSION
charliepark Oct 9, 2024
5ca7a06
Merge main, resolve conflicts
charliepark Oct 9, 2024
39c8ca0
No need to extract UtilizationCell now
charliepark Oct 9, 2024
6f5e7ff
Get IP Pool tab for Internet Gateways working
charliepark Oct 10, 2024
ac61ea2
Merge branch 'main' into internet-gateways-2
david-crespo Oct 30, 2024
b344f81
Merge main, resolve conflicts, including TopBar breadcrumbs
charliepark Nov 19, 2024
1b63138
Update snapshot, but there might still be some other issues to work out
charliepark Nov 19, 2024
1caf6cd
Some headway, but screen is still blank
charliepark Nov 20, 2024
0c64c54
Update snapshots for test
charliepark Nov 20, 2024
445571a
Merge branch 'main' into internet-gateways-2
charliepark Nov 20, 2024
1b9fecc
Update routes; fix mock data
charliepark Nov 20, 2024
38ef9c1
update tests with mock data, but this should probably get pulled to a…
charliepark Nov 20, 2024
8fb8057
Simplify mock data; renaming the default IP Pool to default was unnec…
charliepark Nov 21, 2024
a34ddf7
Merge branch 'main' into internet-gateways-2
david-crespo Nov 23, 2024
dfc3223
convert to new useQueryTable
david-crespo Nov 23, 2024
b5b7eb6
Merge branch 'main' into internet-gateways-2
charliepark Dec 2, 2024
fdc2995
Add internet gateway combobox to router route target field
charliepark Dec 2, 2024
0dfb201
Merge main
charliepark Dec 4, 2024
d3f9e1d
Sidebar for Internet Gateway coming together
charliepark Dec 5, 2024
845940f
Bot commit: format with prettier
github-actions[bot] Dec 5, 2024
000c4f9
DOM shuffling
charliepark Dec 5, 2024
98238ec
Update mock data
charliepark Dec 5, 2024
08030ed
merge main and hopefully resolve issues
charliepark Dec 5, 2024
7097dc6
Update routes to handle new sidebar and main tab together
charliepark Dec 5, 2024
74066fd
Reorder internet gateway sidebar
charliepark Dec 5, 2024
daa24f0
Update paths and snapshots
charliepark Dec 5, 2024
3f04264
use more common internet-gateway-edit syntax for filename
charliepark Dec 5, 2024
b729767
Merge origin/main into internet-gateways-2
david-crespo Dec 6, 2024
1df7ad4
Internet gateways modal tweak (#2607)
benjaminleonard Dec 6, 2024
ec8b67b
Add IP Address and IP Pool columns to Gateway table
charliepark Dec 6, 2024
4762f08
Merge branch 'main' into internet-gateways-2
david-crespo Dec 6, 2024
001971a
test update
charliepark Dec 6, 2024
79991a7
small tweaks to sidebar
charliepark Dec 6, 2024
109a60c
reverting back to vertical table for now, to render IP Pool alonside …
charliepark Dec 6, 2024
abb82ba
Update copy when missing pool or ip address
charliepark Dec 6, 2024
c5013d0
Add a test for internet gateway list and sidemodal
charliepark Dec 9, 2024
c758b5c
Add routes targeting gateway to table
charliepark Dec 10, 2024
7ec29f9
Better handle multiple route spacing; fix test
charliepark Dec 10, 2024
a39c154
Update side modal with routes targeting gateway
charliepark Dec 10, 2024
3d8cad8
Merge branch 'main' into internet-gateways-2
charliepark Dec 10, 2024
0f5b849
Update test for showing route
charliepark Dec 10, 2024
2ab1859
Tweaks to sidemodal
charliepark Dec 11, 2024
bd89623
move example gateway route to custom router, not default router
charliepark Dec 11, 2024
efb0c50
Update tests to reflect gateway route existing on custom router
charliepark Dec 11, 2024
ae9775e
Merge branch 'main' into internet-gateways-2
charliepark Dec 11, 2024
49dfca3
use more specific params in queries
charliepark Dec 11, 2024
7c7f9b7
use titleCrumb for Edit Internet Gateway
charliepark Dec 11, 2024
fb4ede5
fix RR leaf route without element warning
david-crespo Dec 11, 2024
6f91091
let's use a valid UUID
charliepark Dec 11, 2024
2135c92
clean up InternetGatewayRoutes and call it as a component
david-crespo Dec 11, 2024
ca4962d
update the snapshot!
david-crespo Dec 11, 2024
c2d20ae
fix gnarly dependent promises in gateways loader
david-crespo Dec 11, 2024
5062ecc
Merge main and resolve conflict
charliepark Dec 12, 2024
49cfa15
update read only info box
david-crespo Dec 12, 2024
24dc844
clean up gateway routes fetch logic by extracting shared hook
david-crespo Dec 12, 2024
a6c0e7b
extract gateway data logic into a separate file
david-crespo Dec 12, 2024
46ef5d8
Use count of routes; link to sidemodal
charliepark Dec 13, 2024
64a7bbe
Use count of 0 instead of EmptyCell for route count
charliepark Dec 13, 2024
c4b35fe
use EmptyCell for zero routes, copy tweaks, sentence case
david-crespo Dec 13, 2024
9e1d53c
sentence case idp form heading
david-crespo Dec 13, 2024
bc3161a
minor: remove stub e2e test
david-crespo Dec 13, 2024
9dba7fb
merge main
david-crespo Dec 13, 2024
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
5 changes: 5 additions & 0 deletions app/api/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export type NetworkInterface = Readonly<Merge<Instance, { interface?: string }>>
export type Snapshot = Readonly<Merge<Project, { snapshot?: string }>>
export type Vpc = Readonly<Merge<Project, { vpc?: string }>>
export type VpcRouter = Readonly<Merge<Vpc, { router?: string }>>
export type InternetGateway = Readonly<Merge<Vpc, { gateway?: string }>>
export type InternetGatewayIpAddress = Readonly<
Merge<InternetGateway, { address?: string }>
>
export type InternetGatewayIpPool = Merge<InternetGateway, { pool?: string }>
export type VpcRouterRoute = Readonly<Merge<VpcRouter, { route?: string }>>
export type VpcSubnet = Readonly<Merge<Vpc, { subnet?: string }>>
export type FirewallRule = Readonly<Merge<Vpc, { rule?: string }>>
Expand Down
4 changes: 2 additions & 2 deletions app/forms/firewall-rules-common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,9 @@ export const CommonFields = ({ control, nameTaken, error }: CommonFieldsProps) =
target="_blank"
rel="noreferrer"
>
guest networking guide
Networking
</a>{' '}
and{' '}
guide and the{' '}
<a
href="https://docs.oxide.computer/api/vpc_firewall_rules_update"
// don't need color and hover color because message text is already color-info anyway
Expand Down
2 changes: 1 addition & 1 deletion app/forms/idp/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export function CreateIdpSideModalForm() {

<FormDivider />

<SideModal.Heading>Identity Provider</SideModal.Heading>
<SideModal.Heading>Identity provider</SideModal.Heading>
{/* TODO: help text */}
<TextField name="idpEntityId" label="Entity ID" required control={form.control} />
<TextField
Expand Down
2 changes: 1 addition & 1 deletion app/forms/idp/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export function EditIdpSideModalForm() {

<FormDivider />

<SideModal.Heading>Identity Provider</SideModal.Heading>
<SideModal.Heading>Identity provider</SideModal.Heading>
{/* TODO: help text */}
<TextField
name="idpEntityId"
Expand Down
57 changes: 37 additions & 20 deletions app/forms/vpc-router-route-common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@ import { TextField } from '~/components/form/fields/TextField'
import { useVpcRouterSelector } from '~/hooks/use-params'
import { toComboboxItems } from '~/ui/lib/Combobox'
import { Message } from '~/ui/lib/Message'
import { ALL_ISH } from '~/util/consts'
import { validateIp, validateIpNet } from '~/util/ip'

export type RouteFormValues = RouterRouteCreate | Required<RouterRouteUpdate>

export const routeFormMessage = {
vpcSubnetNotModifiable:
'Routes of type VPC Subnet within the system router are not modifiable',
internetGatewayTargetValue:
'For ‘Internet gateway’ targets, the value must be ‘outbound’',
// https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L201-L204
noNewRoutesOnSystemRouter: 'User-provided routes cannot be added to a system router',
// https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L300-L304
Expand Down Expand Up @@ -75,7 +74,7 @@ const destinationValueDescription: Record<RouteDestination['type'], string | und
const targetValuePlaceholder: Record<RouteTarget['type'], string | undefined> = {
ip: 'Enter an IP',
instance: 'Select an instance',
internet_gateway: undefined,
internet_gateway: 'Select an internet gateway',
drop: undefined,
subnet: undefined,
vpc: undefined,
Expand All @@ -84,7 +83,7 @@ const targetValuePlaceholder: Record<RouteTarget['type'], string | undefined> =
const targetValueDescription: Record<RouteTarget['type'], string | undefined> = {
ip: 'An IP address, like 10.0.1.5',
instance: undefined,
internet_gateway: routeFormMessage.internetGatewayTargetValue,
internet_gateway: undefined,
drop: undefined,
subnet: undefined,
vpc: undefined,
Expand All @@ -103,10 +102,15 @@ export const RouteFormFields = ({ form, disabled }: RouteFormFieldsProps) => {
// usePrefetchedApiQuery items below are initially fetched in the loaders in vpc-router-route-create and -edit
const {
data: { items: vpcSubnets },
} = usePrefetchedApiQuery('vpcSubnetList', { query: { project, vpc, limit: 1000 } })
} = usePrefetchedApiQuery('vpcSubnetList', { query: { project, vpc, limit: ALL_ISH } })
const {
data: { items: instances },
} = usePrefetchedApiQuery('instanceList', { query: { project, limit: 1000 } })
} = usePrefetchedApiQuery('instanceList', { query: { project, limit: ALL_ISH } })
const {
data: { items: internetGateways },
} = usePrefetchedApiQuery('internetGatewayList', {
query: { project, vpc, limit: ALL_ISH },
})

const { control } = form
const destinationType = form.watch('destination.type')
Expand All @@ -129,13 +133,35 @@ export const RouteFormFields = ({ form, disabled }: RouteFormFieldsProps) => {
control,
placeholder: targetValuePlaceholder[targetType],
required: true,
// 'internet_gateway' targetTypes can only have the value 'outbound', so we disable the field
disabled: disabled || targetType === 'internet_gateway',
disabled,
description: targetValueDescription[targetType],
// need a default to prevent the text field validation function from
// sticking around when we switch to the combobox
validate: () => undefined,
}

const targetTypeField = () => {
if (targetType === 'drop') {
return null
}
if (targetType === 'instance') {
return <ComboboxField {...targetValueProps} items={toComboboxItems(instances)} />
}
if (targetType === 'internet_gateway') {
return (
<ComboboxField {...targetValueProps} items={toComboboxItems(internetGateways)} />
)
}
return (
<TextField
{...targetValueProps}
validate={(value, { target }) =>
(target.type === 'ip' && validateIp(value)) || undefined
}
/>
)
}

return (
<>
{disabled && (
Expand Down Expand Up @@ -176,22 +202,13 @@ export const RouteFormFields = ({ form, disabled }: RouteFormFieldsProps) => {
items={toListboxItems(targetTypes)}
placeholder="Select a target type"
required
onChange={(value) => {
form.setValue('target.value', value === 'internet_gateway' ? 'outbound' : '')
onChange={() => {
form.setValue('target.value', '')
form.clearErrors('target.value')
}}
disabled={disabled}
/>
{targetType === 'drop' ? null : targetType === 'instance' ? (
<ComboboxField {...targetValueProps} items={toComboboxItems(instances)} />
) : (
<TextField
{...targetValueProps}
validate={(value, { target }) =>
(target.type === 'ip' && validateIp(value)) || undefined
}
/>
)}
{targetTypeField()}
</>
)
}
8 changes: 6 additions & 2 deletions app/forms/vpc-router-route-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { HL } from '~/components/HL'
import { RouteFormFields, type RouteFormValues } from '~/forms/vpc-router-route-common'
import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'

const defaultValues: RouteFormValues = {
Expand All @@ -28,10 +29,13 @@ CreateRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) =
const { project, vpc } = getVpcRouterSelector(params)
await Promise.all([
apiQueryClient.prefetchQuery('vpcSubnetList', {
query: { project, vpc, limit: 1000 },
query: { project, vpc, limit: ALL_ISH },
}),
apiQueryClient.prefetchQuery('instanceList', {
query: { project, limit: 1000 },
query: { project, limit: ALL_ISH },
}),
apiQueryClient.prefetchQuery('internetGatewayList', {
query: { project, vpc, limit: ALL_ISH },
}),
])
return null
Expand Down
9 changes: 7 additions & 2 deletions app/forms/vpc-router-route-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '~/forms/vpc-router-route-common'
import { getVpcRouterRouteSelector, useVpcRouterRouteSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'

EditRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
Expand All @@ -35,10 +36,13 @@ EditRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) =>
query: { project, vpc, router },
}),
apiQueryClient.prefetchQuery('vpcSubnetList', {
query: { project, vpc, limit: 1000 },
query: { project, vpc, limit: ALL_ISH },
}),
apiQueryClient.prefetchQuery('instanceList', {
query: { project, limit: 1000 },
query: { project, limit: ALL_ISH },
}),
apiQueryClient.prefetchQuery('internetGatewayList', {
query: { project, vpc, limit: ALL_ISH },
}),
])
return null
Expand All @@ -65,6 +69,7 @@ export function EditRouterRouteSideModalForm() {
const updateRouterRoute = useApiMutation('vpcRouterRouteUpdate', {
onSuccess(updatedRoute) {
queryClient.invalidateQueries('vpcRouterRouteList')
queryClient.invalidateQueries('vpcRouterRouteView')
addToast(<>Route <HL>{updatedRoute.name}</HL> updated</>) // prettier-ignore
navigate(pb.vpcRouter(routerSelector))
},
Expand Down
3 changes: 3 additions & 0 deletions app/hooks/use-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const getFirewallRuleSelector = requireParams('project', 'vpc', 'rule')
export const getVpcRouterSelector = requireParams('project', 'vpc', 'router')
export const getVpcRouterRouteSelector = requireParams('project', 'vpc', 'router', 'route')
export const getVpcSubnetSelector = requireParams('project', 'vpc', 'subnet')
export const getInternetGatewaySelector = requireParams('project', 'vpc', 'gateway')
export const getSiloSelector = requireParams('silo')
export const getSiloImageSelector = requireParams('image')
export const getSshKeySelector = requireParams('sshKey')
Expand Down Expand Up @@ -86,6 +87,8 @@ export const useVpcSelector = () => useSelectedParams(getVpcSelector)
export const useVpcRouterSelector = () => useSelectedParams(getVpcRouterSelector)
export const useVpcRouterRouteSelector = () => useSelectedParams(getVpcRouterRouteSelector)
export const useVpcSubnetSelector = () => useSelectedParams(getVpcSubnetSelector)
export const useInternetGatewaySelector = () =>
useSelectedParams(getInternetGatewaySelector)
export const useFirewallRuleSelector = () => useSelectedParams(getFirewallRuleSelector)
export const useSiloSelector = () => useSelectedParams(getSiloSelector)
export const useSiloImageSelector = () => useSelectedParams(getSiloImageSelector)
Expand Down
1 change: 1 addition & 0 deletions app/pages/project/vpcs/VpcPage/VpcPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export function VpcPage() {
<Tab to={pb.vpcFirewallRules(vpcSelector)}>Firewall Rules</Tab>
<Tab to={pb.vpcSubnets(vpcSelector)}>Subnets</Tab>
<Tab to={pb.vpcRouters(vpcSelector)}>Routers</Tab>
<Tab to={pb.vpcInternetGateways(vpcSelector)}>Internet Gateways</Tab>
</RouteTabs>
</>
)
Expand Down
151 changes: 151 additions & 0 deletions app/pages/project/vpcs/VpcPage/tabs/VpcGatewaysTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/

import { useQuery } from '@tanstack/react-query'
import { createColumnHelper } from '@tanstack/react-table'
import { useMemo } from 'react'
import { Outlet, type LoaderFunctionArgs } from 'react-router-dom'

import { apiq, getListQFn, queryClient, type InternetGateway } from '~/api'
import { getVpcSelector, useVpcSelector } from '~/hooks/use-params'
import { EmptyCell } from '~/table/cells/EmptyCell'
import { IpPoolCell } from '~/table/cells/IpPoolCell'
import { LinkCell, makeLinkCell } from '~/table/cells/LinkCell'
import { Columns } from '~/table/columns/common'
import { useQueryTable } from '~/table/QueryTable'
import { CopyableIp } from '~/ui/lib/CopyableIp'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'

import {
gatewayIpAddressList,
gatewayIpPoolList,
routeList,
routerList,
useGatewayRoutes,
} from '../../gateway-data'

const gatewayList = ({ project, vpc }: PP.Vpc) =>
getListQFn('internetGatewayList', { query: { project, vpc, limit: ALL_ISH } })
const projectIpPoolList = getListQFn('projectIpPoolList', { query: { limit: ALL_ISH } })

const IpAddressCell = (gatewaySelector: PP.VpcInternetGateway) => {
const { data: addresses } = useQuery(gatewayIpAddressList(gatewaySelector).optionsFn())
if (!addresses || addresses.items.length < 1) return <EmptyCell />
return <CopyableIp ip={addresses.items[0].address} isLinked={false} />
}

const GatewayIpPoolCell = (gatewaySelector: PP.VpcInternetGateway) => {
const { data: gateways } = useQuery(gatewayIpPoolList(gatewaySelector).optionsFn())
if (!gateways || gateways.items.length < 1) return <EmptyCell />
return <IpPoolCell ipPoolId={gateways.items[0].ipPoolId} />
}

const GatewayRoutes = ({ project, vpc, gateway }: PP.VpcInternetGateway) => {
const matchingRoutes = useGatewayRoutes({ project, vpc, gateway })
const to = pb.vpcInternetGateway({ project, vpc, gateway })
if (!matchingRoutes?.length) return <EmptyCell />
return <LinkCell to={to}>{matchingRoutes.length}</LinkCell>
}

const colHelper = createColumnHelper<InternetGateway>()

VpcInternetGatewaysTab.loader = async ({ params }: LoaderFunctionArgs) => {
const { project, vpc } = getVpcSelector(params)
const [gateways, routers] = await Promise.all([
queryClient.fetchQuery(gatewayList({ project, vpc }).optionsFn()),
queryClient.fetchQuery(routerList({ project, vpc }).optionsFn()),
])

await Promise.all([
...gateways.items.flatMap((gateway: InternetGateway) => [
queryClient.prefetchQuery(
gatewayIpAddressList({ project, vpc, gateway: gateway.name }).optionsFn()
),
queryClient.prefetchQuery(
gatewayIpPoolList({ project, vpc, gateway: gateway.name }).optionsFn()
),
]),
...routers.items.map((router) =>
queryClient.prefetchQuery(
routeList({ project, vpc, router: router.name }).optionsFn()
)
),
queryClient.fetchQuery(projectIpPoolList.optionsFn()).then((pools) => {
for (const pool of pools.items) {
const { queryKey } = apiq('projectIpPoolView', { path: { pool: pool.id } })
queryClient.setQueryData(queryKey, pool)
}
}),
] satisfies Promise<unknown>[])

return null
}

export function VpcInternetGatewaysTab() {
const { project, vpc } = useVpcSelector()

const emptyState = (
<EmptyMessage
title="No internet gateways"
body="Create an internet gateway to see it here"
// buttonText="New internet gateway"
// buttonTo={pb.vpcInternetGatewaysNew(vpcSelector)}
/>
)

const columns = useMemo(
() => [
colHelper.accessor('name', {
cell: makeLinkCell((gateway) => pb.vpcInternetGateway({ project, vpc, gateway })),
}),
colHelper.accessor('description', Columns.description),
colHelper.accessor('name', {
// ID needed to avoid key collision with other name column
id: 'ip-address',
header: 'Attached IP Address',
cell: (info) => (
<IpAddressCell project={project} vpc={vpc} gateway={info.getValue()} />
),
}),
colHelper.accessor('name', {
// ID needed to avoid key collision with other name column
id: 'ip-pool',
header: 'Attached IP Pool',
cell: (info) => (
<GatewayIpPoolCell project={project} vpc={vpc} gateway={info.getValue()} />
),
}),
colHelper.accessor('name', {
// ID needed to avoid key collision with other name column
id: 'routes',
header: 'Routes',
cell: (info) => (
<GatewayRoutes project={project} vpc={vpc} gateway={info.getValue()} />
),
}),
colHelper.accessor('timeCreated', Columns.timeCreated),
],
[project, vpc]
)

const { table } = useQueryTable({
query: gatewayList({ project, vpc }),
columns,
emptyState,
})

return (
<>
{table}
<Outlet />
</>
)
}
Loading
Loading