Skip to content

Commit b3c741c

Browse files
committed
instance affinity tab
1 parent 5538906 commit b3c741c

7 files changed

Lines changed: 188 additions & 2 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
9+
import { useMemo } from 'react'
10+
import { type LoaderFunctionArgs } from 'react-router'
11+
12+
import {
13+
apiq,
14+
getListQFn,
15+
queryClient,
16+
usePrefetchedQuery,
17+
type AffinityGroup,
18+
type AntiAffinityGroup,
19+
} from '@oxide/api'
20+
import { Affinity24Icon } from '@oxide/design-system/icons/react'
21+
22+
import { getInstanceSelector, useInstanceSelector } from '~/hooks/use-params'
23+
import { makeLinkCell } from '~/table/cells/LinkCell'
24+
import { Columns } from '~/table/columns/common'
25+
import { Table } from '~/table/Table'
26+
import { Badge } from '~/ui/lib/Badge'
27+
import { CardBlock } from '~/ui/lib/CardBlock'
28+
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
29+
import { TableEmptyBox } from '~/ui/lib/Table'
30+
import { TipIcon } from '~/ui/lib/TipIcon'
31+
import { ALL_ISH } from '~/util/consts'
32+
import { pb } from '~/util/path-builder'
33+
import type * as PP from '~/util/path-params'
34+
35+
const instanceView = ({ project, instance }: PP.Instance) =>
36+
apiq('instanceView', { path: { instance }, query: { project } })
37+
const antiAffinityGroupList = ({ project, instance }: PP.Instance) =>
38+
getListQFn('instanceAntiAffinityGroupList', {
39+
path: { instance },
40+
query: { project, limit: ALL_ISH },
41+
})
42+
43+
export async function clientLoader({ params }: LoaderFunctionArgs) {
44+
const instanceSelector = getInstanceSelector(params)
45+
await Promise.all([
46+
// This is covered by the InstancePage loader but there's no downside to
47+
// being redundant. If it were removed there, we'd still want it here.
48+
queryClient.prefetchQuery(instanceView(instanceSelector)),
49+
queryClient.prefetchQuery(antiAffinityGroupList(instanceSelector).optionsFn()),
50+
])
51+
return null
52+
}
53+
54+
const colHelper = createColumnHelper<AffinityGroup | AntiAffinityGroup>()
55+
const staticCols = [
56+
colHelper.accessor('description', Columns.description),
57+
colHelper.accessor('policy', {
58+
header: () => (
59+
<>
60+
Policy
61+
<TipIcon className="ml-2">
62+
The affinity policy describes what to do when a request cannot be satisfied.
63+
&lsquo;allow&rsquo; means a best-effort approach, while &lsquo;fail&rsquo; means
64+
fail explicitly.
65+
</TipIcon>
66+
</>
67+
),
68+
cell: (info) => <Badge color="neutral">{info.getValue()}</Badge>,
69+
}),
70+
colHelper.accessor('failureDomain', {
71+
header: () => (
72+
<>
73+
Failure Domain
74+
<TipIcon className="ml-2">
75+
Describes the scope of affinity for the purposes of co-location. Currently, only
76+
&lsquo;sled&rsquo; is supported.
77+
</TipIcon>
78+
</>
79+
),
80+
cell: (info) => <Badge color="neutral">{info.getValue()}</Badge>,
81+
}),
82+
]
83+
84+
export const handle = { crumb: 'Affinity' }
85+
86+
export default function AffinityTab() {
87+
const instanceSelector = useInstanceSelector()
88+
const { project } = instanceSelector
89+
90+
const { data: antiAffinityGroups } = usePrefetchedQuery(
91+
antiAffinityGroupList(instanceSelector).optionsFn()
92+
)
93+
94+
const antiAffinityCols = useMemo(
95+
() => [
96+
colHelper.accessor('name', {
97+
header: 'Group Name',
98+
cell: makeLinkCell((antiAffinityGroup) =>
99+
pb.antiAffinityGroup({ project, antiAffinityGroup })
100+
),
101+
}),
102+
...staticCols,
103+
],
104+
[project]
105+
)
106+
107+
// Create tables for both types of groups
108+
const antiAffinityTable = useReactTable({
109+
columns: antiAffinityCols,
110+
data: antiAffinityGroups.items,
111+
getCoreRowModel: getCoreRowModel(),
112+
})
113+
114+
return (
115+
<CardBlock>
116+
<CardBlock.Header title="Anti-affinity groups" titleId="anti-affinity-groups-label" />
117+
118+
<CardBlock.Body>
119+
{antiAffinityGroups.items.length > 0 ? (
120+
<Table
121+
aria-labelledby="anti-affinity-groups-label"
122+
table={antiAffinityTable}
123+
className="table-inline"
124+
/>
125+
) : (
126+
<TableEmptyBox border={false}>
127+
<EmptyMessage
128+
icon={<Affinity24Icon />}
129+
title="No Anti-Affinity Groups"
130+
body="This instance is not a member of any anti-affinity groups"
131+
/>
132+
</TableEmptyBox>
133+
)}
134+
</CardBlock.Body>
135+
</CardBlock>
136+
)
137+
}

