From 32d18ad00a43ac6743ba430f43d2463b43a4f45c Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 27 Mar 2025 14:20:48 -0700 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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}`, })