From 32d18ad00a43ac6743ba430f43d2463b43a4f45c Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 27 Mar 2025 14:20:48 -0700
Subject: [PATCH 01/56] Enable removal of group member
---
.../affinity/AntiAffinityGroupPage.tsx | 60 ++++++++++++++++++-
1 file changed, 58 insertions(+), 2 deletions(-)
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index ee043715d8..b8645a44eb 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -7,7 +7,7 @@
*/
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
-import { useMemo } from 'react'
+import { useCallback, useMemo } from 'react'
import type { LoaderFunctionArgs } from 'react-router'
import { Affinity24Icon } from '@oxide/design-system/icons/react'
@@ -16,15 +16,20 @@ import {
apiq,
getListQFn,
queryClient,
+ useApiMutation,
usePrefetchedQuery,
type AntiAffinityGroupMember,
} from '~/api'
+import { HL } from '~/components/HL'
import { makeCrumb } from '~/hooks/use-crumbs'
import {
getAntiAffinityGroupSelector,
useAntiAffinityGroupSelector,
} from '~/hooks/use-params'
+import { confirmAction } from '~/stores/confirm-action'
+import { addToast } from '~/stores/toast'
import { makeLinkCell } from '~/table/cells/LinkCell'
+import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
import { Columns } from '~/table/columns/common'
import { Table } from '~/table/Table'
import { Badge } from '~/ui/lib/Badge'
@@ -84,7 +89,7 @@ export default function AntiAffinityPage() {
memberList({ antiAffinityGroup, project }).optionsFn()
)
const membersCount = members.items.length
- const columns = useMemo(
+ const cols = useMemo(
() => [
colHelper.accessor('value.name', {
header: 'Name',
@@ -95,6 +100,57 @@ export default function AntiAffinityPage() {
[project]
)
+ const { mutateAsync: removeMember } = useApiMutation(
+ 'antiAffinityGroupMemberInstanceDelete',
+ {
+ onSuccess(_data, variables) {
+ queryClient.invalidateEndpoint('antiAffinityGroupMemberList')
+ queryClient.invalidateEndpoint('antiAffinityGroupView')
+ addToast(<>Member {variables.path.instance} removed from anti-affinity group {group.name} >) // prettier-ignore
+ },
+ }
+ )
+
+ const makeActions = useCallback(
+ (antiAffinityGroupMember: AntiAffinityGroupMember): MenuAction[] => [
+ {
+ label: 'Copy instance ID',
+ onActivate() {
+ navigator.clipboard.writeText(antiAffinityGroupMember.value.id)
+ addToast('ID copied to clipboard')
+ },
+ },
+ {
+ label: 'Remove from group',
+ onActivate() {
+ confirmAction({
+ actionType: 'danger',
+ doAction: () =>
+ removeMember({
+ path: {
+ antiAffinityGroup: antiAffinityGroup,
+ instance: antiAffinityGroupMember.value.name,
+ },
+ query: { project },
+ }),
+ modalTitle: 'Remove instance from anti-affinity group',
+ modalContent: (
+
+ Are you sure you want to remove{' '}
+ {antiAffinityGroupMember.value.name} from the anti-affinity group{' '}
+ {antiAffinityGroup} ?
+
+ ),
+ errorTitle: `Error removing ${antiAffinityGroupMember.value.name}`,
+ })
+ },
+ },
+ ],
+ [project, removeMember, antiAffinityGroup]
+ )
+
+ const columns = useColsWithActions(cols, makeActions)
+
const table = useReactTable({
columns,
data: members.items,
From 429eda6535edc8f9837bb6a382a0e8a7a57293e6 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 27 Mar 2025 15:00:31 -0700
Subject: [PATCH 02/56] Enable deletion of group
---
app/pages/project/affinity/AffinityPage.tsx | 40 +++++++++++++++++++--
mock-api/msw/handlers.ts | 11 +++++-
2 files changed, 48 insertions(+), 3 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 4692233b17..d753e4e8dc 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -8,22 +8,27 @@
import { useQuery } from '@tanstack/react-query'
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
-import { useMemo } from 'react'
+import { useCallback, useMemo } from 'react'
import { Link, type LoaderFunctionArgs } from 'react-router'
import {
apiq,
getListQFn,
queryClient,
+ useApiMutation,
usePrefetchedQuery,
type AffinityPolicy,
type AntiAffinityGroup,
} from '@oxide/api'
import { Affinity24Icon } from '@oxide/design-system/icons/react'
+import { HL } from '~/components/HL'
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
+import { confirmDelete } from '~/stores/confirm-delete'
+import { addToast } from '~/stores/toast'
import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell'
import { makeLinkCell } from '~/table/cells/LinkCell'
+import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
import { Columns } from '~/table/columns/common'
import { Table } from '~/table/Table'
import { Badge } from '~/ui/lib/Badge'
@@ -98,7 +103,7 @@ const staticCols = [
export default function AffinityPage() {
const { project } = useProjectSelector()
- const columns = useMemo(
+ const cols = useMemo(
() => [
colHelper.accessor('name', {
cell: makeLinkCell((antiAffinityGroup) =>
@@ -112,6 +117,37 @@ export default function AffinityPage() {
)
const { data } = usePrefetchedQuery(antiAffinityGroupList({ project }).optionsFn())
+ const { mutateAsync: deleteGroup } = useApiMutation('antiAffinityGroupDelete', {
+ onSuccess(_data, variables) {
+ queryClient.invalidateEndpoint('antiAffinityGroupList')
+ queryClient.invalidateEndpoint('antiAffinityGroupMemberList')
+ addToast(
+ <>
+ Anti-affinity group {variables.path.antiAffinityGroup} deleted
+ >
+ )
+ },
+ })
+
+ const makeActions = useCallback(
+ (antiAffinityGroup: AntiAffinityGroup): MenuAction[] => [
+ {
+ label: 'Delete',
+ onActivate: confirmDelete({
+ doDelete: () =>
+ deleteGroup({
+ path: { antiAffinityGroup: antiAffinityGroup.name },
+ query: { project },
+ }),
+ label: antiAffinityGroup.name,
+ }),
+ },
+ ],
+ [project, deleteGroup]
+ )
+
+ const columns = useColsWithActions(cols, makeActions)
+
const table = useReactTable({
columns,
data: data.items,
diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts
index 04be2d35f1..2e7fcccfe7 100644
--- a/mock-api/msw/handlers.ts
+++ b/mock-api/msw/handlers.ts
@@ -1637,6 +1637,16 @@ export const handlers = makeHandlers({
},
antiAffinityGroupView: ({ path, query }) =>
lookup.antiAffinityGroup({ ...path, ...query }),
+ antiAffinityGroupDelete: ({ path, query }) => {
+ const antiAffinityGroup = lookup.antiAffinityGroup({ ...path, ...query })
+ db.antiAffinityGroups = db.antiAffinityGroups.filter(
+ (i) =>
+ !(
+ i.name === antiAffinityGroup.name && i.project_id === antiAffinityGroup.project_id
+ )
+ )
+ return 204
+ },
antiAffinityGroupMemberList: ({ path, query }) => {
const antiAffinityGroup = lookup.antiAffinityGroup({ ...path, ...query })
const members: Json[] = db.antiAffinityGroupMemberLists
@@ -1672,7 +1682,6 @@ export const handlers = makeHandlers({
affinityGroupMemberInstanceView: NotImplemented,
affinityGroupUpdate: NotImplemented,
antiAffinityGroupCreate: NotImplemented,
- antiAffinityGroupDelete: NotImplemented,
antiAffinityGroupMemberInstanceAdd: NotImplemented,
antiAffinityGroupMemberInstanceView: NotImplemented,
antiAffinityGroupUpdate: NotImplemented,
From eb387a02e8cda30937ab624ffc774110b912411d Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 27 Mar 2025 16:33:14 -0700
Subject: [PATCH 03/56] move away from useMemo for columns
---
app/pages/project/affinity/AffinityPage.tsx | 27 +++++++++----------
.../affinity/AntiAffinityGroupPage.tsx | 23 ++++++++--------
2 files changed, 24 insertions(+), 26 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index d753e4e8dc..0c67039ef3 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -8,7 +8,7 @@
import { useQuery } from '@tanstack/react-query'
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
-import { useCallback, useMemo } from 'react'
+import { useCallback } from 'react'
import { Link, type LoaderFunctionArgs } from 'react-router'
import {
@@ -103,18 +103,6 @@ const staticCols = [
export default function AffinityPage() {
const { project } = useProjectSelector()
- const cols = useMemo(
- () => [
- colHelper.accessor('name', {
- cell: makeLinkCell((antiAffinityGroup) =>
- pb.antiAffinityGroup({ project, antiAffinityGroup })
- ),
- id: 'members',
- }),
- ...staticCols,
- ],
- [project]
- )
const { data } = usePrefetchedQuery(antiAffinityGroupList({ project }).optionsFn())
const { mutateAsync: deleteGroup } = useApiMutation('antiAffinityGroupDelete', {
@@ -146,7 +134,18 @@ export default function AffinityPage() {
[project, deleteGroup]
)
- const columns = useColsWithActions(cols, makeActions)
+ const columns = useColsWithActions(
+ [
+ colHelper.accessor('name', {
+ cell: makeLinkCell((antiAffinityGroup) =>
+ pb.antiAffinityGroup({ project, antiAffinityGroup })
+ ),
+ id: 'members',
+ }),
+ ...staticCols,
+ ],
+ makeActions
+ )
const table = useReactTable({
columns,
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index b8645a44eb..ece9e45c74 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -7,7 +7,7 @@
*/
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
-import { useCallback, useMemo } from 'react'
+import { useCallback } from 'react'
import type { LoaderFunctionArgs } from 'react-router'
import { Affinity24Icon } from '@oxide/design-system/icons/react'
@@ -89,16 +89,6 @@ export default function AntiAffinityPage() {
memberList({ antiAffinityGroup, project }).optionsFn()
)
const membersCount = members.items.length
- const cols = useMemo(
- () => [
- colHelper.accessor('value.name', {
- header: 'Name',
- cell: makeLinkCell((instance) => pb.instance({ project, instance })),
- }),
- colHelper.accessor('value.runState', Columns.instanceState),
- ],
- [project]
- )
const { mutateAsync: removeMember } = useApiMutation(
'antiAffinityGroupMemberInstanceDelete',
@@ -149,7 +139,16 @@ export default function AntiAffinityPage() {
[project, removeMember, antiAffinityGroup]
)
- const columns = useColsWithActions(cols, makeActions)
+ const columns = useColsWithActions(
+ [
+ colHelper.accessor('value.name', {
+ header: 'Name',
+ cell: makeLinkCell((instance) => pb.instance({ project, instance })),
+ }),
+ colHelper.accessor('value.runState', Columns.instanceState),
+ ],
+ makeActions
+ )
const table = useReactTable({
columns,
From 16795fc2bf96e3b65edfbc8ed5b3c9b74ea8a069 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 27 Mar 2025 16:46:19 -0700
Subject: [PATCH 04/56] Update copy in remove confirm modal
---
.../project/affinity/AntiAffinityGroupPage.tsx | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index ece9e45c74..25f9410b8b 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -125,11 +125,17 @@ export default function AntiAffinityPage() {
}),
modalTitle: 'Remove instance from anti-affinity group',
modalContent: (
-
- Are you sure you want to remove{' '}
- {antiAffinityGroupMember.value.name} from the anti-affinity group{' '}
- {antiAffinityGroup} ?
-
+ <>
+
+ Are you sure you want to remove{' '}
+ {antiAffinityGroupMember.value.name} from the anti-affinity group{' '}
+ {antiAffinityGroup} ?
+
+
+ Future placement of this instance will not attempt to satisfy the affinity
+ rules.
+
+ >
),
errorTitle: `Error removing ${antiAffinityGroupMember.value.name}`,
})
From 3b2db66d1bbd727226764c89b1fdc9a383caa038 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 27 Mar 2025 20:34:57 -0700
Subject: [PATCH 05/56] Update copy in delete modal
---
app/pages/project/affinity/AffinityPage.tsx | 35 +++++++++++++++------
1 file changed, 25 insertions(+), 10 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 0c67039ef3..01f5a363e1 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -24,7 +24,7 @@ import { Affinity24Icon } from '@oxide/design-system/icons/react'
import { HL } from '~/components/HL'
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
-import { confirmDelete } from '~/stores/confirm-delete'
+import { confirmAction } from '~/stores/confirm-action'
import { addToast } from '~/stores/toast'
import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell'
import { makeLinkCell } from '~/table/cells/LinkCell'
@@ -108,7 +108,6 @@ export default function AffinityPage() {
const { mutateAsync: deleteGroup } = useApiMutation('antiAffinityGroupDelete', {
onSuccess(_data, variables) {
queryClient.invalidateEndpoint('antiAffinityGroupList')
- queryClient.invalidateEndpoint('antiAffinityGroupMemberList')
addToast(
<>
Anti-affinity group {variables.path.antiAffinityGroup} deleted
@@ -121,14 +120,30 @@ export default function AffinityPage() {
(antiAffinityGroup: AntiAffinityGroup): MenuAction[] => [
{
label: 'Delete',
- onActivate: confirmDelete({
- doDelete: () =>
- deleteGroup({
- path: { antiAffinityGroup: antiAffinityGroup.name },
- query: { project },
- }),
- label: antiAffinityGroup.name,
- }),
+ onActivate() {
+ confirmAction({
+ actionType: 'danger',
+ doAction: () =>
+ deleteGroup({
+ path: { antiAffinityGroup: antiAffinityGroup.name },
+ query: { project },
+ }),
+ modalTitle: 'Delete anti-affinity group',
+ modalContent: (
+ <>
+
+ Are you sure you want to delete the anti-affinity group{' '}
+ {antiAffinityGroup.name} ?
+
+
+ Future placement of the affinity group’s members will not attempt to
+ satisfy the affinity rules.
+
+ >
+ ),
+ errorTitle: `Error removing ${antiAffinityGroup.name}`,
+ })
+ },
},
],
[project, deleteGroup]
From 58e6e6b10b8a4ef4ffa46745f939065c85a55d67 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Fri, 28 Mar 2025 17:05:07 -0500
Subject: [PATCH 06/56] use apiq since we're not paginating
---
app/pages/project/affinity/AffinityPage.tsx | 9 +++------
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 9 +++------
2 files changed, 6 insertions(+), 12 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 01f5a363e1..db8ab826a9 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -13,7 +13,6 @@ import { Link, type LoaderFunctionArgs } from 'react-router'
import {
apiq,
- getListQFn,
queryClient,
useApiMutation,
usePrefetchedQuery,
@@ -42,7 +41,7 @@ import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
const antiAffinityGroupList = ({ project }: PP.Project) =>
- getListQFn('antiAffinityGroupList', { query: { project, limit: ALL_ISH } })
+ apiq('antiAffinityGroupList', { query: { project, limit: ALL_ISH } })
const memberList = ({ antiAffinityGroup, project }: PP.AntiAffinityGroup) =>
apiq('antiAffinityGroupMemberList', {
path: { antiAffinityGroup },
@@ -52,9 +51,7 @@ const memberList = ({ antiAffinityGroup, project }: PP.AntiAffinityGroup) =>
export async function clientLoader({ params }: LoaderFunctionArgs) {
const { project } = getProjectSelector(params)
- const groups = await queryClient.fetchQuery(
- antiAffinityGroupList({ project }).optionsFn()
- )
+ const groups = await queryClient.fetchQuery(antiAffinityGroupList({ project }))
const memberFetches = groups.items.map(({ name }) =>
queryClient.prefetchQuery(memberList({ antiAffinityGroup: name, project }))
)
@@ -103,7 +100,7 @@ const staticCols = [
export default function AffinityPage() {
const { project } = useProjectSelector()
- const { data } = usePrefetchedQuery(antiAffinityGroupList({ project }).optionsFn())
+ const { data } = usePrefetchedQuery(antiAffinityGroupList({ project }))
const { mutateAsync: deleteGroup } = useApiMutation('antiAffinityGroupDelete', {
onSuccess(_data, variables) {
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 25f9410b8b..8976438601 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -14,7 +14,6 @@ import { Affinity24Icon } from '@oxide/design-system/icons/react'
import {
apiq,
- getListQFn,
queryClient,
useApiMutation,
usePrefetchedQuery,
@@ -54,7 +53,7 @@ const colHelper = createColumnHelper()
const antiAffinityGroupView = ({ antiAffinityGroup, project }: PP.AntiAffinityGroup) =>
apiq('antiAffinityGroupView', { path: { antiAffinityGroup }, query: { project } })
const memberList = ({ antiAffinityGroup, project }: PP.AntiAffinityGroup) =>
- getListQFn('antiAffinityGroupMemberList', {
+ apiq('antiAffinityGroupMemberList', {
path: { antiAffinityGroup },
// member limit in DB is currently 32, so pagination isn't needed
query: { project, limit: ALL_ISH },
@@ -64,7 +63,7 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
const { antiAffinityGroup, project } = getAntiAffinityGroupSelector(params)
await Promise.all([
queryClient.fetchQuery(antiAffinityGroupView({ antiAffinityGroup, project })),
- queryClient.fetchQuery(memberList({ antiAffinityGroup, project }).optionsFn()),
+ queryClient.fetchQuery(memberList({ antiAffinityGroup, project })),
])
return null
}
@@ -85,9 +84,7 @@ export default function AntiAffinityPage() {
antiAffinityGroupView({ antiAffinityGroup, project })
)
const { id, name, description, policy, timeCreated } = group
- const { data: members } = usePrefetchedQuery(
- memberList({ antiAffinityGroup, project }).optionsFn()
- )
+ const { data: members } = usePrefetchedQuery(memberList({ antiAffinityGroup, project }))
const membersCount = members.items.length
const { mutateAsync: removeMember } = useApiMutation(
From d42ad1861e6351c1255785937eb9927f94e2354e Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Fri, 28 Mar 2025 17:06:59 -0500
Subject: [PATCH 07/56] use the id for delete
---
mock-api/msw/handlers.ts | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts
index 2e7fcccfe7..e0600c95a5 100644
--- a/mock-api/msw/handlers.ts
+++ b/mock-api/msw/handlers.ts
@@ -1638,13 +1638,8 @@ export const handlers = makeHandlers({
antiAffinityGroupView: ({ path, query }) =>
lookup.antiAffinityGroup({ ...path, ...query }),
antiAffinityGroupDelete: ({ path, query }) => {
- const antiAffinityGroup = lookup.antiAffinityGroup({ ...path, ...query })
- db.antiAffinityGroups = db.antiAffinityGroups.filter(
- (i) =>
- !(
- i.name === antiAffinityGroup.name && i.project_id === antiAffinityGroup.project_id
- )
- )
+ const group = lookup.antiAffinityGroup({ ...path, ...query })
+ db.antiAffinityGroups = db.antiAffinityGroups.filter((i) => i.id !== group.id)
return 204
},
antiAffinityGroupMemberList: ({ path, query }) => {
From 067f9ffdce547d6bd208124499ff3c9ee27478a2 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Fri, 28 Mar 2025 15:08:46 -0700
Subject: [PATCH 08/56] merged main and reconciling diffs
---
app/pages/project/affinity/AffinityPage.tsx | 14 ++++----------
.../project/affinity/AntiAffinityGroupPage.tsx | 17 +++++------------
2 files changed, 9 insertions(+), 22 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index db8ab826a9..ceb19ab0ed 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -127,16 +127,10 @@ export default function AffinityPage() {
}),
modalTitle: 'Delete anti-affinity group',
modalContent: (
- <>
-
- Are you sure you want to delete the anti-affinity group{' '}
- {antiAffinityGroup.name} ?
-
-
- Future placement of the affinity group’s members will not attempt to
- satisfy the affinity rules.
-
- >
+
+ Are you sure you want to delete the anti-affinity group{' '}
+ {antiAffinityGroup.name} ?
+
),
errorTitle: `Error removing ${antiAffinityGroup.name}`,
})
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 8976438601..4e259aaeba 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -104,7 +104,6 @@ export default function AntiAffinityPage() {
label: 'Copy instance ID',
onActivate() {
navigator.clipboard.writeText(antiAffinityGroupMember.value.id)
- addToast('ID copied to clipboard')
},
},
{
@@ -122,17 +121,11 @@ export default function AntiAffinityPage() {
}),
modalTitle: 'Remove instance from anti-affinity group',
modalContent: (
- <>
-
- Are you sure you want to remove{' '}
- {antiAffinityGroupMember.value.name} from the anti-affinity group{' '}
- {antiAffinityGroup} ?
-
-
- Future placement of this instance will not attempt to satisfy the affinity
- rules.
-
- >
+
+ Are you sure you want to remove{' '}
+ {antiAffinityGroupMember.value.name} from the anti-affinity group{' '}
+ {antiAffinityGroup} ?
+
),
errorTitle: `Error removing ${antiAffinityGroupMember.value.name}`,
})
From 6e2249cdd393f813263bc3fca03b3aee8cbfd2e2 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Tue, 1 Apr 2025 13:30:55 -0700
Subject: [PATCH 09/56] Add new anti-affinity group
---
app/forms/anti-affinity-group-create.tsx | 124 ++++++++++++++++++++
app/pages/project/affinity/AffinityPage.tsx | 21 +++-
app/routes.tsx | 10 ++
app/util/path-builder.ts | 1 +
mock-api/msw/handlers.ts | 34 +++++-
5 files changed, 183 insertions(+), 7 deletions(-)
create mode 100644 app/forms/anti-affinity-group-create.tsx
diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx
new file mode 100644
index 0000000000..95bf07e2f9
--- /dev/null
+++ b/app/forms/anti-affinity-group-create.tsx
@@ -0,0 +1,124 @@
+/*
+ * 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 { useForm } from 'react-hook-form'
+import { useNavigate, type LoaderFunctionArgs } from 'react-router'
+
+import {
+ apiq,
+ queryClient,
+ useApiMutation,
+ usePrefetchedQuery,
+ type AffinityPolicy,
+} from '@oxide/api'
+
+import { DescriptionField } from '~/components/form/fields/DescriptionField'
+import { NameField } from '~/components/form/fields/NameField'
+import { RadioField } from '~/components/form/fields/RadioField'
+import { SideModalForm } from '~/components/form/SideModalForm'
+import { HL } from '~/components/HL'
+import { titleCrumb } from '~/hooks/use-crumbs'
+import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
+import { addToast } from '~/stores/toast'
+import { ALL_ISH } from '~/util/consts'
+import { pb } from '~/util/path-builder'
+import type * as PP from '~/util/path-params'
+
+export const handle = titleCrumb('New anti-affinity group')
+
+type AntiAffinityGroupFormValues = {
+ name: string
+ description: string
+ policy: AffinityPolicy
+ affinityGroupMembers: string[]
+}
+
+const affinityGroupList = ({ project }: PP.Project) =>
+ apiq('affinityGroupList', { query: { project, limit: ALL_ISH } })
+const antiAffinityGroupList = ({ project }: PP.Project) =>
+ apiq('antiAffinityGroupList', { query: { project, limit: ALL_ISH } })
+
+export async function clientLoader({ params }: LoaderFunctionArgs) {
+ const { project } = getProjectSelector(params)
+ await Promise.all([
+ queryClient.prefetchQuery(antiAffinityGroupList({ project })),
+ queryClient.prefetchQuery(affinityGroupList({ project })),
+ ])
+ return null
+}
+
+export default function CreateAntiAffintyGroupForm() {
+ const { project } = useProjectSelector()
+
+ const navigate = useNavigate()
+ const onDismiss = () => navigate(pb.affinity({ project }))
+
+ const createAntiAffinityGroup = useApiMutation('antiAffinityGroupCreate', {
+ onSuccess(antiAffinityGroup) {
+ navigate(pb.antiAffinityGroup({ project, antiAffinityGroup: antiAffinityGroup.name }))
+ addToast(<>Anti-affinity group {antiAffinityGroup.name} created>) // prettier-ignore
+ queryClient.invalidateQueries(antiAffinityGroupList({ project }))
+ },
+ })
+
+ const {
+ data: { items: existingAntiAffinityGroups },
+ } = usePrefetchedQuery(antiAffinityGroupList({ project }))
+ const defaultValues: AntiAffinityGroupFormValues = {
+ name: '',
+ description: '',
+ policy: 'allow',
+ affinityGroupMembers: [],
+ }
+ const form = useForm({ defaultValues })
+ const control = form.control
+
+ return (
+ {
+ createAntiAffinityGroup.mutate({
+ query: { project },
+ body: {
+ name: values.name,
+ description: values.description,
+ policy: values.policy,
+ failureDomain: 'sled',
+ },
+ })
+ }}
+ loading={createAntiAffinityGroup.isPending}
+ submitError={createAntiAffinityGroup.error}
+ submitLabel="Add group"
+ >
+ {
+ if (existingAntiAffinityGroups.find((g) => g.name === name)) {
+ return 'Name taken. To update an existing group, edit it directly.'
+ }
+ }}
+ />
+
+
+
+
+ )
+}
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index ceb19ab0ed..8dfe773b47 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -9,7 +9,7 @@
import { useQuery } from '@tanstack/react-query'
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useCallback } from 'react'
-import { Link, type LoaderFunctionArgs } from 'react-router'
+import { Link, Outlet, type LoaderFunctionArgs } from 'react-router'
import {
apiq,
@@ -31,10 +31,11 @@ import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
import { Columns } from '~/table/columns/common'
import { Table } from '~/table/Table'
import { Badge } from '~/ui/lib/Badge'
+import { CreateLink } from '~/ui/lib/CreateButton'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { Slash } from '~/ui/lib/Slash'
-import { TableEmptyBox } from '~/ui/lib/Table'
+import { TableActions, TableEmptyBox } from '~/ui/lib/Table'
import { intersperse } from '~/util/array'
import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'
@@ -100,7 +101,9 @@ const staticCols = [
export default function AffinityPage() {
const { project } = useProjectSelector()
- const { data } = usePrefetchedQuery(antiAffinityGroupList({ project }))
+ const {
+ data: { items: antiAffinityGroups },
+ } = usePrefetchedQuery(antiAffinityGroupList({ project }))
const { mutateAsync: deleteGroup } = useApiMutation('antiAffinityGroupDelete', {
onSuccess(_data, variables) {
@@ -155,14 +158,22 @@ export default function AffinityPage() {
const table = useReactTable({
columns,
- data: data.items,
+ data: antiAffinityGroups,
getCoreRowModel: getCoreRowModel(),
})
return (
<>
- {data.items.length ? : }
+
+ New anti-affinity group
+
+ {antiAffinityGroups.length ? (
+
+ ) : (
+
+ )}
+
>
)
}
diff --git a/app/routes.tsx b/app/routes.tsx
index d3bd20531a..740e2fe205 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -500,6 +500,16 @@ export const routes = createRoutesFromElements(
path="access"
lazy={() => import('./pages/project/access/ProjectAccessPage').then(convert)}
/>
+ import('./pages/project/affinity/AffinityPage').then(convert)}
+ element={null}
+ >
+
+ import('./forms/anti-affinity-group-create').then(convert)}
+ />
+
`${projectBase(params)}/affinity`,
+ affinityNew: (params: PP.Project) => `${projectBase(params)}/affinity-new`,
antiAffinityGroup: (params: PP.AntiAffinityGroup) =>
`${pb.affinity(params)}/${params.antiAffinityGroup}`,
diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts
index e0600c95a5..cb672f729b 100644
--- a/mock-api/msw/handlers.ts
+++ b/mock-api/msw/handlers.ts
@@ -1628,6 +1628,38 @@ export const handlers = makeHandlers({
})
return { items: members }
},
+ antiAffinityGroupCreate(params) {
+ const project = lookup.project(params.query)
+ errIfExists(db.antiAffinityGroups, { name: params.body.name, project_id: project.id })
+
+ const newAntiAffinityGroup: Json = {
+ id: uuid(),
+ project_id: project.id,
+ ...params.body,
+ ...getTimestamps(),
+ }
+ db.antiAffinityGroups.push(newAntiAffinityGroup)
+
+ return json(newAntiAffinityGroup, { status: 201 })
+ },
+ antiAffinityGroupUpdate({ body, path, query }) {
+ const antiAffinityGroup = lookup.antiAffinityGroup({ ...path, ...query })
+ if (body.name) {
+ // Error if changing the group name and that group name already exists
+ if (body.name !== antiAffinityGroup.name) {
+ errIfExists(db.antiAffinityGroups, {
+ project_id: antiAffinityGroup.project_id,
+ name: body.name,
+ })
+ }
+ antiAffinityGroup.name = body.name
+ }
+ updateDesc(antiAffinityGroup, body)
+ if (body.description) {
+ antiAffinityGroup.description = body.description
+ }
+ return antiAffinityGroup
+ },
antiAffinityGroupList: ({ query }) => {
const project = lookup.project({ ...query })
const antiAffinityGroups = db.antiAffinityGroups.filter(
@@ -1676,10 +1708,8 @@ export const handlers = makeHandlers({
affinityGroupMemberInstanceDelete: NotImplemented,
affinityGroupMemberInstanceView: NotImplemented,
affinityGroupUpdate: NotImplemented,
- antiAffinityGroupCreate: NotImplemented,
antiAffinityGroupMemberInstanceAdd: NotImplemented,
antiAffinityGroupMemberInstanceView: NotImplemented,
- antiAffinityGroupUpdate: NotImplemented,
certificateCreate: NotImplemented,
certificateDelete: NotImplemented,
certificateList: NotImplemented,
From ffd7cb079d08a94bc7174d315f015bb518029ee7 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Tue, 1 Apr 2025 13:40:18 -0700
Subject: [PATCH 10/56] =?UTF-8?q?ah=20=E2=80=A6=C2=A0crumbs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/routes.tsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/app/routes.tsx b/app/routes.tsx
index 740e2fe205..f9b5b20a83 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -502,9 +502,8 @@ export const routes = createRoutesFromElements(
/>
import('./pages/project/affinity/AffinityPage').then(convert)}
- element={null}
+ handle={{ crumb: 'Affinity' }}
>
-
import('./forms/anti-affinity-group-create').then(convert)}
From 7456008cf4e9d728b2173ed58afb14bc274ca37d Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Tue, 1 Apr 2025 14:32:18 -0700
Subject: [PATCH 11/56] Edit anti-affinity group is working
---
app/forms/anti-affinity-group-edit.tsx | 114 ++++++++++++++++++
app/pages/project/affinity/AffinityPage.tsx | 49 ++++++--
.../affinity/AntiAffinityGroupPage.tsx | 3 +-
app/routes.tsx | 7 +-
app/util/path-builder.ts | 2 +
5 files changed, 163 insertions(+), 12 deletions(-)
create mode 100644 app/forms/anti-affinity-group-edit.tsx
diff --git a/app/forms/anti-affinity-group-edit.tsx b/app/forms/anti-affinity-group-edit.tsx
new file mode 100644
index 0000000000..673e2c2992
--- /dev/null
+++ b/app/forms/anti-affinity-group-edit.tsx
@@ -0,0 +1,114 @@
+/*
+ * 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 { useForm } from 'react-hook-form'
+import { useNavigate, type LoaderFunctionArgs } from 'react-router'
+
+import {
+ apiq,
+ queryClient,
+ useApiMutation,
+ usePrefetchedQuery,
+ type AntiAffinityGroupUpdate,
+} from '@oxide/api'
+
+import { DescriptionField } from '~/components/form/fields/DescriptionField'
+import { NameField } from '~/components/form/fields/NameField'
+import { SideModalForm } from '~/components/form/SideModalForm'
+import { HL } from '~/components/HL'
+import { titleCrumb } from '~/hooks/use-crumbs'
+import {
+ getAntiAffinityGroupSelector,
+ useAntiAffinityGroupSelector,
+} from '~/hooks/use-params'
+import { addToast } from '~/stores/toast'
+import { ALL_ISH } from '~/util/consts'
+import { pb } from '~/util/path-builder'
+import type * as PP from '~/util/path-params'
+
+export const handle = titleCrumb('New anti-affinity group')
+
+const affinityGroupList = ({ project }: PP.Project) =>
+ apiq('affinityGroupList', { query: { project, limit: ALL_ISH } })
+const antiAffinityGroupList = ({ project }: PP.Project) =>
+ apiq('antiAffinityGroupList', { query: { project, limit: ALL_ISH } })
+const antiAffinityGroupView = ({ project, antiAffinityGroup }: PP.AntiAffinityGroup) =>
+ apiq('antiAffinityGroupView', { path: { antiAffinityGroup }, query: { project } })
+
+export async function clientLoader({ params }: LoaderFunctionArgs) {
+ const { project, antiAffinityGroup } = getAntiAffinityGroupSelector(params)
+ await Promise.all([
+ queryClient.prefetchQuery(antiAffinityGroupList({ project })),
+ queryClient.prefetchQuery(affinityGroupList({ project })),
+ queryClient.prefetchQuery(antiAffinityGroupView({ project, antiAffinityGroup })),
+ ])
+ return null
+}
+
+export default function EditAntiAffintyGroupForm() {
+ const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
+
+ const navigate = useNavigate()
+ const onDismiss = () => navigate(pb.antiAffinityGroup({ project, antiAffinityGroup }))
+
+ const editAntiAffinityGroup = useApiMutation('antiAffinityGroupUpdate', {
+ onSuccess(updatedAntiAffinityGroup) {
+ navigate(
+ pb.antiAffinityGroup({ project, antiAffinityGroup: updatedAntiAffinityGroup.name })
+ )
+ addToast(<>Anti-affinity group {updatedAntiAffinityGroup.name} updated>) // prettier-ignore
+ queryClient.invalidateQueries(antiAffinityGroupList({ project }))
+ },
+ })
+
+ const {
+ data: { items: existingAntiAffinityGroups },
+ } = usePrefetchedQuery(antiAffinityGroupList({ project }))
+ const { data: antiAffinityGroupData } = usePrefetchedQuery(
+ antiAffinityGroupView({ project, antiAffinityGroup })
+ )
+ const defaultValues: AntiAffinityGroupUpdate = {
+ name: antiAffinityGroupData.name,
+ description: antiAffinityGroupData.description,
+ }
+ const form = useForm({ defaultValues })
+ const control = form.control
+
+ return (
+ {
+ editAntiAffinityGroup.mutate({
+ path: { antiAffinityGroup },
+ query: { project },
+ body: {
+ name: values.name || antiAffinityGroupData.name,
+ description: values.description || '',
+ },
+ })
+ }}
+ loading={editAntiAffinityGroup.isPending}
+ submitError={editAntiAffinityGroup.error}
+ submitLabel="Edit group"
+ >
+ {
+ if (existingAntiAffinityGroups.find((g) => g.name === name)) {
+ return 'Name taken. To update an existing group, edit it directly.'
+ }
+ }}
+ />
+
+
+ )
+}
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 8dfe773b47..7a58af0d3b 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -9,7 +9,7 @@
import { useQuery } from '@tanstack/react-query'
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useCallback } from 'react'
-import { Link, Outlet, type LoaderFunctionArgs } from 'react-router'
+import { Link, Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
import {
apiq,
@@ -22,7 +22,12 @@ import {
import { Affinity24Icon } from '@oxide/design-system/icons/react'
import { HL } from '~/components/HL'
-import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
+import { MoreActionsMenu } from '~/components/MoreActionsMenu'
+import {
+ getProjectSelector,
+ useAntiAffinityGroupSelector,
+ useProjectSelector,
+} from '~/hooks/use-params'
import { confirmAction } from '~/stores/confirm-action'
import { addToast } from '~/stores/toast'
import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell'
@@ -32,6 +37,7 @@ import { Columns } from '~/table/columns/common'
import { Table } from '~/table/Table'
import { Badge } from '~/ui/lib/Badge'
import { CreateLink } from '~/ui/lib/CreateButton'
+import * as DropdownMenu from '~/ui/lib/DropdownMenu'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { Slash } from '~/ui/lib/Slash'
@@ -65,13 +71,27 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
const colHelper = createColumnHelper()
-export const AffinityPageHeader = ({ name = 'Affinity' }: { name?: string }) => (
-
- }>{name}
- {/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
-
-)
-
+export const AffinityPageHeader = ({ name = 'Affinity' }: { name?: string }) => {
+ const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
+ return (
+
+ }>{name}
+ {name !== 'Affinity' && (
+
+ {/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
+
+
+
+ Edit
+
+
+
+ )}
+
+ )
+}
type AffinityGroupPolicyBadgeProps = { policy: AffinityPolicy; className?: string }
const AffinityGroupPolicyBadge = ({ policy, className }: AffinityGroupPolicyBadgeProps) => (
[
+ {
+ label: 'Edit',
+ onActivate() {
+ navigate(
+ pb.antiAffinityGroupEdit({ project, antiAffinityGroup: antiAffinityGroup.name })
+ )
+ },
+ },
{
label: 'Delete',
onActivate() {
@@ -140,7 +169,7 @@ export default function AffinityPage() {
},
},
],
- [project, deleteGroup]
+ [project, deleteGroup, navigate]
)
const columns = useColsWithActions(
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 4e259aaeba..d2703fe019 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -8,7 +8,7 @@
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useCallback } from 'react'
-import type { LoaderFunctionArgs } from 'react-router'
+import { Outlet, type LoaderFunctionArgs } from 'react-router'
import { Affinity24Icon } from '@oxide/design-system/icons/react'
@@ -177,6 +177,7 @@ export default function AntiAffinityPage() {
{membersCount ? : }
+
>
)
}
diff --git a/app/routes.tsx b/app/routes.tsx
index f9b5b20a83..6dd1b5397e 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -519,7 +519,12 @@ export const routes = createRoutesFromElements(
lazy={() =>
import('./pages/project/affinity/AntiAffinityGroupPage.tsx').then(convert)
}
- />
+ >
+ import('./forms/anti-affinity-group-edit').then(convert)}
+ />
+
diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts
index 756b24b7dc..11ddc6ab3f 100644
--- a/app/util/path-builder.ts
+++ b/app/util/path-builder.ts
@@ -100,6 +100,8 @@ export const pb = {
affinityNew: (params: PP.Project) => `${projectBase(params)}/affinity-new`,
antiAffinityGroup: (params: PP.AntiAffinityGroup) =>
`${pb.affinity(params)}/${params.antiAffinityGroup}`,
+ antiAffinityGroupEdit: (params: PP.AntiAffinityGroup) =>
+ `${pb.antiAffinityGroup(params)}/edit`,
siloUtilization: () => '/utilization',
siloAccess: () => '/access',
From e837d9d85df58fe364b539a24a22d4de1bdd4deb Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Tue, 1 Apr 2025 14:52:15 -0700
Subject: [PATCH 12/56] Fix bug; add ID column to table
---
app/pages/project/affinity/AffinityPage.tsx | 22 +++++++++------------
app/table/columns/common.tsx | 12 +++++++++++
2 files changed, 21 insertions(+), 13 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 7a58af0d3b..0c032c65b5 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -23,11 +23,7 @@ import { Affinity24Icon } from '@oxide/design-system/icons/react'
import { HL } from '~/components/HL'
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
-import {
- getProjectSelector,
- useAntiAffinityGroupSelector,
- useProjectSelector,
-} from '~/hooks/use-params'
+import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { confirmAction } from '~/stores/confirm-action'
import { addToast } from '~/stores/toast'
import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell'
@@ -72,23 +68,22 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
const colHelper = createColumnHelper()
export const AffinityPageHeader = ({ name = 'Affinity' }: { name?: string }) => {
- const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
+ const { project } = useProjectSelector()
return (
}>{name}
- {name !== 'Affinity' && (
-
- {/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
-
+
+ {/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
+ {name !== 'Affinity' && (
Edit
-
- )}
+ )}
+
)
}
@@ -104,6 +99,7 @@ const AffinityGroupPolicyBadge = ({ policy, className }: AffinityGroupPolicyBadg
)
const staticCols = [
+ colHelper.accessor('id', Columns.id),
colHelper.accessor('description', Columns.description),
colHelper.accessor(() => {}, {
header: 'type',
diff --git a/app/table/columns/common.tsx b/app/table/columns/common.tsx
index f2d0496830..a25f34afd5 100644
--- a/app/table/columns/common.tsx
+++ b/app/table/columns/common.tsx
@@ -11,7 +11,9 @@ import { filesize } from 'filesize'
import type { InstanceState } from '~/api'
import { InstanceStateBadge } from '~/components/StateBadge'
import { DescriptionCell } from '~/table/cells/DescriptionCell'
+import { CopyToClipboard } from '~/ui/lib/CopyToClipboard'
import { DateTime } from '~/ui/lib/DateTime'
+import { Truncate } from '~/ui/lib/Truncate'
// the full type of the info arg is CellContext from RT, but in these
// cells we only care about the return value of getValue
@@ -21,6 +23,15 @@ function dateCell(info: Info) {
return
}
+function idCell(info: Info) {
+ return (
+
+
+
+
+ )
+}
+
function instanceStateCell(info: Info) {
return
}
@@ -40,6 +51,7 @@ export const Columns = {
description: {
cell: (info: Info) => ,
},
+ id: { header: 'ID', cell: idCell },
instanceState: { header: 'state', cell: instanceStateCell },
size: { cell: sizeCell },
timeCreated: { header: 'created', cell: dateCell },
From e93d50603eae61b1b5b3e70f0711402e46652658 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 15:14:41 -0700
Subject: [PATCH 13/56] can add instances to an anti-affinity group
---
app/forms/anti-affinity-group-member-add.tsx | 122 ++++++++++++++++++
.../affinity/AntiAffinityGroupPage.tsx | 22 +++-
mock-api/msw/handlers.ts | 30 ++++-
3 files changed, 171 insertions(+), 3 deletions(-)
create mode 100644 app/forms/anti-affinity-group-member-add.tsx
diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx
new file mode 100644
index 0000000000..adbabbe443
--- /dev/null
+++ b/app/forms/anti-affinity-group-member-add.tsx
@@ -0,0 +1,122 @@
+/*
+ * 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 { useForm } from 'react-hook-form'
+import type { LoaderFunctionArgs } from 'react-router'
+
+import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '~/api'
+import { ComboboxField } from '~/components/form/fields/ComboboxField'
+import { HL } from '~/components/HL'
+import {
+ getAntiAffinityGroupSelector,
+ useAntiAffinityGroupSelector,
+} from '~/hooks/use-params'
+import { addToast } from '~/stores/toast'
+import { toComboboxItems } from '~/ui/lib/Combobox'
+import { Modal } from '~/ui/lib/Modal'
+import { ALL_ISH } from '~/util/consts'
+import type * as PP from '~/util/path-params'
+
+const memberList = ({ antiAffinityGroup, project }: PP.AntiAffinityGroup) =>
+ apiq('antiAffinityGroupMemberList', {
+ path: { antiAffinityGroup },
+ // member limit in DB is currently 32, so pagination isn't needed
+ query: { project, limit: ALL_ISH },
+ })
+const instanceList = ({ project }: PP.Project) =>
+ apiq('instanceList', { query: { project, limit: ALL_ISH } })
+const affinityGroupList = ({ project }: PP.Project) =>
+ apiq('affinityGroupList', { query: { project, limit: ALL_ISH } })
+
+export async function clientLoader({ params }: LoaderFunctionArgs) {
+ const { antiAffinityGroup, project } = getAntiAffinityGroupSelector(params)
+ await Promise.all([
+ queryClient.fetchQuery(memberList({ antiAffinityGroup, project })),
+ queryClient.prefetchQuery(instanceList({ project })),
+ queryClient.prefetchQuery(affinityGroupList({ project })),
+ ])
+ return null
+}
+
+export function AddAntiAffinityGroupMemberForm({
+ isModalOpen,
+ setIsModalOpen,
+}: {
+ isModalOpen: boolean
+ setIsModalOpen: (open: boolean) => void
+}) {
+ const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
+
+ const form = useForm({
+ defaultValues: {
+ antiAffinityGroupMember: '',
+ },
+ })
+
+ const onDismiss = () => {
+ setIsModalOpen(false)
+ form.reset()
+ }
+
+ const { data: members } = usePrefetchedQuery(memberList({ antiAffinityGroup, project }))
+
+ // We need to construct a list of availableInstances. These should not include any
+ // instances already in this anti-affinity group. They should also not include any
+ // instances that are already in an affinity group that is included in this list.
+ const { data: instances } = usePrefetchedQuery(instanceList({ project }))
+ const availableInstances = toComboboxItems(
+ instances.items.filter((instance) => {
+ const isInThisGroup = members.items.some(({ value }) => value.name === instance.name)
+ // TODO: Check if the instance is already in an affinity group that is in this anti-affinity group
+ return !isInThisGroup
+ })
+ )
+
+ const { mutateAsync: addMember } = useApiMutation('antiAffinityGroupMemberInstanceAdd', {
+ onSuccess(_data, variables) {
+ onDismiss()
+ queryClient.invalidateEndpoint('antiAffinityGroupMemberList')
+ queryClient.invalidateEndpoint('antiAffinityGroupView')
+ addToast(<>Member {variables.path.instance} added to anti-affinity group {antiAffinityGroup} >) // prettier-ignore
+ },
+ })
+
+ return (
+
+
+
+
+ Select an instance to add to the anti-affinity group{' '}
+ {antiAffinityGroup} .
+
+
+
+
+
+ addMember({
+ path: {
+ antiAffinityGroup,
+ instance: form.getValues('antiAffinityGroupMember'),
+ },
+ query: { project },
+ })
+ }
+ actionText="Add to group"
+ />
+
+ )
+}
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index d2703fe019..ca34d175f2 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -7,7 +7,7 @@
*/
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
-import { useCallback } from 'react'
+import { useCallback, useState } from 'react'
import { Outlet, type LoaderFunctionArgs } from 'react-router'
import { Affinity24Icon } from '@oxide/design-system/icons/react'
@@ -20,6 +20,7 @@ import {
type AntiAffinityGroupMember,
} from '~/api'
import { HL } from '~/components/HL'
+import { AddAntiAffinityGroupMemberForm } from '~/forms/anti-affinity-group-member-add'
import { makeCrumb } from '~/hooks/use-crumbs'
import {
getAntiAffinityGroupSelector,
@@ -33,6 +34,7 @@ import { Columns } from '~/table/columns/common'
import { Table } from '~/table/Table'
import { Badge } from '~/ui/lib/Badge'
import { CardBlock } from '~/ui/lib/CardBlock'
+import { CreateButton } from '~/ui/lib/CreateButton'
import { Divider } from '~/ui/lib/Divider'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
@@ -58,12 +60,18 @@ const memberList = ({ antiAffinityGroup, project }: PP.AntiAffinityGroup) =>
// member limit in DB is currently 32, so pagination isn't needed
query: { project, limit: ALL_ISH },
})
+const instanceList = ({ project }: PP.Project) =>
+ apiq('instanceList', { query: { project, limit: ALL_ISH } })
+const affinityGroupList = ({ project }: PP.Project) =>
+ apiq('affinityGroupList', { query: { project, limit: ALL_ISH } })
export async function clientLoader({ params }: LoaderFunctionArgs) {
const { antiAffinityGroup, project } = getAntiAffinityGroupSelector(params)
await Promise.all([
queryClient.fetchQuery(antiAffinityGroupView({ antiAffinityGroup, project })),
queryClient.fetchQuery(memberList({ antiAffinityGroup, project })),
+ queryClient.prefetchQuery(instanceList({ project })),
+ queryClient.prefetchQuery(affinityGroupList({ project })),
])
return null
}
@@ -97,6 +105,8 @@ export default function AntiAffinityPage() {
},
}
)
+ // useState is at this level because the CreateButton needs to control the modal
+ const [isModalOpen, setIsModalOpen] = useState(false)
const makeActions = useCallback(
(antiAffinityGroupMember: AntiAffinityGroupMember): MenuAction[] => [
@@ -172,11 +182,19 @@ export default function AntiAffinityPage() {
+ >
+ setIsModalOpen(true)}>
+ Add instance to group
+
+
{membersCount ? : }
+
>
)
diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts
index cb672f729b..acf64c93c7 100644
--- a/mock-api/msw/handlers.ts
+++ b/mock-api/msw/handlers.ts
@@ -1686,6 +1686,35 @@ export const handlers = makeHandlers({
})
return { items: members }
},
+ antiAffinityGroupMemberInstanceAdd({ path, query }) {
+ const project = lookup.project({ ...query })
+ const instance = lookup.instance({ ...query, instance: path.instance })
+ const antiAffinityGroup = lookup.antiAffinityGroup({
+ project: project.id,
+ antiAffinityGroup: path.antiAffinityGroup,
+ })
+ const alreadyThere = db.antiAffinityGroupMemberLists.some(
+ (i) =>
+ i.anti_affinity_group_id === antiAffinityGroup.id &&
+ i.anti_affinity_group_member.id === instance.id
+ )
+ if (alreadyThere) {
+ throw 'Instance already in anti-affinity group'
+ }
+ const newMember: Json = {
+ type: 'instance',
+ value: {
+ id: instance.id,
+ name: instance.name,
+ run_state: instance.run_state,
+ },
+ }
+ db.antiAffinityGroupMemberLists.push({
+ anti_affinity_group_id: antiAffinityGroup.id,
+ anti_affinity_group_member: { type: 'instance', id: instance.id },
+ })
+ return json(newMember, { status: 201 })
+ },
antiAffinityGroupMemberInstanceDelete: ({ path, query }) => {
const project = lookup.project({ ...query })
const instance = lookup.instance({ ...query, instance: path.instance })
@@ -1708,7 +1737,6 @@ export const handlers = makeHandlers({
affinityGroupMemberInstanceDelete: NotImplemented,
affinityGroupMemberInstanceView: NotImplemented,
affinityGroupUpdate: NotImplemented,
- antiAffinityGroupMemberInstanceAdd: NotImplemented,
antiAffinityGroupMemberInstanceView: NotImplemented,
certificateCreate: NotImplemented,
certificateDelete: NotImplemented,
From 3511e0d356fae71fefcdf12db8d3ad298d691c93 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 15:31:17 -0700
Subject: [PATCH 14/56] Update to ID column truncation
---
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 1 +
app/table/columns/common.tsx | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index ca34d175f2..159057c8c2 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -151,6 +151,7 @@ export default function AntiAffinityPage() {
header: 'Name',
cell: makeLinkCell((instance) => pb.instance({ project, instance })),
}),
+ colHelper.accessor('value.id', Columns.id),
colHelper.accessor('value.runState', Columns.instanceState),
],
makeActions
diff --git a/app/table/columns/common.tsx b/app/table/columns/common.tsx
index a25f34afd5..e20059eb21 100644
--- a/app/table/columns/common.tsx
+++ b/app/table/columns/common.tsx
@@ -26,7 +26,7 @@ function dateCell(info: Info) {
function idCell(info: Info) {
return (
-
+
)
From 271b8545893b928269815979321e3414d3fe900e Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 16:43:37 -0700
Subject: [PATCH 15/56] Refactoring
---
app/forms/affinity-util.tsx | 36 +++++++++++++++
app/forms/anti-affinity-group-create.tsx | 10 +---
app/forms/anti-affinity-group-edit.tsx | 16 +++----
app/forms/anti-affinity-group-member-add.tsx | 46 +++++++------------
app/pages/project/affinity/AffinityPage.tsx | 13 +-----
.../affinity/AntiAffinityGroupPage.tsx | 30 +++++-------
6 files changed, 73 insertions(+), 78 deletions(-)
create mode 100644 app/forms/affinity-util.tsx
diff --git a/app/forms/affinity-util.tsx b/app/forms/affinity-util.tsx
new file mode 100644
index 0000000000..21503233b8
--- /dev/null
+++ b/app/forms/affinity-util.tsx
@@ -0,0 +1,36 @@
+/*
+ * 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 { apiq } from '~/api'
+import { ALL_ISH } from '~/util/consts'
+import type * as PP from '~/util/path-params'
+
+export const antiAffinityGroupMemberList = ({
+ antiAffinityGroup,
+ project,
+}: PP.AntiAffinityGroup) =>
+ apiq('antiAffinityGroupMemberList', {
+ path: { antiAffinityGroup },
+ // member limit in DB is currently 32, so pagination isn't needed
+ query: { project, limit: ALL_ISH },
+ })
+
+export const instanceList = ({ project }: PP.Project) =>
+ apiq('instanceList', { query: { project, limit: ALL_ISH } })
+
+export const affinityGroupList = ({ project }: PP.Project) =>
+ apiq('affinityGroupList', { query: { project, limit: ALL_ISH } })
+
+export const antiAffinityGroupList = ({ project }: PP.Project) =>
+ apiq('antiAffinityGroupList', { query: { project, limit: ALL_ISH } })
+
+export const antiAffinityGroupView = ({
+ project,
+ antiAffinityGroup,
+}: PP.AntiAffinityGroup) =>
+ apiq('antiAffinityGroupView', { path: { antiAffinityGroup }, query: { project } })
diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx
index 95bf07e2f9..1b45352f75 100644
--- a/app/forms/anti-affinity-group-create.tsx
+++ b/app/forms/anti-affinity-group-create.tsx
@@ -9,7 +9,6 @@ import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router'
import {
- apiq,
queryClient,
useApiMutation,
usePrefetchedQuery,
@@ -24,9 +23,9 @@ import { HL } from '~/components/HL'
import { titleCrumb } from '~/hooks/use-crumbs'
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
-import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'
-import type * as PP from '~/util/path-params'
+
+import { affinityGroupList, antiAffinityGroupList } from './affinity-util'
export const handle = titleCrumb('New anti-affinity group')
@@ -37,11 +36,6 @@ type AntiAffinityGroupFormValues = {
affinityGroupMembers: string[]
}
-const affinityGroupList = ({ project }: PP.Project) =>
- apiq('affinityGroupList', { query: { project, limit: ALL_ISH } })
-const antiAffinityGroupList = ({ project }: PP.Project) =>
- apiq('antiAffinityGroupList', { query: { project, limit: ALL_ISH } })
-
export async function clientLoader({ params }: LoaderFunctionArgs) {
const { project } = getProjectSelector(params)
await Promise.all([
diff --git a/app/forms/anti-affinity-group-edit.tsx b/app/forms/anti-affinity-group-edit.tsx
index 673e2c2992..08a9f91784 100644
--- a/app/forms/anti-affinity-group-edit.tsx
+++ b/app/forms/anti-affinity-group-edit.tsx
@@ -9,7 +9,6 @@ import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router'
import {
- apiq,
queryClient,
useApiMutation,
usePrefetchedQuery,
@@ -26,18 +25,15 @@ import {
useAntiAffinityGroupSelector,
} from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
-import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'
-import type * as PP from '~/util/path-params'
-export const handle = titleCrumb('New anti-affinity group')
+import {
+ affinityGroupList,
+ antiAffinityGroupList,
+ antiAffinityGroupView,
+} from './affinity-util'
-const affinityGroupList = ({ project }: PP.Project) =>
- apiq('affinityGroupList', { query: { project, limit: ALL_ISH } })
-const antiAffinityGroupList = ({ project }: PP.Project) =>
- apiq('antiAffinityGroupList', { query: { project, limit: ALL_ISH } })
-const antiAffinityGroupView = ({ project, antiAffinityGroup }: PP.AntiAffinityGroup) =>
- apiq('antiAffinityGroupView', { path: { antiAffinityGroup }, query: { project } })
+export const handle = titleCrumb('New anti-affinity group')
export async function clientLoader({ params }: LoaderFunctionArgs) {
const { project, antiAffinityGroup } = getAntiAffinityGroupSelector(params)
diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx
index adbabbe443..a724861afd 100644
--- a/app/forms/anti-affinity-group-member-add.tsx
+++ b/app/forms/anti-affinity-group-member-add.tsx
@@ -9,7 +9,7 @@
import { useForm } from 'react-hook-form'
import type { LoaderFunctionArgs } from 'react-router'
-import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '~/api'
+import { queryClient, useApiMutation, usePrefetchedQuery } from '~/api'
import { ComboboxField } from '~/components/form/fields/ComboboxField'
import { HL } from '~/components/HL'
import {
@@ -19,24 +19,17 @@ import {
import { addToast } from '~/stores/toast'
import { toComboboxItems } from '~/ui/lib/Combobox'
import { Modal } from '~/ui/lib/Modal'
-import { ALL_ISH } from '~/util/consts'
-import type * as PP from '~/util/path-params'
-const memberList = ({ antiAffinityGroup, project }: PP.AntiAffinityGroup) =>
- apiq('antiAffinityGroupMemberList', {
- path: { antiAffinityGroup },
- // member limit in DB is currently 32, so pagination isn't needed
- query: { project, limit: ALL_ISH },
- })
-const instanceList = ({ project }: PP.Project) =>
- apiq('instanceList', { query: { project, limit: ALL_ISH } })
-const affinityGroupList = ({ project }: PP.Project) =>
- apiq('affinityGroupList', { query: { project, limit: ALL_ISH } })
+import {
+ affinityGroupList,
+ antiAffinityGroupMemberList,
+ instanceList,
+} from './affinity-util'
export async function clientLoader({ params }: LoaderFunctionArgs) {
const { antiAffinityGroup, project } = getAntiAffinityGroupSelector(params)
await Promise.all([
- queryClient.fetchQuery(memberList({ antiAffinityGroup, project })),
+ queryClient.fetchQuery(antiAffinityGroupMemberList({ antiAffinityGroup, project })),
queryClient.prefetchQuery(instanceList({ project })),
queryClient.prefetchQuery(affinityGroupList({ project })),
])
@@ -52,6 +45,15 @@ export function AddAntiAffinityGroupMemberForm({
}) {
const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
+ const { data: members } = usePrefetchedQuery(
+ antiAffinityGroupMemberList({ antiAffinityGroup, project })
+ )
+ const { data: instances } = usePrefetchedQuery(instanceList({ project }))
+ // Construct a list of all instances not currently in this anti-affinity group.
+ const availableInstances = instances.items.filter(
+ (instance) => !members.items.some(({ value }) => value.name === instance.name)
+ )
+
const form = useForm({
defaultValues: {
antiAffinityGroupMember: '',
@@ -63,20 +65,6 @@ export function AddAntiAffinityGroupMemberForm({
form.reset()
}
- const { data: members } = usePrefetchedQuery(memberList({ antiAffinityGroup, project }))
-
- // We need to construct a list of availableInstances. These should not include any
- // instances already in this anti-affinity group. They should also not include any
- // instances that are already in an affinity group that is included in this list.
- const { data: instances } = usePrefetchedQuery(instanceList({ project }))
- const availableInstances = toComboboxItems(
- instances.items.filter((instance) => {
- const isInThisGroup = members.items.some(({ value }) => value.name === instance.name)
- // TODO: Check if the instance is already in an affinity group that is in this anti-affinity group
- return !isInThisGroup
- })
- )
-
const { mutateAsync: addMember } = useApiMutation('antiAffinityGroupMemberInstanceAdd', {
onSuccess(_data, variables) {
onDismiss()
@@ -98,7 +86,7 @@ export function AddAntiAffinityGroupMemberForm({
label="Instance"
placeholder="Select an instance"
name="antiAffinityGroupMember"
- items={availableInstances}
+ items={toComboboxItems(availableInstances)}
required
control={form.control}
/>
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 0c032c65b5..94a4d5f06d 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -12,7 +12,6 @@ import { useCallback } from 'react'
import { Link, Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
import {
- apiq,
queryClient,
useApiMutation,
usePrefetchedQuery,
@@ -23,6 +22,7 @@ import { Affinity24Icon } from '@oxide/design-system/icons/react'
import { HL } from '~/components/HL'
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
+import { antiAffinityGroupList, memberList } from '~/forms/affinity-util'
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { confirmAction } from '~/stores/confirm-action'
import { addToast } from '~/stores/toast'
@@ -39,18 +39,7 @@ import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { Slash } from '~/ui/lib/Slash'
import { TableActions, TableEmptyBox } from '~/ui/lib/Table'
import { intersperse } from '~/util/array'
-import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'
-import type * as PP from '~/util/path-params'
-
-const antiAffinityGroupList = ({ project }: PP.Project) =>
- apiq('antiAffinityGroupList', { query: { project, limit: ALL_ISH } })
-const memberList = ({ antiAffinityGroup, project }: PP.AntiAffinityGroup) =>
- apiq('antiAffinityGroupMemberList', {
- path: { antiAffinityGroup },
- // We only need to get the first 2 members for preview
- query: { project, limit: 2 },
- })
export async function clientLoader({ params }: LoaderFunctionArgs) {
const { project } = getProjectSelector(params)
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 159057c8c2..e9c889b671 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -13,13 +13,18 @@ import { Outlet, type LoaderFunctionArgs } from 'react-router'
import { Affinity24Icon } from '@oxide/design-system/icons/react'
import {
- apiq,
queryClient,
useApiMutation,
usePrefetchedQuery,
type AntiAffinityGroupMember,
} from '~/api'
import { HL } from '~/components/HL'
+import {
+ affinityGroupList,
+ antiAffinityGroupMemberList,
+ antiAffinityGroupView,
+ instanceList,
+} from '~/forms/affinity-util'
import { AddAntiAffinityGroupMemberForm } from '~/forms/anti-affinity-group-member-add'
import { makeCrumb } from '~/hooks/use-crumbs'
import {
@@ -39,9 +44,7 @@ import { Divider } from '~/ui/lib/Divider'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { TableEmptyBox } from '~/ui/lib/Table'
-import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'
-import type * as PP from '~/util/path-params'
import { AffinityPageHeader } from './AffinityPage'
@@ -52,24 +55,11 @@ export const handle = makeCrumb(
const colHelper = createColumnHelper()
-const antiAffinityGroupView = ({ antiAffinityGroup, project }: PP.AntiAffinityGroup) =>
- apiq('antiAffinityGroupView', { path: { antiAffinityGroup }, query: { project } })
-const memberList = ({ antiAffinityGroup, project }: PP.AntiAffinityGroup) =>
- apiq('antiAffinityGroupMemberList', {
- path: { antiAffinityGroup },
- // member limit in DB is currently 32, so pagination isn't needed
- query: { project, limit: ALL_ISH },
- })
-const instanceList = ({ project }: PP.Project) =>
- apiq('instanceList', { query: { project, limit: ALL_ISH } })
-const affinityGroupList = ({ project }: PP.Project) =>
- apiq('affinityGroupList', { query: { project, limit: ALL_ISH } })
-
export async function clientLoader({ params }: LoaderFunctionArgs) {
const { antiAffinityGroup, project } = getAntiAffinityGroupSelector(params)
await Promise.all([
queryClient.fetchQuery(antiAffinityGroupView({ antiAffinityGroup, project })),
- queryClient.fetchQuery(memberList({ antiAffinityGroup, project })),
+ queryClient.fetchQuery(antiAffinityGroupMemberList({ antiAffinityGroup, project })),
queryClient.prefetchQuery(instanceList({ project })),
queryClient.prefetchQuery(affinityGroupList({ project })),
])
@@ -92,7 +82,9 @@ export default function AntiAffinityPage() {
antiAffinityGroupView({ antiAffinityGroup, project })
)
const { id, name, description, policy, timeCreated } = group
- const { data: members } = usePrefetchedQuery(memberList({ antiAffinityGroup, project }))
+ const { data: members } = usePrefetchedQuery(
+ antiAffinityGroupMemberList({ antiAffinityGroup, project })
+ )
const membersCount = members.items.length
const { mutateAsync: removeMember } = useApiMutation(
@@ -105,7 +97,7 @@ export default function AntiAffinityPage() {
},
}
)
- // useState is at this level because the CreateButton needs to control the modal
+ // useState is at this level so the CreateButton can open the modal
const [isModalOpen, setIsModalOpen] = useState(false)
const makeActions = useCallback(
From edd0456c3404f08dc171144d6482308684bd285f Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 16:49:00 -0700
Subject: [PATCH 16/56] Missed a spot in the refactoring
---
app/pages/project/affinity/AffinityPage.tsx | 12 ++++++++----
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 2 +-
2 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 94a4d5f06d..deaf08b03a 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -22,7 +22,7 @@ import { Affinity24Icon } from '@oxide/design-system/icons/react'
import { HL } from '~/components/HL'
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
-import { antiAffinityGroupList, memberList } from '~/forms/affinity-util'
+import { antiAffinityGroupList, antiAffinityGroupMemberList } from '~/forms/affinity-util'
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { confirmAction } from '~/stores/confirm-action'
import { addToast } from '~/stores/toast'
@@ -45,7 +45,9 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
const { project } = getProjectSelector(params)
const groups = await queryClient.fetchQuery(antiAffinityGroupList({ project }))
const memberFetches = groups.items.map(({ name }) =>
- queryClient.prefetchQuery(memberList({ antiAffinityGroup: name, project }))
+ queryClient.prefetchQuery(
+ antiAffinityGroupMemberList({ antiAffinityGroup: name, project })
+ )
)
// The browser will fetch up to 6 anti-affinity group member lists without queuing,
// so we can prefetch them without slowing down the page. If there are more than 6 groups,
@@ -88,7 +90,6 @@ const AffinityGroupPolicyBadge = ({ policy, className }: AffinityGroupPolicyBadg
)
const staticCols = [
- colHelper.accessor('id', Columns.id),
colHelper.accessor('description', Columns.description),
colHelper.accessor(() => {}, {
header: 'type',
@@ -101,6 +102,7 @@ const staticCols = [
header: 'members',
cell: (info) => ,
}),
+ colHelper.accessor('id', Columns.id),
colHelper.accessor('timeCreated', Columns.timeCreated),
]
@@ -209,7 +211,9 @@ export const AffinityGroupMembersCell = ({
antiAffinityGroup: string
}) => {
const { project } = useProjectSelector()
- const { data: members } = useQuery(memberList({ antiAffinityGroup, project }))
+ const { data: members } = useQuery(
+ antiAffinityGroupMemberList({ antiAffinityGroup, project })
+ )
if (!members) return
if (!members.items.length) return
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index e9c889b671..f98908cee3 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -143,8 +143,8 @@ export default function AntiAffinityPage() {
header: 'Name',
cell: makeLinkCell((instance) => pb.instance({ project, instance })),
}),
- colHelper.accessor('value.id', Columns.id),
colHelper.accessor('value.runState', Columns.instanceState),
+ colHelper.accessor('value.id', Columns.id),
],
makeActions
)
From c307e7231a2b7f0fb1f8dc45d9483591a0de620d Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 17:40:54 -0700
Subject: [PATCH 17/56] Update snapshots
---
.../__snapshots__/path-builder.spec.ts.snap | 32 +++++++++++++++++++
app/util/path-builder.spec.ts | 2 ++
2 files changed, 34 insertions(+)
diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap
index 922c3b6c12..fc51c123cc 100644
--- a/app/util/__snapshots__/path-builder.spec.ts.snap
+++ b/app/util/__snapshots__/path-builder.spec.ts.snap
@@ -16,6 +16,20 @@ exports[`breadcrumbs 2`] = `
"path": "/projects/p/affinity",
},
],
+ "affinityNew (/projects/p/affinity-new)": [
+ {
+ "label": "Projects",
+ "path": "/projects",
+ },
+ {
+ "label": "p",
+ "path": "/projects/p/instances",
+ },
+ {
+ "label": "Affinity",
+ "path": "/projects/p/",
+ },
+ ],
"antiAffinityGroup (/projects/p/affinity/aag)": [
{
"label": "Projects",
@@ -34,6 +48,24 @@ exports[`breadcrumbs 2`] = `
"path": "/projects/p/affinity/aag",
},
],
+ "antiAffinityGroupEdit (/projects/p/affinity/aag/edit)": [
+ {
+ "label": "Projects",
+ "path": "/projects",
+ },
+ {
+ "label": "p",
+ "path": "/projects/p/instances",
+ },
+ {
+ "label": "Affinity",
+ "path": "/projects/p/affinity",
+ },
+ {
+ "label": "aag",
+ "path": "/projects/p/affinity/aag",
+ },
+ ],
"deviceSuccess (/device/success)": [],
"diskInventory (/system/inventory/disks)": [
{
diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts
index a7949dcca0..c8992564f3 100644
--- a/app/util/path-builder.spec.ts
+++ b/app/util/path-builder.spec.ts
@@ -42,7 +42,9 @@ test('path builder', () => {
.toMatchInlineSnapshot(`
{
"affinity": "/projects/p/affinity",
+ "affinityNew": "/projects/p/affinity-new",
"antiAffinityGroup": "/projects/p/affinity/aag",
+ "antiAffinityGroupEdit": "/projects/p/affinity/aag/edit",
"deviceSuccess": "/device/success",
"diskInventory": "/system/inventory/disks",
"disks": "/projects/p/disks",
From b31c1ca9298c7d07db10b54a9d8238402bc786f8 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 17:53:15 -0700
Subject: [PATCH 18/56] Can just use prefetchQuery, since we don't need
returned data yet
---
app/forms/anti-affinity-group-member-add.tsx | 2 +-
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx
index a724861afd..86233f6590 100644
--- a/app/forms/anti-affinity-group-member-add.tsx
+++ b/app/forms/anti-affinity-group-member-add.tsx
@@ -29,7 +29,7 @@ import {
export async function clientLoader({ params }: LoaderFunctionArgs) {
const { antiAffinityGroup, project } = getAntiAffinityGroupSelector(params)
await Promise.all([
- queryClient.fetchQuery(antiAffinityGroupMemberList({ antiAffinityGroup, project })),
+ queryClient.prefetchQuery(antiAffinityGroupMemberList({ antiAffinityGroup, project })),
queryClient.prefetchQuery(instanceList({ project })),
queryClient.prefetchQuery(affinityGroupList({ project })),
])
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index f98908cee3..922f30d6f2 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -58,8 +58,8 @@ const colHelper = createColumnHelper()
export async function clientLoader({ params }: LoaderFunctionArgs) {
const { antiAffinityGroup, project } = getAntiAffinityGroupSelector(params)
await Promise.all([
- queryClient.fetchQuery(antiAffinityGroupView({ antiAffinityGroup, project })),
- queryClient.fetchQuery(antiAffinityGroupMemberList({ antiAffinityGroup, project })),
+ queryClient.prefetchQuery(antiAffinityGroupView({ antiAffinityGroup, project })),
+ queryClient.prefetchQuery(antiAffinityGroupMemberList({ antiAffinityGroup, project })),
queryClient.prefetchQuery(instanceList({ project })),
queryClient.prefetchQuery(affinityGroupList({ project })),
])
From d1f7945ee66129e36fae1e1b45f065a8caaa9d1a Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 17:54:53 -0700
Subject: [PATCH 19/56] reorder functions
---
app/forms/affinity-util.tsx | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/app/forms/affinity-util.tsx b/app/forms/affinity-util.tsx
index 21503233b8..1dac1b6992 100644
--- a/app/forms/affinity-util.tsx
+++ b/app/forms/affinity-util.tsx
@@ -10,16 +10,6 @@ import { apiq } from '~/api'
import { ALL_ISH } from '~/util/consts'
import type * as PP from '~/util/path-params'
-export const antiAffinityGroupMemberList = ({
- antiAffinityGroup,
- project,
-}: PP.AntiAffinityGroup) =>
- apiq('antiAffinityGroupMemberList', {
- path: { antiAffinityGroup },
- // member limit in DB is currently 32, so pagination isn't needed
- query: { project, limit: ALL_ISH },
- })
-
export const instanceList = ({ project }: PP.Project) =>
apiq('instanceList', { query: { project, limit: ALL_ISH } })
@@ -34,3 +24,13 @@ export const antiAffinityGroupView = ({
antiAffinityGroup,
}: PP.AntiAffinityGroup) =>
apiq('antiAffinityGroupView', { path: { antiAffinityGroup }, query: { project } })
+
+export const antiAffinityGroupMemberList = ({
+ antiAffinityGroup,
+ project,
+}: PP.AntiAffinityGroup) =>
+ apiq('antiAffinityGroupMemberList', {
+ path: { antiAffinityGroup },
+ // member limit in DB is currently 32, so pagination isn't needed
+ query: { project, limit: ALL_ISH },
+ })
From 97b01375281ded667eb6bdc272ca3f26f0738a2b Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 21:28:28 -0700
Subject: [PATCH 20/56] move instanceList fetching up a level; use to disable
button when no instances available
---
app/forms/anti-affinity-group-member-add.tsx | 35 +++----------------
.../affinity/AntiAffinityGroupPage.tsx | 12 ++++++-
2 files changed, 15 insertions(+), 32 deletions(-)
diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx
index 86233f6590..adc9d8c34a 100644
--- a/app/forms/anti-affinity-group-member-add.tsx
+++ b/app/forms/anti-affinity-group-member-add.tsx
@@ -7,53 +7,26 @@
*/
import { useForm } from 'react-hook-form'
-import type { LoaderFunctionArgs } from 'react-router'
-import { queryClient, useApiMutation, usePrefetchedQuery } from '~/api'
+import { queryClient, useApiMutation, type Instance } from '~/api'
import { ComboboxField } from '~/components/form/fields/ComboboxField'
import { HL } from '~/components/HL'
-import {
- getAntiAffinityGroupSelector,
- useAntiAffinityGroupSelector,
-} from '~/hooks/use-params'
+import { useAntiAffinityGroupSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { toComboboxItems } from '~/ui/lib/Combobox'
import { Modal } from '~/ui/lib/Modal'
-import {
- affinityGroupList,
- antiAffinityGroupMemberList,
- instanceList,
-} from './affinity-util'
-
-export async function clientLoader({ params }: LoaderFunctionArgs) {
- const { antiAffinityGroup, project } = getAntiAffinityGroupSelector(params)
- await Promise.all([
- queryClient.prefetchQuery(antiAffinityGroupMemberList({ antiAffinityGroup, project })),
- queryClient.prefetchQuery(instanceList({ project })),
- queryClient.prefetchQuery(affinityGroupList({ project })),
- ])
- return null
-}
-
export function AddAntiAffinityGroupMemberForm({
+ availableInstances,
isModalOpen,
setIsModalOpen,
}: {
+ availableInstances: Instance[]
isModalOpen: boolean
setIsModalOpen: (open: boolean) => void
}) {
const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
- const { data: members } = usePrefetchedQuery(
- antiAffinityGroupMemberList({ antiAffinityGroup, project })
- )
- const { data: instances } = usePrefetchedQuery(instanceList({ project }))
- // Construct a list of all instances not currently in this anti-affinity group.
- const availableInstances = instances.items.filter(
- (instance) => !members.items.some(({ value }) => value.name === instance.name)
- )
-
const form = useForm({
defaultValues: {
antiAffinityGroupMember: '',
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 922f30d6f2..6fc2afd762 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -87,6 +87,12 @@ export default function AntiAffinityPage() {
)
const membersCount = members.items.length
+ const { data: instances } = usePrefetchedQuery(instanceList({ project }))
+ // Construct a list of all instances not currently in this anti-affinity group.
+ const availableInstances = instances.items
+ .filter((instance) => !members.items.some(({ value }) => value.name === instance.name))
+ .sort((a, b) => a.name.localeCompare(b.name))
+
const { mutateAsync: removeMember } = useApiMutation(
'antiAffinityGroupMemberInstanceDelete',
{
@@ -176,7 +182,10 @@ export default function AntiAffinityPage() {
title="Members"
description="Instances in this anti-affinity group"
>
- setIsModalOpen(true)}>
+ setIsModalOpen(true)}
+ disabled={!availableInstances.length}
+ >
Add instance to group
@@ -185,6 +194,7 @@ export default function AntiAffinityPage() {
From e6d996b9d3ffc45a744ecc473fd6ae327cd2fa33 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 21:58:49 -0700
Subject: [PATCH 21/56] Use existing types for forms
---
app/forms/anti-affinity-group-create.tsx | 24 +++++------------
app/forms/anti-affinity-group-edit.tsx | 28 +++++++++-----------
app/forms/anti-affinity-group-member-add.tsx | 15 +++++++----
3 files changed, 28 insertions(+), 39 deletions(-)
diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx
index 1b45352f75..4b8e6c9287 100644
--- a/app/forms/anti-affinity-group-create.tsx
+++ b/app/forms/anti-affinity-group-create.tsx
@@ -12,7 +12,7 @@ import {
queryClient,
useApiMutation,
usePrefetchedQuery,
- type AffinityPolicy,
+ type AntiAffinityGroupCreate,
} from '@oxide/api'
import { DescriptionField } from '~/components/form/fields/DescriptionField'
@@ -29,13 +29,6 @@ import { affinityGroupList, antiAffinityGroupList } from './affinity-util'
export const handle = titleCrumb('New anti-affinity group')
-type AntiAffinityGroupFormValues = {
- name: string
- description: string
- policy: AffinityPolicy
- affinityGroupMembers: string[]
-}
-
export async function clientLoader({ params }: LoaderFunctionArgs) {
const { project } = getProjectSelector(params)
await Promise.all([
@@ -62,13 +55,13 @@ export default function CreateAntiAffintyGroupForm() {
const {
data: { items: existingAntiAffinityGroups },
} = usePrefetchedQuery(antiAffinityGroupList({ project }))
- const defaultValues: AntiAffinityGroupFormValues = {
+ const defaultValues = {
name: '',
description: '',
- policy: 'allow',
- affinityGroupMembers: [],
+ failureDomain: 'sled' as const,
+ policy: 'allow' as const,
}
- const form = useForm({ defaultValues })
+ const form = useForm({ defaultValues })
const control = form.control
return (
@@ -81,12 +74,7 @@ export default function CreateAntiAffintyGroupForm() {
onSubmit={(values) => {
createAntiAffinityGroup.mutate({
query: { project },
- body: {
- name: values.name,
- description: values.description,
- policy: values.policy,
- failureDomain: 'sled',
- },
+ body: { ...values },
})
}}
loading={createAntiAffinityGroup.isPending}
diff --git a/app/forms/anti-affinity-group-edit.tsx b/app/forms/anti-affinity-group-edit.tsx
index 08a9f91784..c9f65fe1a2 100644
--- a/app/forms/anti-affinity-group-edit.tsx
+++ b/app/forms/anti-affinity-group-edit.tsx
@@ -61,18 +61,17 @@ export default function EditAntiAffintyGroupForm() {
},
})
- const {
- data: { items: existingAntiAffinityGroups },
- } = usePrefetchedQuery(antiAffinityGroupList({ project }))
+ const { data: existingAntiAffinityGroups } = usePrefetchedQuery(
+ antiAffinityGroupList({ project })
+ )
+
const { data: antiAffinityGroupData } = usePrefetchedQuery(
antiAffinityGroupView({ project, antiAffinityGroup })
)
- const defaultValues: AntiAffinityGroupUpdate = {
- name: antiAffinityGroupData.name,
- description: antiAffinityGroupData.description,
- }
- const form = useForm({ defaultValues })
- const control = form.control
+
+ const form = useForm({
+ defaultValues: { ...antiAffinityGroupData },
+ })
return (
{
- if (existingAntiAffinityGroups.find((g) => g.name === name)) {
+ if (existingAntiAffinityGroups.items.find((g) => g.name === name)) {
return 'Name taken. To update an existing group, edit it directly.'
}
}}
/>
-
+
)
}
diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx
index adc9d8c34a..0a82018671 100644
--- a/app/forms/anti-affinity-group-member-add.tsx
+++ b/app/forms/anti-affinity-group-member-add.tsx
@@ -8,7 +8,12 @@
import { useForm } from 'react-hook-form'
-import { queryClient, useApiMutation, type Instance } from '~/api'
+import {
+ queryClient,
+ useApiMutation,
+ type AntiAffinityGroupMemberInstanceAddPathParams,
+ type Instance,
+} from '~/api'
import { ComboboxField } from '~/components/form/fields/ComboboxField'
import { HL } from '~/components/HL'
import { useAntiAffinityGroupSelector } from '~/hooks/use-params'
@@ -27,9 +32,9 @@ export function AddAntiAffinityGroupMemberForm({
}) {
const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
- const form = useForm({
+ const form = useForm({
defaultValues: {
- antiAffinityGroupMember: '',
+ instance: '',
},
})
@@ -58,7 +63,7 @@ export function AddAntiAffinityGroupMemberForm({
Date: Wed, 2 Apr 2025 22:05:07 -0700
Subject: [PATCH 22/56] export function as default
---
app/forms/anti-affinity-group-member-add.tsx | 2 +-
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx
index 0a82018671..ecaeef1605 100644
--- a/app/forms/anti-affinity-group-member-add.tsx
+++ b/app/forms/anti-affinity-group-member-add.tsx
@@ -21,7 +21,7 @@ import { addToast } from '~/stores/toast'
import { toComboboxItems } from '~/ui/lib/Combobox'
import { Modal } from '~/ui/lib/Modal'
-export function AddAntiAffinityGroupMemberForm({
+export default function AddAntiAffinityGroupMemberForm({
availableInstances,
isModalOpen,
setIsModalOpen,
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 6fc2afd762..107ea84769 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -25,7 +25,7 @@ import {
antiAffinityGroupView,
instanceList,
} from '~/forms/affinity-util'
-import { AddAntiAffinityGroupMemberForm } from '~/forms/anti-affinity-group-member-add'
+import AddAntiAffinityGroupMemberForm from '~/forms/anti-affinity-group-member-add'
import { makeCrumb } from '~/hooks/use-crumbs'
import {
getAntiAffinityGroupSelector,
From 829344b8ce8624c2223f9e9a6bb5e3f13528ec3a Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 22:22:00 -0700
Subject: [PATCH 23/56] refactor idCell
---
app/table/columns/common.tsx | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/app/table/columns/common.tsx b/app/table/columns/common.tsx
index e20059eb21..eb8c2ac451 100644
--- a/app/table/columns/common.tsx
+++ b/app/table/columns/common.tsx
@@ -11,7 +11,6 @@ import { filesize } from 'filesize'
import type { InstanceState } from '~/api'
import { InstanceStateBadge } from '~/components/StateBadge'
import { DescriptionCell } from '~/table/cells/DescriptionCell'
-import { CopyToClipboard } from '~/ui/lib/CopyToClipboard'
import { DateTime } from '~/ui/lib/DateTime'
import { Truncate } from '~/ui/lib/Truncate'
@@ -24,12 +23,7 @@ function dateCell(info: Info) {
}
function idCell(info: Info) {
- return (
-
-
-
-
- )
+ return
}
function instanceStateCell(info: Info) {
From a54352dbf957f93c114aba7f0cb7d9c827e02eb7 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 22:23:44 -0700
Subject: [PATCH 24/56] Update mock-api/msw/handlers.ts
Co-authored-by: David Crespo
---
mock-api/msw/handlers.ts | 3 ---
1 file changed, 3 deletions(-)
diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts
index c025467d0c..2e55a4f8be 100644
--- a/mock-api/msw/handlers.ts
+++ b/mock-api/msw/handlers.ts
@@ -1668,9 +1668,6 @@ export const handlers = makeHandlers({
antiAffinityGroup.name = body.name
}
updateDesc(antiAffinityGroup, body)
- if (body.description) {
- antiAffinityGroup.description = body.description
- }
return antiAffinityGroup
},
antiAffinityGroupList: ({ query }) => {
From 3d2ae30f37e25a3ca72d2dec439d464b92ff5488 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 22:29:34 -0700
Subject: [PATCH 25/56] don't reuse AffinityPageHeader
---
app/pages/project/affinity/AffinityPage.tsx | 29 ++++---------------
.../affinity/AntiAffinityGroupPage.tsx | 19 ++++++++++--
2 files changed, 22 insertions(+), 26 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index deaf08b03a..4ef27dcfb0 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -21,7 +21,6 @@ import {
import { Affinity24Icon } from '@oxide/design-system/icons/react'
import { HL } from '~/components/HL'
-import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import { antiAffinityGroupList, antiAffinityGroupMemberList } from '~/forms/affinity-util'
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { confirmAction } from '~/stores/confirm-action'
@@ -33,7 +32,6 @@ import { Columns } from '~/table/columns/common'
import { Table } from '~/table/Table'
import { Badge } from '~/ui/lib/Badge'
import { CreateLink } from '~/ui/lib/CreateButton'
-import * as DropdownMenu from '~/ui/lib/DropdownMenu'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { Slash } from '~/ui/lib/Slash'
@@ -58,26 +56,6 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
const colHelper = createColumnHelper()
-export const AffinityPageHeader = ({ name = 'Affinity' }: { name?: string }) => {
- const { project } = useProjectSelector()
- return (
-
- }>{name}
-
- {/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
- {name !== 'Affinity' && (
-
-
- Edit
-
-
- )}
-
-
- )
-}
type AffinityGroupPolicyBadgeProps = { policy: AffinityPolicy; className?: string }
const AffinityGroupPolicyBadge = ({ policy, className }: AffinityGroupPolicyBadgeProps) => (
-
+
+ }>Affinity
+
+ {/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
+
+
New anti-affinity group
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 107ea84769..44f6eb10ed 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -19,6 +19,7 @@ import {
type AntiAffinityGroupMember,
} from '~/api'
import { HL } from '~/components/HL'
+import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import {
affinityGroupList,
antiAffinityGroupMemberList,
@@ -41,13 +42,13 @@ import { Badge } from '~/ui/lib/Badge'
import { CardBlock } from '~/ui/lib/CardBlock'
import { CreateButton } from '~/ui/lib/CreateButton'
import { Divider } from '~/ui/lib/Divider'
+import * as DropdownMenu from '~/ui/lib/DropdownMenu'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
+import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { TableEmptyBox } from '~/ui/lib/Table'
import { pb } from '~/util/path-builder'
-import { AffinityPageHeader } from './AffinityPage'
-
export const handle = makeCrumb(
(p) => p.antiAffinityGroup!,
(p) => pb.antiAffinityGroup(getAntiAffinityGroupSelector(p))
@@ -163,7 +164,19 @@ export default function AntiAffinityPage() {
return (
<>
-
+
+ }>{name}
+
+ {/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
+
+
+ Edit
+
+
+
+
anti-affinity
From 72efb005520135077e9bc390e1c82cb3fa090adc Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 22:35:49 -0700
Subject: [PATCH 26/56] Shorter button copy; new page header
---
app/pages/project/affinity/AffinityPage.tsx | 4 ++--
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 4ef27dcfb0..187636b309 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -159,13 +159,13 @@ export default function AffinityPage() {
return (
<>
- }>Affinity
+ }>Anti-Affinity Groups
{/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
- New anti-affinity group
+ New group
{antiAffinityGroups.length ? (
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 44f6eb10ed..f16025e9db 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -199,7 +199,7 @@ export default function AntiAffinityPage() {
onClick={() => setIsModalOpen(true)}
disabled={!availableInstances.length}
>
- Add instance to group
+ Add instance
From 5e341961a03c96a00522a0f84d485dc7bb33b040 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 22:44:03 -0700
Subject: [PATCH 27/56] Don't include sorting; already present in actual data
---
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index f16025e9db..219f1c2391 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -90,9 +90,9 @@ export default function AntiAffinityPage() {
const { data: instances } = usePrefetchedQuery(instanceList({ project }))
// Construct a list of all instances not currently in this anti-affinity group.
- const availableInstances = instances.items
- .filter((instance) => !members.items.some(({ value }) => value.name === instance.name))
- .sort((a, b) => a.name.localeCompare(b.name))
+ const availableInstances = instances.items.filter(
+ (instance) => !members.items.some(({ value }) => value.name === instance.name)
+ )
const { mutateAsync: removeMember } = useApiMutation(
'antiAffinityGroupMemberInstanceDelete',
From 3ae6cf9df7dc7d37ec3a72ff3df0cd495b7d7ec3 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 22:55:53 -0700
Subject: [PATCH 28/56] Try 'anti-affinity' as header / nav link
---
app/layouts/ProjectLayoutBase.tsx | 4 ++--
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 3 +++
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/app/layouts/ProjectLayoutBase.tsx b/app/layouts/ProjectLayoutBase.tsx
index cbb5fb9d35..17b7cfda7f 100644
--- a/app/layouts/ProjectLayoutBase.tsx
+++ b/app/layouts/ProjectLayoutBase.tsx
@@ -69,7 +69,7 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
{ value: 'Images', path: pb.projectImages(projectSelector) },
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
- { value: 'Affinity', path: pb.affinity(projectSelector) },
+ { value: 'Anti-affinity', path: pb.affinity(projectSelector) },
{ value: 'Access', path: pb.projectAccess(projectSelector) },
]
// filter out the entry for the path we're currently on
@@ -115,7 +115,7 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
Floating IPs
- Affinity
+ Anti-affinity
Access
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 219f1c2391..b3b1acd067 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -198,6 +198,9 @@ export default function AntiAffinityPage() {
setIsModalOpen(true)}
disabled={!availableInstances.length}
+ disabledReason={
+ availableInstances.length ? undefined : 'No instances are available to add'
+ }
>
Add instance
From 666230d258a66cffd4fd263e15afe11d37bb1c76 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 23:04:52 -0700
Subject: [PATCH 29/56] Clean up copy button a bit
---
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 6 ------
app/table/columns/common.tsx | 12 ++++++++++--
2 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index b3b1acd067..7059216755 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -109,12 +109,6 @@ export default function AntiAffinityPage() {
const makeActions = useCallback(
(antiAffinityGroupMember: AntiAffinityGroupMember): MenuAction[] => [
- {
- label: 'Copy instance ID',
- onActivate() {
- navigator.clipboard.writeText(antiAffinityGroupMember.value.id)
- },
- },
{
label: 'Remove from group',
onActivate() {
diff --git a/app/table/columns/common.tsx b/app/table/columns/common.tsx
index eb8c2ac451..21c368f3cb 100644
--- a/app/table/columns/common.tsx
+++ b/app/table/columns/common.tsx
@@ -11,8 +11,8 @@ import { filesize } from 'filesize'
import type { InstanceState } from '~/api'
import { InstanceStateBadge } from '~/components/StateBadge'
import { DescriptionCell } from '~/table/cells/DescriptionCell'
+import { CopyToClipboard } from '~/ui/lib/CopyToClipboard'
import { DateTime } from '~/ui/lib/DateTime'
-import { Truncate } from '~/ui/lib/Truncate'
// the full type of the info arg is CellContext from RT, but in these
// cells we only care about the return value of getValue
@@ -23,7 +23,15 @@ function dateCell(info: Info) {
}
function idCell(info: Info) {
- return
+ const text = info.getValue()
+ return (
+
+ )
}
function instanceStateCell(info: Info) {
From 2a996271b9692c6c2bd1a79dabf71fb1a49e2dc0 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Wed, 2 Apr 2025 23:42:50 -0700
Subject: [PATCH 30/56] More clever disabledReason; needs max member
verification
---
.../project/affinity/AntiAffinityGroupPage.tsx | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 7059216755..dfdc076861 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -156,6 +156,20 @@ export default function AntiAffinityPage() {
getCoreRowModel: getCoreRowModel(),
})
+ const disabledReason = () => {
+ // TODO: Verify maximum number of members
+ if (membersCount >= 16) {
+ return 'Maximum number of members reached'
+ }
+ if (!instances.items.length) {
+ return 'No instances available'
+ }
+ if (!availableInstances.length) {
+ return 'All instances are already in this group'
+ }
+ return undefined
+ }
+
return (
<>
@@ -192,9 +206,7 @@ export default function AntiAffinityPage() {
setIsModalOpen(true)}
disabled={!availableInstances.length}
- disabledReason={
- availableInstances.length ? undefined : 'No instances are available to add'
- }
+ disabledReason={disabledReason()}
>
Add instance
From 22978df265fba8c6de221c224791ee89129790d7 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Thu, 3 Apr 2025 10:27:54 -0500
Subject: [PATCH 31/56] use regular link for group edit row action
---
app/pages/project/affinity/AffinityPage.tsx | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 187636b309..1d136e7707 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -9,7 +9,7 @@
import { useQuery } from '@tanstack/react-query'
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useCallback } from 'react'
-import { Link, Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
+import { Link, Outlet, type LoaderFunctionArgs } from 'react-router'
import {
queryClient,
@@ -89,7 +89,6 @@ export default function AffinityPage() {
const {
data: { items: antiAffinityGroups },
} = usePrefetchedQuery(antiAffinityGroupList({ project }))
- const navigate = useNavigate()
const { mutateAsync: deleteGroup } = useApiMutation('antiAffinityGroupDelete', {
onSuccess(_data, variables) {
@@ -106,11 +105,10 @@ export default function AffinityPage() {
(antiAffinityGroup: AntiAffinityGroup): MenuAction[] => [
{
label: 'Edit',
- onActivate() {
- navigate(
- pb.antiAffinityGroupEdit({ project, antiAffinityGroup: antiAffinityGroup.name })
- )
- },
+ to: pb.antiAffinityGroupEdit({
+ project,
+ antiAffinityGroup: antiAffinityGroup.name,
+ }),
},
{
label: 'Delete',
@@ -134,7 +132,7 @@ export default function AffinityPage() {
},
},
],
- [project, deleteGroup, navigate]
+ [project, deleteGroup]
)
const columns = useColsWithActions(
From cc9875a2a8272037447054a8e81619c3e2fd94d4 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Thu, 3 Apr 2025 10:34:54 -0500
Subject: [PATCH 32/56] put it back to Affinity title
---
app/layouts/ProjectLayoutBase.tsx | 8 ++++----
app/pages/project/affinity/AffinityPage.tsx | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/layouts/ProjectLayoutBase.tsx b/app/layouts/ProjectLayoutBase.tsx
index 17b7cfda7f..8266c34a37 100644
--- a/app/layouts/ProjectLayoutBase.tsx
+++ b/app/layouts/ProjectLayoutBase.tsx
@@ -69,7 +69,7 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
{ value: 'Images', path: pb.projectImages(projectSelector) },
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
- { value: 'Anti-affinity', path: pb.affinity(projectSelector) },
+ { value: 'Affinity', path: pb.affinity(projectSelector) },
{ value: 'Access', path: pb.projectAccess(projectSelector) },
]
// filter out the entry for the path we're currently on
@@ -106,7 +106,7 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
Snapshots
- Images
+ Images
VPCs
@@ -115,10 +115,10 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
Floating IPs
- Anti-affinity
+ Affinity
- Access
+ Access
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 1d136e7707..f08f90fd04 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -157,7 +157,7 @@ export default function AffinityPage() {
return (
<>
- }>Anti-Affinity Groups
+ }>Affinity
{/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
From 00cc0292631c185029a3d56eb3813c4d3eef6f53 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Thu, 3 Apr 2025 11:06:01 -0500
Subject: [PATCH 33/56] draft docs popover
---
app/layouts/ProjectLayoutBase.tsx | 4 ++--
app/pages/project/affinity/AffinityPage.tsx | 13 +++++++++----
app/util/links.ts | 7 +++++++
3 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/app/layouts/ProjectLayoutBase.tsx b/app/layouts/ProjectLayoutBase.tsx
index 8266c34a37..b404cc64fa 100644
--- a/app/layouts/ProjectLayoutBase.tsx
+++ b/app/layouts/ProjectLayoutBase.tsx
@@ -115,10 +115,10 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
Floating IPs
- Affinity
+ Affinity
- Access
+ Access
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index f08f90fd04..aeeab7f6b2 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -18,8 +18,9 @@ import {
type AffinityPolicy,
type AntiAffinityGroup,
} from '@oxide/api'
-import { Affinity24Icon } from '@oxide/design-system/icons/react'
+import { Affinity16Icon, Affinity24Icon } from '@oxide/design-system/icons/react'
+import { DocsPopover } from '~/components/DocsPopover'
import { HL } from '~/components/HL'
import { antiAffinityGroupList, antiAffinityGroupMemberList } from '~/forms/affinity-util'
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
@@ -37,6 +38,7 @@ import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { Slash } from '~/ui/lib/Slash'
import { TableActions, TableEmptyBox } from '~/ui/lib/Table'
import { intersperse } from '~/util/array'
+import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'
export async function clientLoader({ params }: LoaderFunctionArgs) {
@@ -158,9 +160,12 @@ export default function AffinityPage() {
<>
}>Affinity
-
- {/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
-
+ }
+ summary="Instances in an anti-affinity group will be placed on different sleds when they start. The policy attribute controls whether this is a hard or soft constraint."
+ links={[docLinks.affinity]}
+ />{' '}
New group
diff --git a/app/util/links.ts b/app/util/links.ts
index 3d780d18cc..1113b14888 100644
--- a/app/util/links.ts
+++ b/app/util/links.ts
@@ -10,6 +10,9 @@ const remoteAccess = 'https://docs.oxide.computer/guides/remote-access'
export const links = {
accessDocs: 'https://docs.oxide.computer/guides/configuring-access',
+ // TODO: make sure this is right before merging
+ affinityDocs:
+ 'https://docs.oxide.computer/guides/deploying-workloads#_anti_affinity_groups',
cloudInitFormat: 'https://cloudinit.readthedocs.io/en/latest/explanation/format.html',
cloudInitExamples: 'https://cloudinit.readthedocs.io/en/latest/reference/examples.html',
disksDocs: 'https://docs.oxide.computer/guides/managing-disks-and-snapshots',
@@ -67,6 +70,10 @@ export const docLinks = {
href: links.accessDocs,
linkText: 'Access Control',
},
+ affinity: {
+ href: links.affinityDocs,
+ linkText: 'Anti-Affinity Groups',
+ },
disks: {
href: links.disksDocs,
linkText: 'Disks and Snapshots',
From 5e5b3ee752346aff2f9246306be7fb4162f1e621 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 3 Apr 2025 09:22:08 -0700
Subject: [PATCH 34/56] A few more refactors / PR comments
---
app/forms/anti-affinity-group-create.tsx | 24 +++++++------------
app/pages/project/affinity/AffinityPage.tsx | 1 -
.../affinity/AntiAffinityGroupPage.tsx | 7 +++---
3 files changed, 12 insertions(+), 20 deletions(-)
diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx
index 4b8e6c9287..342158ea33 100644
--- a/app/forms/anti-affinity-group-create.tsx
+++ b/app/forms/anti-affinity-group-create.tsx
@@ -5,15 +5,11 @@
*
* Copyright Oxide Computer Company
*/
+import { useQuery } from '@tanstack/react-query'
import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router'
-import {
- queryClient,
- useApiMutation,
- usePrefetchedQuery,
- type AntiAffinityGroupCreate,
-} from '@oxide/api'
+import { queryClient, useApiMutation, type AntiAffinityGroupCreate } from '@oxide/api'
import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
@@ -25,16 +21,13 @@ import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'
-import { affinityGroupList, antiAffinityGroupList } from './affinity-util'
+import { antiAffinityGroupList } from './affinity-util'
export const handle = titleCrumb('New anti-affinity group')
-export async function clientLoader({ params }: LoaderFunctionArgs) {
+export function clientLoader({ params }: LoaderFunctionArgs) {
const { project } = getProjectSelector(params)
- await Promise.all([
- queryClient.prefetchQuery(antiAffinityGroupList({ project })),
- queryClient.prefetchQuery(affinityGroupList({ project })),
- ])
+ queryClient.prefetchQuery(antiAffinityGroupList({ project }))
return null
}
@@ -52,9 +45,7 @@ export default function CreateAntiAffintyGroupForm() {
},
})
- const {
- data: { items: existingAntiAffinityGroups },
- } = usePrefetchedQuery(antiAffinityGroupList({ project }))
+ const { data: existingAntiAffinityGroups } = useQuery(antiAffinityGroupList({ project }))
const defaultValues = {
name: '',
description: '',
@@ -78,6 +69,7 @@ export default function CreateAntiAffintyGroupForm() {
})
}}
loading={createAntiAffinityGroup.isPending}
+ submitDisabled={existingAntiAffinityGroups === undefined ? 'Loading …' : undefined}
submitError={createAntiAffinityGroup.error}
submitLabel="Add group"
>
@@ -85,7 +77,7 @@ export default function CreateAntiAffintyGroupForm() {
name="name"
control={control}
validate={(name) => {
- if (existingAntiAffinityGroups.find((g) => g.name === name)) {
+ if (existingAntiAffinityGroups?.items.find((g) => g.name === name)) {
return 'Name taken. To update an existing group, edit it directly.'
}
}}
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index aeeab7f6b2..0f2fcc3ed1 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -82,7 +82,6 @@ const staticCols = [
header: 'members',
cell: (info) => ,
}),
- colHelper.accessor('id', Columns.id),
colHelper.accessor('timeCreated', Columns.timeCreated),
]
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index dfdc076861..1be2be4892 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -39,8 +39,8 @@ import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
import { Columns } from '~/table/columns/common'
import { Table } from '~/table/Table'
import { Badge } from '~/ui/lib/Badge'
+import { Button } from '~/ui/lib/Button'
import { CardBlock } from '~/ui/lib/CardBlock'
-import { CreateButton } from '~/ui/lib/CreateButton'
import { Divider } from '~/ui/lib/Divider'
import * as DropdownMenu from '~/ui/lib/DropdownMenu'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
@@ -203,13 +203,14 @@ export default function AntiAffinityPage() {
title="Members"
description="Instances in this anti-affinity group"
>
- setIsModalOpen(true)}
disabled={!availableInstances.length}
disabledReason={disabledReason()}
>
Add instance
-
+
{membersCount ? : }
From 3fe3fc2438e24f556ff4fcd5fbb2661af34efec7 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 3 Apr 2025 09:36:05 -0700
Subject: [PATCH 35/56] routing fix
---
app/routes.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/routes.tsx b/app/routes.tsx
index 6dd1b5397e..f5b01da296 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -506,7 +506,7 @@ export const routes = createRoutesFromElements(
>
import('./forms/anti-affinity-group-create').then(convert)}
+ lazy={() => import('./forms/anti-affinity-group-create')}
/>
From 8bcb26bfef3a91f2a8be1867c1b0fe23f78735e4 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 3 Apr 2025 10:18:10 -0700
Subject: [PATCH 36/56] reintroduce convert in routes for group create
---
app/forms/anti-affinity-group-create.tsx | 5 +++--
app/routes.tsx | 2 +-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx
index 342158ea33..f8d2250317 100644
--- a/app/forms/anti-affinity-group-create.tsx
+++ b/app/forms/anti-affinity-group-create.tsx
@@ -25,10 +25,11 @@ import { antiAffinityGroupList } from './affinity-util'
export const handle = titleCrumb('New anti-affinity group')
-export function clientLoader({ params }: LoaderFunctionArgs) {
+export async function clientLoader({ params }: LoaderFunctionArgs) {
const { project } = getProjectSelector(params)
queryClient.prefetchQuery(antiAffinityGroupList({ project }))
- return null
+ // the async demands a promise, so this just returns a promise that resolves to null
+ return Promise.resolve(null)
}
export default function CreateAntiAffintyGroupForm() {
diff --git a/app/routes.tsx b/app/routes.tsx
index f5b01da296..6dd1b5397e 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -506,7 +506,7 @@ export const routes = createRoutesFromElements(
>
import('./forms/anti-affinity-group-create')}
+ lazy={() => import('./forms/anti-affinity-group-create').then(convert)}
/>
From 36884e5bf4bac531e972fcb82606628424f1f333 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 3 Apr 2025 12:43:27 -0700
Subject: [PATCH 37/56] update max members value
---
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 1be2be4892..1d9637ebbb 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -157,8 +157,8 @@ export default function AntiAffinityPage() {
})
const disabledReason = () => {
- // TODO: Verify maximum number of members
- if (membersCount >= 16) {
+ // https://github.com/oxidecomputer/omicron/blob/main/nexus/db-queries/src/db/datastore/affinity.rs#L66
+ if (membersCount >= 32) {
return 'Maximum number of members reached'
}
if (!instances.items.length) {
From 8e5e31ca5ce3ca6bf0dd705d8f450ca837ef751a Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 3 Apr 2025 13:56:44 -0700
Subject: [PATCH 38/56] Link to specific commit for line reference stability
---
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 1d9637ebbb..1e8e5e33cb 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -157,7 +157,7 @@ export default function AntiAffinityPage() {
})
const disabledReason = () => {
- // https://github.com/oxidecomputer/omicron/blob/main/nexus/db-queries/src/db/datastore/affinity.rs#L66
+ // https://github.com/oxidecomputer/omicron/blob/77c4136a767d4d1365c3ad715a335da9035415db/nexus/db-queries/src/db/datastore/affinity.rs#L66
if (membersCount >= 32) {
return 'Maximum number of members reached'
}
From e27d8546c90c024a30f006f0d7287afbdefe7232 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Thu, 3 Apr 2025 17:38:53 -0700
Subject: [PATCH 39/56] Add e2e tests
---
app/forms/anti-affinity-group-edit.tsx | 2 +-
.../affinity/AntiAffinityGroupPage.tsx | 4 +-
test/e2e/anti-affinity.e2e.ts | 128 ++++++++++++++++++
3 files changed, 130 insertions(+), 4 deletions(-)
create mode 100644 test/e2e/anti-affinity.e2e.ts
diff --git a/app/forms/anti-affinity-group-edit.tsx b/app/forms/anti-affinity-group-edit.tsx
index c9f65fe1a2..415aff532b 100644
--- a/app/forms/anti-affinity-group-edit.tsx
+++ b/app/forms/anti-affinity-group-edit.tsx
@@ -78,7 +78,7 @@ export default function EditAntiAffintyGroupForm() {
form={form}
formType="create"
resourceName="rule"
- title="Add anti-affinity group"
+ title="Edit anti-affinity group"
onDismiss={onDismiss}
onSubmit={(values) => {
editAntiAffinityGroup.mutate({
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 1e8e5e33cb..b636505978 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -21,7 +21,6 @@ import {
import { HL } from '~/components/HL'
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import {
- affinityGroupList,
antiAffinityGroupMemberList,
antiAffinityGroupView,
instanceList,
@@ -62,7 +61,6 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
queryClient.prefetchQuery(antiAffinityGroupView({ antiAffinityGroup, project })),
queryClient.prefetchQuery(antiAffinityGroupMemberList({ antiAffinityGroup, project })),
queryClient.prefetchQuery(instanceList({ project })),
- queryClient.prefetchQuery(affinityGroupList({ project })),
])
return null
}
@@ -141,7 +139,7 @@ export default function AntiAffinityPage() {
const columns = useColsWithActions(
[
colHelper.accessor('value.name', {
- header: 'Name',
+ header: 'name',
cell: makeLinkCell((instance) => pb.instance({ project, instance })),
}),
colHelper.accessor('value.runState', Columns.instanceState),
diff --git a/test/e2e/anti-affinity.e2e.ts b/test/e2e/anti-affinity.e2e.ts
new file mode 100644
index 0000000000..42d97c9da1
--- /dev/null
+++ b/test/e2e/anti-affinity.e2e.ts
@@ -0,0 +1,128 @@
+/*
+ * 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 { expect, test } from '@playwright/test'
+
+import { clickRowAction, expectRowVisible } from './utils'
+
+test('can nav to Affinity from /', async ({ page }) => {
+ await page.goto('/')
+ await page.getByRole('table').getByRole('link', { name: 'mock-project' }).click()
+ await page.getByRole('link', { name: 'Affinity' }).click()
+
+ await expectRowVisible(page.getByRole('table'), {
+ name: 'romulus-remus',
+ type: 'anti-affinity',
+ policy: 'fail',
+ members: 'db1/db2',
+ })
+
+ // click the anti-affinity group name cell to go to the view page
+ await page.getByRole('link', { name: 'romulus-remus' }).click()
+
+ await expect(page.getByRole('heading', { name: 'romulus-remus' })).toBeVisible()
+ await expect(page).toHaveURL('/projects/mock-project/affinity/romulus-remus')
+ await expect(page).toHaveTitle(
+ 'romulus-remus / Affinity / mock-project / Projects / Oxide Console'
+ )
+})
+
+test('can add a new anti-affinity group', async ({ page }) => {
+ await page.goto('/projects/mock-project/affinity')
+ await page.getByRole('link', { name: 'New group' }).click()
+ await expect(page).toHaveURL('/projects/mock-project/affinity-new')
+ await expect(page.getByRole('heading', { name: 'Add anti-affinity group' })).toBeVisible()
+
+ // fill out the form
+ await page.getByLabel('Name').fill('new-anti-affinity-group')
+ await page
+ .getByRole('textbox', { name: 'Description' })
+ .fill('this is a new anti-affinity group')
+ await page.getByRole('radio', { name: 'Fail' }).click()
+
+ // submit the form
+ await page.getByRole('button', { name: 'Add group' }).click()
+
+ // check that we are on the view page for the new anti-affinity group
+ await expect(page).toHaveURL('/projects/mock-project/affinity/new-anti-affinity-group')
+
+ // add a member to the new anti-affinity group
+ await page.getByRole('button', { name: 'Add instance' }).click()
+ await expect(page.getByRole('heading', { name: 'Add instance to group' })).toBeVisible()
+ await page.getByRole('combobox', { name: 'Instance' }).fill('db1')
+ await page.getByRole('option', { name: 'db1' }).click()
+ await page.getByRole('button', { name: 'Add to group' }).click()
+
+ const cell = page.getByRole('cell', { name: 'db1' })
+ await expect(cell).toBeVisible()
+
+ // remove the instance from the group
+ await clickRowAction(page, 'db1', 'Remove from group')
+ await page.getByRole('button', { name: 'Confirm' }).click()
+ await expect(cell).toBeHidden()
+
+ // expect empty message
+ await expect(page.getByText('No anti-affinity group members')).toBeVisible()
+})
+
+// edit an anti-affinity group from the view page
+test('can edit an anti-affinity group', async ({ page }) => {
+ await page.goto('/projects/mock-project/affinity/romulus-remus')
+ await page.getByRole('button', { name: 'Anti-affinity group actions' }).click()
+ await page.getByRole('menuitem', { name: 'Edit' }).click()
+
+ // can see Add anti-affinity group header
+ await expect(
+ page.getByRole('heading', { name: 'Edit anti-affinity group' })
+ ).toBeVisible()
+
+ // change the name to romulus-remus-2
+ await page.getByLabel('Name').fill('romulus-remus-2')
+ await page.getByRole('button', { name: 'Edit group' }).click()
+ await expect(page).toHaveURL('/projects/mock-project/affinity/romulus-remus-2')
+ await expect(page.getByRole('heading', { name: 'romulus-remus-2' })).toBeVisible()
+})
+
+// delete an anti-affinity group
+test('can delete an anti-affinity group', async ({ page }) => {
+ await page.goto('/projects/mock-project/affinity')
+ await clickRowAction(page, 'set-osiris', 'Delete')
+
+ await expect(
+ page.getByRole('heading', { name: 'Delete anti-affinity group' })
+ ).toBeVisible()
+
+ // confirm the deletion
+ await page.getByRole('button', { name: 'Confirm' }).click()
+
+ // check that we are back on the affinity page
+ await expect(page).toHaveURL('/projects/mock-project/affinity')
+
+ // can't see set-osiris in the table
+ await expect(page.getByRole('table').getByText('set-osiris')).toBeHidden()
+
+ // can create a new anti-affinity group with the same name
+ await page.getByRole('link', { name: 'New group' }).click()
+ await expect(page).toHaveURL('/projects/mock-project/affinity-new')
+ await expect(page.getByRole('heading', { name: 'Add anti-affinity group' })).toBeVisible()
+ await page.getByLabel('Name').fill('set-osiris')
+ await page
+ .getByRole('textbox', { name: 'Description' })
+ .fill('this is a new anti-affinity group')
+ await page.getByRole('radio', { name: 'Fail' }).click()
+ await page.getByRole('button', { name: 'Add group' }).click()
+
+ await expect(page).toHaveURL('/projects/mock-project/affinity/set-osiris')
+ await expect(page.getByRole('heading', { name: 'set-osiris' })).toBeVisible()
+
+ // click on Affinity in crumbs
+ await page.getByRole('link', { name: 'Affinity' }).first().click()
+ await expect(page).toHaveURL('/projects/mock-project/affinity')
+ // check that we can see the new anti-affinity group in the table
+ await expect(page.getByRole('table').getByText('set-osiris')).toBeVisible()
+})
From ff31223bc3ef4e3281ff88e66d23d857d8552b82 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Fri, 4 Apr 2025 09:53:31 -0700
Subject: [PATCH 40/56] Refresh of Affinity Groups table columns; use count in
place of instance names
---
app/pages/project/affinity/AffinityPage.tsx | 28 +++------------------
1 file changed, 4 insertions(+), 24 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 0f2fcc3ed1..9acbba69cc 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -6,10 +6,9 @@
* Copyright Oxide Computer Company
*/
-import { useQuery } from '@tanstack/react-query'
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useCallback } from 'react'
-import { Link, Outlet, type LoaderFunctionArgs } from 'react-router'
+import { Outlet, type LoaderFunctionArgs } from 'react-router'
import {
queryClient,
@@ -35,9 +34,7 @@ import { Badge } from '~/ui/lib/Badge'
import { CreateLink } from '~/ui/lib/CreateButton'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
-import { Slash } from '~/ui/lib/Slash'
import { TableActions, TableEmptyBox } from '~/ui/lib/Table'
-import { intersperse } from '~/util/array'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'
@@ -70,7 +67,6 @@ const AffinityGroupPolicyBadge = ({ policy, className }: AffinityGroupPolicyBadg
)
const staticCols = [
- colHelper.accessor('description', Columns.description),
colHelper.accessor(() => {}, {
header: 'type',
cell: () => anti-affinity ,
@@ -82,6 +78,7 @@ const staticCols = [
header: 'members',
cell: (info) => ,
}),
+ colHelper.accessor('description', Columns.description),
colHelper.accessor('timeCreated', Columns.timeCreated),
]
@@ -189,33 +186,16 @@ export const AntiAffinityGroupEmptyState = () => (
)
-// TODO: Use the prefetched query
export const AffinityGroupMembersCell = ({
antiAffinityGroup,
}: {
antiAffinityGroup: string
}) => {
const { project } = useProjectSelector()
- const { data: members } = useQuery(
+ const { data: members } = usePrefetchedQuery(
antiAffinityGroupMemberList({ antiAffinityGroup, project })
)
-
if (!members) return
if (!members.items.length) return
-
- const instances = members.items.map((member) => member.value.name)
- const instancesToShow = instances.slice(0, 2)
- const links = instancesToShow.map((instance) => (
-
- {instance}
-
- ))
- if (instances.length > instancesToShow.length) {
- links.push(<>…>)
- }
- return {intersperse(links, )}
+ return <>{members.items.length}>
}
From 4c7f8f3efc258874438e50b941ed0fd9323d5dab Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Fri, 4 Apr 2025 09:54:55 -0700
Subject: [PATCH 41/56] update test
---
test/e2e/anti-affinity.e2e.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/e2e/anti-affinity.e2e.ts b/test/e2e/anti-affinity.e2e.ts
index 42d97c9da1..835f3a34c2 100644
--- a/test/e2e/anti-affinity.e2e.ts
+++ b/test/e2e/anti-affinity.e2e.ts
@@ -19,7 +19,7 @@ test('can nav to Affinity from /', async ({ page }) => {
name: 'romulus-remus',
type: 'anti-affinity',
policy: 'fail',
- members: 'db1/db2',
+ members: '2',
})
// click the anti-affinity group name cell to go to the view page
From ee4e2f193ebda6f6a54540d80714c0c7b85146e3 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Fri, 4 Apr 2025 14:34:51 -0500
Subject: [PATCH 42/56] don't fetch affinity groups
---
app/forms/affinity-util.tsx | 3 ---
app/forms/anti-affinity-group-edit.tsx | 7 +------
2 files changed, 1 insertion(+), 9 deletions(-)
diff --git a/app/forms/affinity-util.tsx b/app/forms/affinity-util.tsx
index 1dac1b6992..275da1bd50 100644
--- a/app/forms/affinity-util.tsx
+++ b/app/forms/affinity-util.tsx
@@ -13,9 +13,6 @@ import type * as PP from '~/util/path-params'
export const instanceList = ({ project }: PP.Project) =>
apiq('instanceList', { query: { project, limit: ALL_ISH } })
-export const affinityGroupList = ({ project }: PP.Project) =>
- apiq('affinityGroupList', { query: { project, limit: ALL_ISH } })
-
export const antiAffinityGroupList = ({ project }: PP.Project) =>
apiq('antiAffinityGroupList', { query: { project, limit: ALL_ISH } })
diff --git a/app/forms/anti-affinity-group-edit.tsx b/app/forms/anti-affinity-group-edit.tsx
index 415aff532b..97bae3f100 100644
--- a/app/forms/anti-affinity-group-edit.tsx
+++ b/app/forms/anti-affinity-group-edit.tsx
@@ -27,11 +27,7 @@ import {
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'
-import {
- affinityGroupList,
- antiAffinityGroupList,
- antiAffinityGroupView,
-} from './affinity-util'
+import { antiAffinityGroupList, antiAffinityGroupView } from './affinity-util'
export const handle = titleCrumb('New anti-affinity group')
@@ -39,7 +35,6 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
const { project, antiAffinityGroup } = getAntiAffinityGroupSelector(params)
await Promise.all([
queryClient.prefetchQuery(antiAffinityGroupList({ project })),
- queryClient.prefetchQuery(affinityGroupList({ project })),
queryClient.prefetchQuery(antiAffinityGroupView({ project, antiAffinityGroup })),
])
return null
From fb18dd3ae9c18be521562e949607a66849cdd8e8 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Fri, 4 Apr 2025 14:55:26 -0500
Subject: [PATCH 43/56] members col -> instances, don't validate name
uniqueness
---
app/forms/anti-affinity-group-create.tsx | 32 +++----------------
app/forms/anti-affinity-group-edit.tsx | 35 ++++++---------------
app/pages/project/affinity/AffinityPage.tsx | 11 ++++---
test/e2e/anti-affinity.e2e.ts | 2 +-
4 files changed, 22 insertions(+), 58 deletions(-)
diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx
index f8d2250317..7de55e8d5d 100644
--- a/app/forms/anti-affinity-group-create.tsx
+++ b/app/forms/anti-affinity-group-create.tsx
@@ -5,9 +5,8 @@
*
* Copyright Oxide Computer Company
*/
-import { useQuery } from '@tanstack/react-query'
import { useForm } from 'react-hook-form'
-import { useNavigate, type LoaderFunctionArgs } from 'react-router'
+import { useNavigate } from 'react-router'
import { queryClient, useApiMutation, type AntiAffinityGroupCreate } from '@oxide/api'
@@ -17,36 +16,25 @@ import { RadioField } from '~/components/form/fields/RadioField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { HL } from '~/components/HL'
import { titleCrumb } from '~/hooks/use-crumbs'
-import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
+import { useProjectSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'
-import { antiAffinityGroupList } from './affinity-util'
-
export const handle = titleCrumb('New anti-affinity group')
-export async function clientLoader({ params }: LoaderFunctionArgs) {
- const { project } = getProjectSelector(params)
- queryClient.prefetchQuery(antiAffinityGroupList({ project }))
- // the async demands a promise, so this just returns a promise that resolves to null
- return Promise.resolve(null)
-}
-
export default function CreateAntiAffintyGroupForm() {
const { project } = useProjectSelector()
const navigate = useNavigate()
- const onDismiss = () => navigate(pb.affinity({ project }))
const createAntiAffinityGroup = useApiMutation('antiAffinityGroupCreate', {
onSuccess(antiAffinityGroup) {
+ queryClient.invalidateEndpoint('antiAffinityGroupList')
navigate(pb.antiAffinityGroup({ project, antiAffinityGroup: antiAffinityGroup.name }))
addToast(<>Anti-affinity group {antiAffinityGroup.name} created>) // prettier-ignore
- queryClient.invalidateQueries(antiAffinityGroupList({ project }))
},
})
- const { data: existingAntiAffinityGroups } = useQuery(antiAffinityGroupList({ project }))
const defaultValues = {
name: '',
description: '',
@@ -62,7 +50,7 @@ export default function CreateAntiAffintyGroupForm() {
formType="create"
resourceName="rule"
title="Add anti-affinity group"
- onDismiss={onDismiss}
+ onDismiss={() => navigate(pb.affinity({ project }))}
onSubmit={(values) => {
createAntiAffinityGroup.mutate({
query: { project },
@@ -70,21 +58,11 @@ export default function CreateAntiAffintyGroupForm() {
})
}}
loading={createAntiAffinityGroup.isPending}
- submitDisabled={existingAntiAffinityGroups === undefined ? 'Loading …' : undefined}
submitError={createAntiAffinityGroup.error}
submitLabel="Add group"
>
- {
- if (existingAntiAffinityGroups?.items.find((g) => g.name === name)) {
- return 'Name taken. To update an existing group, edit it directly.'
- }
- }}
- />
+
-
navigate(pb.antiAffinityGroup({ project, antiAffinityGroup }))
const editAntiAffinityGroup = useApiMutation('antiAffinityGroupUpdate', {
- onSuccess(updatedAntiAffinityGroup) {
- navigate(
- pb.antiAffinityGroup({ project, antiAffinityGroup: updatedAntiAffinityGroup.name })
- )
- addToast(<>Anti-affinity group {updatedAntiAffinityGroup.name} updated>) // prettier-ignore
- queryClient.invalidateQueries(antiAffinityGroupList({ project }))
+ onSuccess(updatedGroup) {
+ queryClient.invalidateEndpoint('antiAffinityGroupView')
+ queryClient.invalidateEndpoint('antiAffinityGroupList')
+ navigate(pb.antiAffinityGroup({ project, antiAffinityGroup: updatedGroup.name }))
+ addToast(<>Anti-affinity group {updatedGroup.name} updated>) // prettier-ignore
},
})
- const { data: existingAntiAffinityGroups } = usePrefetchedQuery(
- antiAffinityGroupList({ project })
- )
-
const { data: antiAffinityGroupData } = usePrefetchedQuery(
antiAffinityGroupView({ project, antiAffinityGroup })
)
@@ -74,7 +65,7 @@ export default function EditAntiAffintyGroupForm() {
formType="create"
resourceName="rule"
title="Edit anti-affinity group"
- onDismiss={onDismiss}
+ onDismiss={() => navigate(pb.antiAffinityGroup({ project, antiAffinityGroup }))}
onSubmit={(values) => {
editAntiAffinityGroup.mutate({
path: { antiAffinityGroup },
@@ -86,15 +77,7 @@ export default function EditAntiAffintyGroupForm() {
submitError={editAntiAffinityGroup.error}
submitLabel="Edit group"
>
- {
- if (existingAntiAffinityGroups.items.find((g) => g.name === name)) {
- return 'Name taken. To update an existing group, edit it directly.'
- }
- }}
- />
+
)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 9acbba69cc..2fd769da6a 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -25,6 +25,7 @@ import { antiAffinityGroupList, antiAffinityGroupMemberList } from '~/forms/affi
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { confirmAction } from '~/stores/confirm-action'
import { addToast } from '~/stores/toast'
+import { DescriptionCell } from '~/table/cells/DescriptionCell'
import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell'
import { makeLinkCell } from '~/table/cells/LinkCell'
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
@@ -71,14 +72,16 @@ const staticCols = [
header: 'type',
cell: () => anti-affinity ,
}),
- colHelper.accessor('policy', {
- cell: (info) => ,
+ colHelper.accessor('description', {
+ cell: (info) => ,
}),
colHelper.accessor('name', {
- header: 'members',
+ header: 'instances',
cell: (info) => ,
}),
- colHelper.accessor('description', Columns.description),
+ colHelper.accessor('policy', {
+ cell: (info) => ,
+ }),
colHelper.accessor('timeCreated', Columns.timeCreated),
]
diff --git a/test/e2e/anti-affinity.e2e.ts b/test/e2e/anti-affinity.e2e.ts
index 835f3a34c2..451f9137b0 100644
--- a/test/e2e/anti-affinity.e2e.ts
+++ b/test/e2e/anti-affinity.e2e.ts
@@ -19,7 +19,7 @@ test('can nav to Affinity from /', async ({ page }) => {
name: 'romulus-remus',
type: 'anti-affinity',
policy: 'fail',
- members: '2',
+ instances: '2',
})
// click the anti-affinity group name cell to go to the view page
From f635780fa78255143a4d2cdbb25c31b70ecf90d0 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Fri, 4 Apr 2025 15:01:34 -0500
Subject: [PATCH 44/56] add delete and docs popover to group detail, use
confirmDelete
---
app/components/AffinityDocsPopover.tsx | 21 ++++++++++
app/forms/anti-affinity-group-create.tsx | 20 ++++-----
app/forms/anti-affinity-group-edit.tsx | 10 ++---
app/pages/project/affinity/AffinityPage.tsx | 41 ++++++-------------
.../affinity/AntiAffinityGroupPage.tsx | 30 +++++++++++++-
test/e2e/anti-affinity.e2e.ts | 22 +++++++++-
6 files changed, 98 insertions(+), 46 deletions(-)
create mode 100644 app/components/AffinityDocsPopover.tsx
diff --git a/app/components/AffinityDocsPopover.tsx b/app/components/AffinityDocsPopover.tsx
new file mode 100644
index 0000000000..f52f37c89e
--- /dev/null
+++ b/app/components/AffinityDocsPopover.tsx
@@ -0,0 +1,21 @@
+/*
+ * 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 { Affinity16Icon } from '@oxide/design-system/icons/react'
+
+import { docLinks } from '~/util/links'
+
+import { DocsPopover } from './DocsPopover'
+
+export const AffinityDocsPopover = () => (
+ }
+ summary="Instances in an anti-affinity group will be placed on different sleds when they start. The policy controls whether this is a hard or soft constraint."
+ links={[docLinks.affinity]}
+ />
+)
diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx
index 7de55e8d5d..290875226b 100644
--- a/app/forms/anti-affinity-group-create.tsx
+++ b/app/forms/anti-affinity-group-create.tsx
@@ -22,6 +22,12 @@ import { pb } from '~/util/path-builder'
export const handle = titleCrumb('New anti-affinity group')
+const defaultValues: Omit = {
+ name: '',
+ description: '',
+ policy: 'allow',
+}
+
export default function CreateAntiAffintyGroupForm() {
const { project } = useProjectSelector()
@@ -35,13 +41,7 @@ export default function CreateAntiAffintyGroupForm() {
},
})
- const defaultValues = {
- name: '',
- description: '',
- failureDomain: 'sled' as const,
- policy: 'allow' as const,
- }
- const form = useForm({ defaultValues })
+ const form = useForm({ defaultValues })
const control = form.control
return (
@@ -51,12 +51,12 @@ export default function CreateAntiAffintyGroupForm() {
resourceName="rule"
title="Add anti-affinity group"
onDismiss={() => navigate(pb.affinity({ project }))}
- onSubmit={(values) => {
+ onSubmit={(values) =>
createAntiAffinityGroup.mutate({
query: { project },
- body: { ...values },
+ body: { ...values, failureDomain: 'sled' },
})
- }}
+ }
loading={createAntiAffinityGroup.isPending}
submitError={createAntiAffinityGroup.error}
submitLabel="Add group"
diff --git a/app/forms/anti-affinity-group-edit.tsx b/app/forms/anti-affinity-group-edit.tsx
index f8dfd8d43d..e098c05f6a 100644
--- a/app/forms/anti-affinity-group-edit.tsx
+++ b/app/forms/anti-affinity-group-edit.tsx
@@ -7,6 +7,7 @@
*/
import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router'
+import * as R from 'remeda'
import {
queryClient,
@@ -51,13 +52,12 @@ export default function EditAntiAffintyGroupForm() {
},
})
- const { data: antiAffinityGroupData } = usePrefetchedQuery(
+ const { data: group } = usePrefetchedQuery(
antiAffinityGroupView({ project, antiAffinityGroup })
)
- const form = useForm({
- defaultValues: { ...antiAffinityGroupData },
- })
+ const defaultValues: AntiAffinityGroupUpdate = R.pick(group, ['name', 'description'])
+ const form = useForm({ defaultValues })
return (
- deleteGroup({
- path: { antiAffinityGroup: antiAffinityGroup.name },
- query: { project },
- }),
- modalTitle: 'Delete anti-affinity group',
- modalContent: (
-
- Are you sure you want to delete the anti-affinity group{' '}
- {antiAffinityGroup.name} ?
-
- ),
- errorTitle: `Error removing ${antiAffinityGroup.name}`,
- })
- },
+ onActivate: confirmDelete({
+ doDelete: () =>
+ deleteGroup({
+ path: { antiAffinityGroup: antiAffinityGroup.name },
+ query: { project },
+ }),
+ label: antiAffinityGroup.name,
+ resourceKind: 'anti-affinity group',
+ }),
},
],
[project, deleteGroup]
@@ -159,12 +149,7 @@ export default function AffinityPage() {
<>
}>Affinity
- }
- summary="Instances in an anti-affinity group will be placed on different sleds when they start. The policy attribute controls whether this is a hard or soft constraint."
- links={[docLinks.affinity]}
- />{' '}
+
New group
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index b636505978..4eda5d869a 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -8,7 +8,7 @@
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { useCallback, useState } from 'react'
-import { Outlet, type LoaderFunctionArgs } from 'react-router'
+import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
import { Affinity24Icon } from '@oxide/design-system/icons/react'
@@ -18,6 +18,7 @@ import {
usePrefetchedQuery,
type AntiAffinityGroupMember,
} from '~/api'
+import { AffinityDocsPopover } from '~/components/AffinityDocsPopover'
import { HL } from '~/components/HL'
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import {
@@ -32,6 +33,7 @@ import {
useAntiAffinityGroupSelector,
} from '~/hooks/use-params'
import { confirmAction } from '~/stores/confirm-action'
+import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import { makeLinkCell } from '~/table/cells/LinkCell'
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
@@ -102,6 +104,17 @@ export default function AntiAffinityPage() {
},
}
)
+
+ const navigate = useNavigate()
+
+ const { mutateAsync: deleteGroup } = useApiMutation('antiAffinityGroupDelete', {
+ onSuccess() {
+ navigate(pb.affinity({ project }))
+ queryClient.invalidateEndpoint('antiAffinityGroupList')
+ addToast(<>Anti-affinity group {group.name} deleted>) // prettier-ignore
+ },
+ })
+
// useState is at this level so the CreateButton can open the modal
const [isModalOpen, setIsModalOpen] = useState(false)
@@ -173,13 +186,26 @@ export default function AntiAffinityPage() {
}>{name}
- {/* TODO: Add a DocsPopover with docLinks.affinity once the doc page exists */}
+
Edit
+
+ deleteGroup({
+ path: { antiAffinityGroup: group.name },
+ query: { project },
+ }),
+ label: group.name,
+ resourceKind: 'anti-affinity group',
+ })}
+ className="destructive"
+ />
diff --git a/test/e2e/anti-affinity.e2e.ts b/test/e2e/anti-affinity.e2e.ts
index 451f9137b0..fde1952c2c 100644
--- a/test/e2e/anti-affinity.e2e.ts
+++ b/test/e2e/anti-affinity.e2e.ts
@@ -8,7 +8,7 @@
import { expect, test } from '@playwright/test'
-import { clickRowAction, expectRowVisible } from './utils'
+import { clickRowAction, closeToast, expectRowVisible } from './utils'
test('can nav to Affinity from /', async ({ page }) => {
await page.goto('/')
@@ -126,3 +126,23 @@ test('can delete an anti-affinity group', async ({ page }) => {
// check that we can see the new anti-affinity group in the table
await expect(page.getByRole('table').getByText('set-osiris')).toBeVisible()
})
+
+test('can delete anti-affinity group from detail page', async ({ page }) => {
+ await page.goto('/projects/mock-project/affinity/romulus-remus')
+
+ const modal = page.getByRole('dialog', { name: 'Confirm delete' })
+ await expect(modal).toBeHidden()
+
+ await page.getByLabel('Anti-affinity group actions').click()
+ await page.getByRole('menuitem', { name: 'Delete' }).click()
+
+ await expect(modal).toBeVisible()
+ await page.getByRole('button', { name: 'Confirm' }).click()
+
+ // modal closes, row is gone
+ await expect(modal).toBeHidden()
+ await closeToast(page)
+ await expect(page).toHaveURL('/projects/mock-project/affinity')
+ await expectRowVisible(page.getByRole('table'), { name: 'set-osiris' })
+ await expect(page.getByRole('cell', { name: 'romulus-remus' })).toBeHidden()
+})
From ff32750948d09d0a770ee6c94d650ed488e9669e Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Fri, 4 Apr 2025 16:19:41 -0500
Subject: [PATCH 45/56] help text on policy field and tip icon on policy
columns
---
app/components/AffinityDocsPopover.tsx | 10 +++++++++-
app/forms/affinity-util.tsx | 3 +++
app/forms/anti-affinity-group-create.tsx | 5 ++++-
app/pages/project/affinity/AffinityPage.tsx | 3 ++-
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 4 ++--
app/pages/project/instances/AntiAffinityCard.tsx | 2 ++
app/ui/lib/PropertiesTable.tsx | 2 +-
test/e2e/anti-affinity.e2e.ts | 2 +-
8 files changed, 24 insertions(+), 7 deletions(-)
diff --git a/app/components/AffinityDocsPopover.tsx b/app/components/AffinityDocsPopover.tsx
index f52f37c89e..b86159884e 100644
--- a/app/components/AffinityDocsPopover.tsx
+++ b/app/components/AffinityDocsPopover.tsx
@@ -7,6 +7,8 @@
*/
import { Affinity16Icon } from '@oxide/design-system/icons/react'
+import { policyHelpText } from '~/forms/affinity-util'
+import { TipIcon } from '~/ui/lib/TipIcon'
import { docLinks } from '~/util/links'
import { DocsPopover } from './DocsPopover'
@@ -15,7 +17,13 @@ export const AffinityDocsPopover = () => (
}
- summary="Instances in an anti-affinity group will be placed on different sleds when they start. The policy controls whether this is a hard or soft constraint."
+ summary="Instances in an anti-affinity group will be placed on different sleds when they start."
links={[docLinks.affinity]}
/>
)
+
+export const AffinityPolicyHeader = () => (
+ <>
+ Policy{policyHelpText}
+ >
+)
diff --git a/app/forms/affinity-util.tsx b/app/forms/affinity-util.tsx
index 275da1bd50..a2fd247e33 100644
--- a/app/forms/affinity-util.tsx
+++ b/app/forms/affinity-util.tsx
@@ -31,3 +31,6 @@ export const antiAffinityGroupMemberList = ({
// member limit in DB is currently 32, so pagination isn't needed
query: { project, limit: ALL_ISH },
})
+
+export const policyHelpText =
+ 'Whether instances are allowed to start when every available sled already has a group member on it'
diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx
index 290875226b..0e80f65a19 100644
--- a/app/forms/anti-affinity-group-create.tsx
+++ b/app/forms/anti-affinity-group-create.tsx
@@ -20,6 +20,8 @@ import { useProjectSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'
+import { policyHelpText } from './affinity-util'
+
export const handle = titleCrumb('New anti-affinity group')
const defaultValues: Omit = {
@@ -28,7 +30,7 @@ const defaultValues: Omit = {
policy: 'allow',
}
-export default function CreateAntiAffintyGroupForm() {
+export default function CreateAntiAffinityGroupForm() {
const { project } = useProjectSelector()
const navigate = useNavigate()
@@ -65,6 +67,7 @@ export default function CreateAntiAffintyGroupForm() {
,
}),
colHelper.accessor('policy', {
+ header: AffinityPolicyHeader,
cell: (info) => ,
}),
colHelper.accessor('timeCreated', Columns.timeCreated),
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 4eda5d869a..7a8b0c8faa 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -18,7 +18,7 @@ import {
usePrefetchedQuery,
type AntiAffinityGroupMember,
} from '~/api'
-import { AffinityDocsPopover } from '~/components/AffinityDocsPopover'
+import { AffinityDocsPopover, AffinityPolicyHeader } from '~/components/AffinityDocsPopover'
import { HL } from '~/components/HL'
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import {
@@ -214,7 +214,7 @@ export default function AntiAffinityPage() {
anti-affinity
-
+ }>
{policy}
diff --git a/app/pages/project/instances/AntiAffinityCard.tsx b/app/pages/project/instances/AntiAffinityCard.tsx
index 95c4649fcb..7063687597 100644
--- a/app/pages/project/instances/AntiAffinityCard.tsx
+++ b/app/pages/project/instances/AntiAffinityCard.tsx
@@ -17,6 +17,7 @@ import {
} from '@oxide/api'
import { Affinity24Icon } from '@oxide/design-system/icons/react'
+import { AffinityPolicyHeader } from '~/components/AffinityDocsPopover'
import { useInstanceSelector } from '~/hooks/use-params'
import { makeLinkCell } from '~/table/cells/LinkCell'
import { Columns } from '~/table/columns/common'
@@ -39,6 +40,7 @@ const colHelper = createColumnHelper()
const staticCols = [
colHelper.accessor('description', Columns.description),
colHelper.accessor('policy', {
+ header: AffinityPolicyHeader,
cell: (info) => {info.getValue()} ,
}),
]
diff --git a/app/ui/lib/PropertiesTable.tsx b/app/ui/lib/PropertiesTable.tsx
index 9162cb4eae..8101ea8b30 100644
--- a/app/ui/lib/PropertiesTable.tsx
+++ b/app/ui/lib/PropertiesTable.tsx
@@ -54,7 +54,7 @@ export function PropertiesTable({
}
interface PropertiesTableRowProps {
- label: string
+ label: ReactNode
children: ReactNode
}
PropertiesTable.Row = ({ label, children }: PropertiesTableRowProps) => (
diff --git a/test/e2e/anti-affinity.e2e.ts b/test/e2e/anti-affinity.e2e.ts
index fde1952c2c..a1c7d3d869 100644
--- a/test/e2e/anti-affinity.e2e.ts
+++ b/test/e2e/anti-affinity.e2e.ts
@@ -18,7 +18,7 @@ test('can nav to Affinity from /', async ({ page }) => {
await expectRowVisible(page.getByRole('table'), {
name: 'romulus-remus',
type: 'anti-affinity',
- policy: 'fail',
+ Policy: 'fail',
instances: '2',
})
From 872feac6ccf4196a112cd11ee3161c3a10864414 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Fri, 4 Apr 2025 14:39:01 -0700
Subject: [PATCH 46/56] merge main and resolve conflicts with
AffinityGroupPolicyBadge
---
app/pages/project/affinity/AffinityPage.tsx | 14 ++++----------
.../project/affinity/AntiAffinityGroupPage.tsx | 3 ++-
app/pages/project/instances/AntiAffinityCard.tsx | 4 ++--
3 files changed, 8 insertions(+), 13 deletions(-)
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 89d2c74277..bef8193545 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -55,16 +55,10 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
const colHelper = createColumnHelper()
-type AffinityGroupPolicyBadgeProps = { policy: AffinityPolicy; className?: string }
-const AffinityGroupPolicyBadge = ({ policy, className }: AffinityGroupPolicyBadgeProps) => (
-
- {policy}
-
-)
+export const AffinityGroupPolicyBadge = ({ policy }: { policy: AffinityPolicy }) => {
+ const variant = { allow: 'default' as const, fail: 'solid' as const }[policy]
+ return {policy} // prettier-ignore
+}
const staticCols = [
colHelper.accessor(() => {}, {
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 7a8b0c8faa..0b5b1fb3d8 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -32,6 +32,7 @@ import {
getAntiAffinityGroupSelector,
useAntiAffinityGroupSelector,
} from '~/hooks/use-params'
+import { AffinityGroupPolicyBadge } from '~/pages/project/affinity/AffinityPage'
import { confirmAction } from '~/stores/confirm-action'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
@@ -215,7 +216,7 @@ export default function AntiAffinityPage() {
}>
- {policy}
+
{membersCount}
diff --git a/app/pages/project/instances/AntiAffinityCard.tsx b/app/pages/project/instances/AntiAffinityCard.tsx
index 7063687597..6d4c237a7e 100644
--- a/app/pages/project/instances/AntiAffinityCard.tsx
+++ b/app/pages/project/instances/AntiAffinityCard.tsx
@@ -19,10 +19,10 @@ import { Affinity24Icon } from '@oxide/design-system/icons/react'
import { AffinityPolicyHeader } from '~/components/AffinityDocsPopover'
import { useInstanceSelector } from '~/hooks/use-params'
+import { AffinityGroupPolicyBadge } from '~/pages/project/affinity/AffinityPage'
import { makeLinkCell } from '~/table/cells/LinkCell'
import { Columns } from '~/table/columns/common'
import { Table } from '~/table/Table'
-import { Badge } from '~/ui/lib/Badge'
import { CardBlock } from '~/ui/lib/CardBlock'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { TableEmptyBox } from '~/ui/lib/Table'
@@ -41,7 +41,7 @@ const staticCols = [
colHelper.accessor('description', Columns.description),
colHelper.accessor('policy', {
header: AffinityPolicyHeader,
- cell: (info) => {info.getValue()} ,
+ cell: (info) => ,
}),
]
From 08b81e1df6050978374de657cb67569849111d74 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Fri, 4 Apr 2025 17:17:07 -0500
Subject: [PATCH 47/56] put back the line about policy in the popover
---
app/components/AffinityDocsPopover.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/components/AffinityDocsPopover.tsx b/app/components/AffinityDocsPopover.tsx
index b86159884e..5d4ec1101a 100644
--- a/app/components/AffinityDocsPopover.tsx
+++ b/app/components/AffinityDocsPopover.tsx
@@ -17,7 +17,7 @@ export const AffinityDocsPopover = () => (
}
- summary="Instances in an anti-affinity group will be placed on different sleds when they start."
+ summary="Instances in an anti-affinity group will be placed on different sleds when they start. The policy attribute determines whether instances can still start when a unique sled is not available."
links={[docLinks.affinity]}
/>
)
From ee98abbbce772e6e01274a440b19fe34ec8b8884 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Fri, 4 Apr 2025 18:05:30 -0500
Subject: [PATCH 48/56] remove title from icons in sidebar nav
---
app/layouts/ProjectLayoutBase.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/layouts/ProjectLayoutBase.tsx b/app/layouts/ProjectLayoutBase.tsx
index b404cc64fa..8266c34a37 100644
--- a/app/layouts/ProjectLayoutBase.tsx
+++ b/app/layouts/ProjectLayoutBase.tsx
@@ -115,10 +115,10 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
Floating IPs
- Affinity
+ Affinity
- Access
+ Access
From 5a5831269aeb8753e722eacc69e5604816cd83f5 Mon Sep 17 00:00:00 2001
From: Charlie Park
Date: Fri, 4 Apr 2025 17:30:51 -0700
Subject: [PATCH 49/56] Refactoring form in add instance to A-A group modal
---
app/forms/anti-affinity-group-member-add.tsx | 52 ++++++++++----------
1 file changed, 27 insertions(+), 25 deletions(-)
diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx
index ecaeef1605..5b43319ac8 100644
--- a/app/forms/anti-affinity-group-member-add.tsx
+++ b/app/forms/anti-affinity-group-member-add.tsx
@@ -32,15 +32,16 @@ export default function AddAntiAffinityGroupMemberForm({
}) {
const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
- const form = useForm({
- defaultValues: {
- instance: '',
- },
- })
+ const { control, handleSubmit, reset } =
+ useForm({
+ defaultValues: {
+ instance: '',
+ },
+ })
const onDismiss = () => {
setIsModalOpen(false)
- form.reset()
+ reset()
}
const { mutateAsync: addMember } = useApiMutation('antiAffinityGroupMemberInstanceAdd', {
@@ -48,39 +49,40 @@ export default function AddAntiAffinityGroupMemberForm({
onDismiss()
queryClient.invalidateEndpoint('antiAffinityGroupMemberList')
queryClient.invalidateEndpoint('antiAffinityGroupView')
- addToast(<>Member {variables.path.instance} added to anti-affinity group {antiAffinityGroup} >) // prettier-ignore
+ addToast(<>Instance {variables.path.instance} added to anti-affinity group {antiAffinityGroup} >) // prettier-ignore
},
})
+ const onSubmit = ({ instance }: AntiAffinityGroupMemberInstanceAddPathParams) => {
+ addMember({
+ path: { antiAffinityGroup, instance },
+ query: { project },
+ })
+ }
+
return (
Select an instance to add to the anti-affinity group{' '}
- {antiAffinityGroup} .
+ {antiAffinityGroup} . Only stopped instances can be added to the group.
-
+
- addMember({
- path: {
- antiAffinityGroup,
- instance: form.getValues('instance'),
- },
- query: { project },
- })
- }
+ onAction={handleSubmit(onSubmit)}
actionText="Add to group"
/>
From 99a2c788a4cc4c29f0531c3b915b863ac56bed0c Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Mon, 7 Apr 2025 10:18:38 -0500
Subject: [PATCH 50/56] type -> group type, remove description column
---
app/forms/affinity-util.tsx | 2 +-
app/pages/project/affinity/AffinityPage.tsx | 6 +-----
test/e2e/anti-affinity.e2e.ts | 2 +-
3 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/app/forms/affinity-util.tsx b/app/forms/affinity-util.tsx
index a2fd247e33..99106e2933 100644
--- a/app/forms/affinity-util.tsx
+++ b/app/forms/affinity-util.tsx
@@ -33,4 +33,4 @@ export const antiAffinityGroupMemberList = ({
})
export const policyHelpText =
- 'Whether instances are allowed to start when every available sled already has a group member on it'
+ 'Whether member instances are allowed to start when every available sled already has a group member on it'
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index bef8193545..4b067613e9 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -25,7 +25,6 @@ import { antiAffinityGroupList, antiAffinityGroupMemberList } from '~/forms/affi
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
-import { DescriptionCell } from '~/table/cells/DescriptionCell'
import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell'
import { makeLinkCell } from '~/table/cells/LinkCell'
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
@@ -62,12 +61,9 @@ export const AffinityGroupPolicyBadge = ({ policy }: { policy: AffinityPolicy })
const staticCols = [
colHelper.accessor(() => {}, {
- header: 'type',
+ header: 'Group type',
cell: () => anti-affinity ,
}),
- colHelper.accessor('description', {
- cell: (info) => ,
- }),
colHelper.accessor('name', {
header: 'instances',
cell: (info) => ,
diff --git a/test/e2e/anti-affinity.e2e.ts b/test/e2e/anti-affinity.e2e.ts
index a1c7d3d869..784b68b4da 100644
--- a/test/e2e/anti-affinity.e2e.ts
+++ b/test/e2e/anti-affinity.e2e.ts
@@ -17,7 +17,7 @@ test('can nav to Affinity from /', async ({ page }) => {
await expectRowVisible(page.getByRole('table'), {
name: 'romulus-remus',
- type: 'anti-affinity',
+ 'Group type': 'anti-affinity',
Policy: 'fail',
instances: '2',
})
From 6f21f11aaf33cb97f6df46962bd0e5092c599e57 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Mon, 7 Apr 2025 11:00:32 -0500
Subject: [PATCH 51/56] on second thought: make page title Affinity Groups
---
app/layouts/ProjectLayoutBase.tsx | 4 ++--
app/pages/project/affinity/AffinityPage.tsx | 6 +++---
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 2 +-
app/routes.tsx | 4 ++--
app/util/__snapshots__/path-builder.spec.ts.snap | 8 ++++----
test/e2e/anti-affinity.e2e.ts | 6 +++---
6 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/app/layouts/ProjectLayoutBase.tsx b/app/layouts/ProjectLayoutBase.tsx
index 8266c34a37..5381ee9597 100644
--- a/app/layouts/ProjectLayoutBase.tsx
+++ b/app/layouts/ProjectLayoutBase.tsx
@@ -69,7 +69,7 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
{ value: 'Images', path: pb.projectImages(projectSelector) },
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
- { value: 'Affinity', path: pb.affinity(projectSelector) },
+ { value: 'Affinity Groups', path: pb.affinity(projectSelector) },
{ value: 'Access', path: pb.projectAccess(projectSelector) },
]
// filter out the entry for the path we're currently on
@@ -115,7 +115,7 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
Floating IPs
- Affinity
+ Affinity Groups
Access
diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx
index 4b067613e9..8c9c72452b 100644
--- a/app/pages/project/affinity/AffinityPage.tsx
+++ b/app/pages/project/affinity/AffinityPage.tsx
@@ -60,8 +60,8 @@ export const AffinityGroupPolicyBadge = ({ policy }: { policy: AffinityPolicy })
}
const staticCols = [
- colHelper.accessor(() => {}, {
- header: 'Group type',
+ colHelper.display({
+ header: 'type',
cell: () => anti-affinity ,
}),
colHelper.accessor('name', {
@@ -139,7 +139,7 @@ export default function AffinityPage() {
return (
<>
- }>Affinity
+ }>Affinity Groups
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 0b5b1fb3d8..dd8ccfcafa 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -72,7 +72,7 @@ const AntiAffinityGroupMemberEmptyState = () => (
}
- title="No anti-affinity group members"
+ title="No group members"
body="Add an instance to the group to see it here"
/>
diff --git a/app/routes.tsx b/app/routes.tsx
index 6dd1b5397e..965944f833 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -502,14 +502,14 @@ export const routes = createRoutesFromElements(
/>
import('./pages/project/affinity/AffinityPage').then(convert)}
- handle={{ crumb: 'Affinity' }}
+ handle={{ crumb: 'Affinity Groups' }}
>
import('./forms/anti-affinity-group-create').then(convert)}
/>
-
+
import('./pages/project/affinity/AffinityPage.tsx').then(convert)}
diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap
index fc51c123cc..d08eaa234f 100644
--- a/app/util/__snapshots__/path-builder.spec.ts.snap
+++ b/app/util/__snapshots__/path-builder.spec.ts.snap
@@ -12,7 +12,7 @@ exports[`breadcrumbs 2`] = `
"path": "/projects/p/instances",
},
{
- "label": "Affinity",
+ "label": "Affinity Groups",
"path": "/projects/p/affinity",
},
],
@@ -26,7 +26,7 @@ exports[`breadcrumbs 2`] = `
"path": "/projects/p/instances",
},
{
- "label": "Affinity",
+ "label": "Affinity Groups",
"path": "/projects/p/",
},
],
@@ -40,7 +40,7 @@ exports[`breadcrumbs 2`] = `
"path": "/projects/p/instances",
},
{
- "label": "Affinity",
+ "label": "Affinity Groups",
"path": "/projects/p/affinity",
},
{
@@ -58,7 +58,7 @@ exports[`breadcrumbs 2`] = `
"path": "/projects/p/instances",
},
{
- "label": "Affinity",
+ "label": "Affinity Groups",
"path": "/projects/p/affinity",
},
{
diff --git a/test/e2e/anti-affinity.e2e.ts b/test/e2e/anti-affinity.e2e.ts
index 784b68b4da..edd751d9b1 100644
--- a/test/e2e/anti-affinity.e2e.ts
+++ b/test/e2e/anti-affinity.e2e.ts
@@ -17,7 +17,7 @@ test('can nav to Affinity from /', async ({ page }) => {
await expectRowVisible(page.getByRole('table'), {
name: 'romulus-remus',
- 'Group type': 'anti-affinity',
+ type: 'anti-affinity',
Policy: 'fail',
instances: '2',
})
@@ -28,7 +28,7 @@ test('can nav to Affinity from /', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'romulus-remus' })).toBeVisible()
await expect(page).toHaveURL('/projects/mock-project/affinity/romulus-remus')
await expect(page).toHaveTitle(
- 'romulus-remus / Affinity / mock-project / Projects / Oxide Console'
+ 'romulus-remus / Affinity Groups / mock-project / Projects / Oxide Console'
)
})
@@ -67,7 +67,7 @@ test('can add a new anti-affinity group', async ({ page }) => {
await expect(cell).toBeHidden()
// expect empty message
- await expect(page.getByText('No anti-affinity group members')).toBeVisible()
+ await expect(page.getByText('No group members')).toBeVisible()
})
// edit an anti-affinity group from the view page
From 064879ec42e36a11f834d713f2573c7156f6fefb Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Mon, 7 Apr 2025 11:19:08 -0500
Subject: [PATCH 52/56] simplify form reset by unmounting, test that in e2e
---
app/forms/anti-affinity-group-member-add.tsx | 40 ++++++-------------
.../affinity/AntiAffinityGroupPage.tsx | 11 ++---
test/e2e/anti-affinity.e2e.ts | 21 ++++++++--
3 files changed, 37 insertions(+), 35 deletions(-)
diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx
index 5b43319ac8..3c1944709e 100644
--- a/app/forms/anti-affinity-group-member-add.tsx
+++ b/app/forms/anti-affinity-group-member-add.tsx
@@ -8,12 +8,7 @@
import { useForm } from 'react-hook-form'
-import {
- queryClient,
- useApiMutation,
- type AntiAffinityGroupMemberInstanceAddPathParams,
- type Instance,
-} from '~/api'
+import { queryClient, useApiMutation, type Instance } from '~/api'
import { ComboboxField } from '~/components/form/fields/ComboboxField'
import { HL } from '~/components/HL'
import { useAntiAffinityGroupSelector } from '~/hooks/use-params'
@@ -21,28 +16,19 @@ import { addToast } from '~/stores/toast'
import { toComboboxItems } from '~/ui/lib/Combobox'
import { Modal } from '~/ui/lib/Modal'
+type Values = { instance: string }
+
+const defaultValues: Values = { instance: '' }
+
+type Props = { instances: Instance[]; onDismiss: () => void }
+
export default function AddAntiAffinityGroupMemberForm({
- availableInstances,
- isModalOpen,
- setIsModalOpen,
-}: {
- availableInstances: Instance[]
- isModalOpen: boolean
- setIsModalOpen: (open: boolean) => void
-}) {
+ instances: availableInstances,
+ onDismiss,
+}: Props) {
const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
- const { control, handleSubmit, reset } =
- useForm({
- defaultValues: {
- instance: '',
- },
- })
-
- const onDismiss = () => {
- setIsModalOpen(false)
- reset()
- }
+ const { control, handleSubmit } = useForm({ defaultValues })
const { mutateAsync: addMember } = useApiMutation('antiAffinityGroupMemberInstanceAdd', {
onSuccess(_data, variables) {
@@ -53,7 +39,7 @@ export default function AddAntiAffinityGroupMemberForm({
},
})
- const onSubmit = ({ instance }: AntiAffinityGroupMemberInstanceAddPathParams) => {
+ const onSubmit = ({ instance }: Values) => {
addMember({
path: { antiAffinityGroup, instance },
query: { project },
@@ -61,7 +47,7 @@ export default function AddAntiAffinityGroupMemberForm({
}
return (
-
+
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index dd8ccfcafa..67d7c88066 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -241,11 +241,12 @@ export default function AntiAffinityPage() {
{membersCount ?
: }
-
+ {isModalOpen && (
+ setIsModalOpen(false)}
+ />
+ )}
>
)
diff --git a/test/e2e/anti-affinity.e2e.ts b/test/e2e/anti-affinity.e2e.ts
index edd751d9b1..873c80ccc7 100644
--- a/test/e2e/anti-affinity.e2e.ts
+++ b/test/e2e/anti-affinity.e2e.ts
@@ -52,9 +52,24 @@ test('can add a new anti-affinity group', async ({ page }) => {
await expect(page).toHaveURL('/projects/mock-project/affinity/new-anti-affinity-group')
// add a member to the new anti-affinity group
- await page.getByRole('button', { name: 'Add instance' }).click()
- await expect(page.getByRole('heading', { name: 'Add instance to group' })).toBeVisible()
- await page.getByRole('combobox', { name: 'Instance' }).fill('db1')
+ const addInstanceButton = page.getByRole('button', { name: 'Add instance' })
+ const addInstanceModal = page.getByRole('dialog', { name: 'Add instance to group' })
+ const instanceCombobox = page.getByRole('combobox', { name: 'Instance' })
+
+ // open modal and pick instance
+ await addInstanceButton.click()
+ await expect(addInstanceModal).toBeVisible()
+ await instanceCombobox.fill('db1')
+ await page.getByRole('option', { name: 'db1' }).click()
+ await expect(instanceCombobox).toHaveValue('db1')
+
+ // close and reopen the modal to make sure the field clears
+ await page.getByRole('button', { name: 'Cancel' }).click()
+ await expect(addInstanceModal).toBeHidden()
+ await addInstanceButton.click()
+ await expect(instanceCombobox).toHaveValue('')
+
+ // now do it again for real and submit
await page.getByRole('option', { name: 'db1' }).click()
await page.getByRole('button', { name: 'Add to group' }).click()
From 16bb0329756463a7a5cb73f04fd45bbae0a079e9 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Mon, 7 Apr 2025 11:42:58 -0500
Subject: [PATCH 53/56] use handleSubmit higher so we don't have to type
explicitly
---
app/forms/anti-affinity-group-member-add.tsx | 19 ++++++-------------
1 file changed, 6 insertions(+), 13 deletions(-)
diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx
index 3c1944709e..88d8dc8e77 100644
--- a/app/forms/anti-affinity-group-member-add.tsx
+++ b/app/forms/anti-affinity-group-member-add.tsx
@@ -22,10 +22,7 @@ const defaultValues: Values = { instance: '' }
type Props = { instances: Instance[]; onDismiss: () => void }
-export default function AddAntiAffinityGroupMemberForm({
- instances: availableInstances,
- onDismiss,
-}: Props) {
+export default function AddAntiAffinityGroupMemberForm({ instances, onDismiss }: Props) {
const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
const { control, handleSubmit } = useForm({ defaultValues })
@@ -39,12 +36,12 @@ export default function AddAntiAffinityGroupMemberForm({
},
})
- const onSubmit = ({ instance }: Values) => {
+ const onSubmit = handleSubmit(({ instance }) => {
addMember({
path: { antiAffinityGroup, instance },
query: { project },
})
- }
+ })
return (
@@ -54,23 +51,19 @@ export default function AddAntiAffinityGroupMemberForm({
Select an instance to add to the anti-affinity group{' '}
{antiAffinityGroup} . Only stopped instances can be added to the group.
-
-
+
)
}
From 37a673383c7d1de68f13429432ca08b93c3472df Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Mon, 7 Apr 2025 14:50:25 -0500
Subject: [PATCH 54/56] make enter submit add instance modal form
---
app/forms/anti-affinity-group-member-add.tsx | 12 +++++++-----
app/ui/lib/Combobox.tsx | 12 +++++++-----
2 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/app/forms/anti-affinity-group-member-add.tsx b/app/forms/anti-affinity-group-member-add.tsx
index 88d8dc8e77..64a6c8b5cf 100644
--- a/app/forms/anti-affinity-group-member-add.tsx
+++ b/app/forms/anti-affinity-group-member-add.tsx
@@ -6,6 +6,7 @@
* Copyright Oxide Computer Company
*/
+import { useId } from 'react'
import { useForm } from 'react-hook-form'
import { queryClient, useApiMutation, type Instance } from '~/api'
@@ -25,7 +26,8 @@ type Props = { instances: Instance[]; onDismiss: () => void }
export default function AddAntiAffinityGroupMemberForm({ instances, onDismiss }: Props) {
const { project, antiAffinityGroup } = useAntiAffinityGroupSelector()
- const { control, handleSubmit } = useForm({ defaultValues })
+ const form = useForm({ defaultValues })
+ const formId = useId()
const { mutateAsync: addMember } = useApiMutation('antiAffinityGroupMemberInstanceAdd', {
onSuccess(_data, variables) {
@@ -36,7 +38,7 @@ export default function AddAntiAffinityGroupMemberForm({ instances, onDismiss }:
},
})
- const onSubmit = handleSubmit(({ instance }) => {
+ const onSubmit = form.handleSubmit(({ instance }) => {
addMember({
path: { antiAffinityGroup, instance },
query: { project },
@@ -51,19 +53,19 @@ export default function AddAntiAffinityGroupMemberForm({ instances, onDismiss }:
Select an instance to add to the anti-affinity group{' '}
{antiAffinityGroup} . Only stopped instances can be added to the group.
-
-
+
)
}
diff --git a/app/ui/lib/Combobox.tsx b/app/ui/lib/Combobox.tsx
index 4369921695..45859241f8 100644
--- a/app/ui/lib/Combobox.tsx
+++ b/app/ui/lib/Combobox.tsx
@@ -213,12 +213,14 @@ export const Combobox = ({
onInputChange?.(value)
}}
onKeyDown={(e) => {
- // Prevent form submission when the user presses Enter inside a combobox.
- // The combobox component already handles Enter keypresses to select items,
- // so we only preventDefault when the combobox is closed.
- if (e.key === 'Enter' && !open) {
+ // If the caller is using onEnter to override enter behavior, preventDefault
+ // in order to prevent the containing form from being submitted too. We don't
+ // need to do this when the combobox is open because that enter keypress is
+ // already handled internally (selects the highlighted item). So we only do
+ // this when the combobox is closed.
+ if (e.key === 'Enter' && onEnter && !open) {
e.preventDefault()
- onEnter?.(e)
+ onEnter(e)
}
}}
placeholder={placeholder}
From 42f772e8c0b04624e745bf303d557cf5e3e1402d Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Mon, 7 Apr 2025 15:20:54 -0500
Subject: [PATCH 55/56] link to instance settings rather than default tab
---
app/pages/project/affinity/AntiAffinityGroupPage.tsx | 2 +-
test/e2e/anti-affinity.e2e.ts | 4 ++++
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/pages/project/affinity/AntiAffinityGroupPage.tsx b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
index 67d7c88066..92c3a908ab 100644
--- a/app/pages/project/affinity/AntiAffinityGroupPage.tsx
+++ b/app/pages/project/affinity/AntiAffinityGroupPage.tsx
@@ -154,7 +154,7 @@ export default function AntiAffinityPage() {
[
colHelper.accessor('value.name', {
header: 'name',
- cell: makeLinkCell((instance) => pb.instance({ project, instance })),
+ cell: makeLinkCell((instance) => pb.instanceSettings({ project, instance })),
}),
colHelper.accessor('value.runState', Columns.instanceState),
colHelper.accessor('value.id', Columns.id),
diff --git a/test/e2e/anti-affinity.e2e.ts b/test/e2e/anti-affinity.e2e.ts
index 873c80ccc7..36c7bb3f5b 100644
--- a/test/e2e/anti-affinity.e2e.ts
+++ b/test/e2e/anti-affinity.e2e.ts
@@ -30,6 +30,10 @@ test('can nav to Affinity from /', async ({ page }) => {
await expect(page).toHaveTitle(
'romulus-remus / Affinity Groups / mock-project / Projects / Oxide Console'
)
+
+ // click through to instance
+ await page.getByRole('link', { name: 'db1' }).click()
+ await expect(page).toHaveURL('/projects/mock-project/instances/db1/settings')
})
test('can add a new anti-affinity group', async ({ page }) => {
From 696484e3365af25d217e45fa84aee94a9c145881 Mon Sep 17 00:00:00 2001
From: David Crespo
Date: Mon, 7 Apr 2025 15:49:27 -0500
Subject: [PATCH 56/56] hopefully final policy help copy tweaks
---
app/forms/affinity-util.tsx | 2 +-
app/forms/anti-affinity-group-create.tsx | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/app/forms/affinity-util.tsx b/app/forms/affinity-util.tsx
index 99106e2933..32408f32ae 100644
--- a/app/forms/affinity-util.tsx
+++ b/app/forms/affinity-util.tsx
@@ -33,4 +33,4 @@ export const antiAffinityGroupMemberList = ({
})
export const policyHelpText =
- 'Whether member instances are allowed to start when every available sled already has a group member on it'
+ "Determines whether member instances are allowed to start when the anti-affinity rule can't be satisfied"
diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx
index 0e80f65a19..119888f921 100644
--- a/app/forms/anti-affinity-group-create.tsx
+++ b/app/forms/anti-affinity-group-create.tsx
@@ -67,7 +67,8 @@ export default function CreateAntiAffinityGroupForm() {