app/pages/project/instances/InstancePage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ export default function InstancePage() {
273273
Metrics
274274
</Tab>
275275
<Tab to={pb.instanceConnect(instanceSelector)}>Connect</Tab>
276+
<Tab to={pb.instanceAffinity(instanceSelector)}>Affinity</Tab>
276277
<Tab to={pb.instanceSettings(instanceSelector)}>Settings</Tab>
277278
</RouteTabs>
278279
{resizeInstance && (

app/routes.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,10 @@ export const routes = createRoutesFromElements(
335335
path="settings"
336336
lazy={() => import('./pages/project/instances/SettingsTab').then(convert)}
337337
/>
338+
<Route
339+
path="affinity"
340+
lazy={() => import('./pages/project/instances/AffinityTab').then(convert)}
341+
/>
338342
</Route>
339343
</Route>
340344
</Route>

app/util/__snapshots__/path-builder.spec.ts.snap

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,28 @@ exports[`breadcrumbs 2`] = `
137137
"path": "/projects/p/instances/i/storage",
138138
},
139139
],
140+
"instanceAffinity (/projects/p/instances/i/affinity)": [
141+
{
142+
"label": "Projects",
143+
"path": "/projects",
144+
},
145+
{
146+
"label": "p",
147+
"path": "/projects/p/instances",
148+
},
149+
{
150+
"label": "Instances",
151+
"path": "/projects/p/instances",
152+
},
153+
{
154+
"label": "i",
155+
"path": "/projects/p/instances/i/storage",
156+
},
157+
{
158+
"label": "Affinity",
159+
"path": "/projects/p/instances/i/affinity",
160+
},
161+
],
140162
"instanceConnect (/projects/p/instances/i/connect)": [
141163
{
142164
"label": "Projects",

app/util/path-builder.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ test('path builder', () => {
5151
"floatingIps": "/projects/p/floating-ips",
5252
"floatingIpsNew": "/projects/p/floating-ips-new",
5353
"instance": "/projects/p/instances/i/storage",
54+
"instanceAffinity": "/projects/p/instances/i/affinity",
5455
"instanceConnect": "/projects/p/instances/i/connect",
5556
"instanceCpuMetrics": "/projects/p/instances/i/metrics/cpu",
5657
"instanceDiskMetrics": "/projects/p/instances/i/metrics/disk",

app/util/path-builder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const pb = {
4949
instanceStorage: (params: PP.Instance) => `${instanceBase(params)}/storage`,
5050
instanceConnect: (params: PP.Instance) => `${instanceBase(params)}/connect`,
5151
instanceNetworking: (params: PP.Instance) => `${instanceBase(params)}/networking`,
52+
instanceAffinity: (params: PP.Instance) => `${instanceBase(params)}/affinity`,
5253
serialConsole: (params: PP.Instance) => `${instanceBase(params)}/serial-console`,
5354
instanceSettings: (params: PP.Instance) => `${instanceBase(params)}/settings`,
5455

mock-api/msw/handlers.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,28 @@ export const handlers = makeHandlers({
16631663
)
16641664
return 204
16651665
},
1666+
instanceAntiAffinityGroupList: ({ path, query }) => {
1667+
const instance = lookup.instance({ ...path, ...query })
1668+
const antiAffinityGroups = db.antiAffinityGroups.filter((group) =>
1669+
db.antiAffinityGroupMemberLists.some(
1670+
(member) =>
1671+
member.anti_affinity_group_id === group.id &&
1672+
member.anti_affinity_group_member.id === instance.id
1673+
)
1674+
)
1675+
return paginated(query, antiAffinityGroups)
1676+
},
1677+
instanceAffinityGroupList: ({ path, query }) => {
1678+
const instance = lookup.instance({ ...path, ...query })
1679+
const affinityGroups = db.affinityGroups.filter((group) =>
1680+
db.affinityGroupMemberLists.some(
1681+
(member) =>
1682+
member.affinity_group_id === group.id &&
1683+
member.affinity_group_member.id === instance.id
1684+
)
1685+
)
1686+
return paginated(query, affinityGroups)
1687+
},
16661688

16671689
// Misc endpoints we're not using yet in the console
16681690
affinityGroupCreate: NotImplemented,
@@ -1680,8 +1702,6 @@ export const handlers = makeHandlers({
16801702
certificateDelete: NotImplemented,
16811703
certificateList: NotImplemented,
16821704
certificateView: NotImplemented,
1683-
instanceAffinityGroupList: NotImplemented,
1684-
instanceAntiAffinityGroupList: NotImplemented,
16851705
instanceSerialConsole: NotImplemented,
16861706
instanceSerialConsoleStream: NotImplemented,
16871707
instanceSshPublicKeyList: NotImplemented,

0 commit comments

Comments
 (0)