From 3da57c1190a229516c702508dd1b4d891b37835d Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 29 Aug 2024 18:42:46 -0700 Subject: [PATCH 1/8] Move edit form to view page for IP Pools --- app/forms/ip-pool-edit.tsx | 16 ++++--- app/pages/system/networking/IpPoolPage.tsx | 47 ++++++++++++++++++--- app/pages/system/networking/IpPoolsPage.tsx | 2 + app/routes.tsx | 12 +++--- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx index befe790dcf..67fd99c933 100644 --- a/app/forms/ip-pool-edit.tsx +++ b/app/forms/ip-pool-edit.tsx @@ -30,22 +30,24 @@ EditIpPoolSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { export function EditIpPoolSideModalForm() { const queryClient = useApiQueryClient() const navigate = useNavigate() - const poolSelector = useIpPoolSelector() - const onDismiss = () => navigate(pb.ipPools()) - const { data: pool } = usePrefetchedApiQuery('ipPoolView', { path: poolSelector }) + const form = useForm({ defaultValues: pool }) + const editPool = useApiMutation('ipPoolUpdate', { onSuccess(_pool) { - queryClient.invalidateQueries('ipPoolList') + if (pool.name !== _pool.name) { + navigate(pb.ipPool({ pool: _pool.name })) + } else { + queryClient.invalidateQueries('ipPoolView') + navigate(pb.ipPool({ pool: pool.name })) + } addToast({ content: 'Your IP pool has been updated' }) - onDismiss() }, }) - - const form = useForm({ defaultValues: pool }) + const onDismiss = () => navigate(pb.ipPool({ pool: poolSelector.pool })) return ( [ + { + label: 'Edit', + onActivate() { + navigate(pb.ipPoolEdit(poolSelector)) + }, + }, + { + label: 'Delete', + onActivate: confirmDelete({ + doDelete: () => deletePool.mutateAsync({ path: { pool: pool.name } }), + label: pool.name, + }), + }, + ], + [deletePool, navigate, poolSelector, pool.name] + ) + return ( <> }>{pool.name} - } - summary="IP pools are collections of external IPs you can assign to silos. When a pool is linked to a silo, users in that silo can allocate IPs from the pool for their instances." - links={[docLinks.systemIpPools]} - /> +
+ } + summary="IP pools are collections of external IPs you can assign to silos. When a pool is linked to a silo, users in that silo can allocate IPs from the pool for their instances." + links={[docLinks.systemIpPools]} + /> + +
diff --git a/app/pages/system/networking/IpPoolsPage.tsx b/app/pages/system/networking/IpPoolsPage.tsx index f18d58bc7a..02792b4ba4 100644 --- a/app/pages/system/networking/IpPoolsPage.tsx +++ b/app/pages/system/networking/IpPoolsPage.tsx @@ -23,6 +23,7 @@ import { DocsPopover } from '~/components/DocsPopover' import { IpUtilCell } from '~/components/IpPoolUtilization' import { useQuickActions } from '~/hooks' import { confirmDelete } from '~/stores/confirm-delete' +import { addToast } from '~/stores/toast' import { SkeletonCell } from '~/table/cells/EmptyCell' import { makeLinkCell } from '~/table/cells/LinkCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' @@ -79,6 +80,7 @@ export function IpPoolsPage() { const deletePool = useApiMutation('ipPoolDelete', { onSuccess() { apiQueryClient.invalidateQueries('ipPoolList') + addToast({ content: 'IP pool deleted' }) }, }) diff --git a/app/routes.tsx b/app/routes.tsx index 455f86ca6d..f6f21f3856 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -198,12 +198,6 @@ export const routes = createRoutesFromElements( > } /> - } - loader={EditIpPoolSideModalForm.loader} - handle={{ crumb: 'Edit IP pool' }} - /> @@ -213,6 +207,12 @@ export const routes = createRoutesFromElements( loader={IpPoolPage.loader} handle={{ crumb: poolCrumb }} > + } + loader={EditIpPoolSideModalForm.loader} + handle={{ crumb: 'Edit IP pool' }} + /> } /> From 6f784e3d8eed3403acc3a86cb4e3a02fb8868354 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 30 Aug 2024 09:17:28 -0700 Subject: [PATCH 2/8] Add deletion restriction, disabling when IP ranges present --- app/pages/system/networking/IpPoolPage.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/pages/system/networking/IpPoolPage.tsx b/app/pages/system/networking/IpPoolPage.tsx index d28c7aac82..33b1f25d9f 100644 --- a/app/pages/system/networking/IpPoolPage.tsx +++ b/app/pages/system/networking/IpPoolPage.tsx @@ -48,9 +48,10 @@ import { TipIcon } from '~/ui/lib/TipIcon' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' +const query = { limit: PAGE_SIZE } + IpPoolPage.loader = async function ({ params }: LoaderFunctionArgs) { const { pool } = getIpPoolSelector(params) - const query = { limit: PAGE_SIZE } await Promise.all([ apiQueryClient.prefetchQuery('ipPoolView', { path: { pool } }), apiQueryClient.prefetchQuery('ipPoolSiloList', { path: { pool }, query }), @@ -72,6 +73,10 @@ IpPoolPage.loader = async function ({ params }: LoaderFunctionArgs) { export function IpPoolPage() { const poolSelector = useIpPoolSelector() const { data: pool } = usePrefetchedApiQuery('ipPoolView', { path: poolSelector }) + const { data: ranges } = usePrefetchedApiQuery('ipPoolRangeList', { + path: poolSelector, + query, + }) const navigate = useNavigate() const deletePool = useApiMutation('ipPoolDelete', { onSuccess() { @@ -95,9 +100,12 @@ export function IpPoolPage() { doDelete: () => deletePool.mutateAsync({ path: { pool: pool.name } }), label: pool.name, }), + disabled: + !!ranges.items.length && 'IP pool cannot be deleted while it contains IP ranges', + className: ranges.items.length ? '' : 'destructive', }, ], - [deletePool, navigate, poolSelector, pool.name] + [deletePool, navigate, poolSelector, pool.name, ranges.items] ) return ( From 40d914daf8fe4da65f3bef4b2841b752c3d56649 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 30 Aug 2024 09:43:44 -0700 Subject: [PATCH 3/8] Add test for view-page deletion --- test/e2e/ip-pools.e2e.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/e2e/ip-pools.e2e.ts b/test/e2e/ip-pools.e2e.ts index 9470794898..a9beb7503c 100644 --- a/test/e2e/ip-pools.e2e.ts +++ b/test/e2e/ip-pools.e2e.ts @@ -110,7 +110,7 @@ test('IP pool link silo', async ({ page }) => { await expectRowVisible(table, { Silo: 'myriad', 'Pool is silo default': '' }) }) -test('IP pool delete', async ({ page }) => { +test('IP pool delete from IP Pools list page', async ({ page }) => { await page.goto('/system/networking/ip-pools') // can't delete a pool containing ranges @@ -133,6 +133,24 @@ test('IP pool delete', async ({ page }) => { await expect(page.getByRole('cell', { name: 'ip-pool-3' })).toBeHidden() }) +test('IP pool delete from IP Pool view page', async ({ page }) => { + // can't delete a pool containing ranges + await page.goto('/system/networking/ip-pools/ip-pool-1') + await page.getByRole('button', { name: 'IP pool actions' }).click() + await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeDisabled() + + // can delete a pool with no ranges + await page.goto('/system/networking/ip-pools/ip-pool-3') + await page.getByRole('button', { name: 'IP pool actions' }).click() + await page.getByRole('menuitem', { name: 'Delete' }).click() + await expect(page.getByRole('dialog', { name: 'Confirm delete' })).toBeVisible() + await page.getByRole('button', { name: 'Confirm' }).click() + + // get redirected back to the list after successful delete + await expect(page).toHaveURL('/system/networking/ip-pools') + await expect(page.getByRole('cell', { name: 'ip-pool-3' })).toBeHidden() +}) + test('IP pool create', async ({ page }) => { await page.goto('/system/networking/ip-pools') await expect(page.getByRole('cell', { name: 'another-pool' })).toBeHidden() From 6f2fb6cb3605011313cc2d6bce4a01a4b3a426aa Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 30 Aug 2024 10:40:33 -0700 Subject: [PATCH 4/8] Add test for IP Pool edit --- app/forms/ip-pool-edit.tsx | 1 + test/e2e/ip-pools.e2e.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx index 67fd99c933..cca6326118 100644 --- a/app/forms/ip-pool-edit.tsx +++ b/app/forms/ip-pool-edit.tsx @@ -39,6 +39,7 @@ export function EditIpPoolSideModalForm() { const editPool = useApiMutation('ipPoolUpdate', { onSuccess(_pool) { if (pool.name !== _pool.name) { + queryClient.invalidateQueries('ipPoolList') navigate(pb.ipPool({ pool: _pool.name })) } else { queryClient.invalidateQueries('ipPoolView') diff --git a/test/e2e/ip-pools.e2e.ts b/test/e2e/ip-pools.e2e.ts index a9beb7503c..df0de16b04 100644 --- a/test/e2e/ip-pools.e2e.ts +++ b/test/e2e/ip-pools.e2e.ts @@ -173,6 +173,23 @@ test('IP pool create', async ({ page }) => { }) }) +test('IP pool edit', async ({ page }) => { + await page.goto('/system/networking/ip-pools/ip-pool-3') + await page.getByRole('button', { name: 'IP pool actions' }).click() + await page.getByRole('menuitem', { name: 'Edit' }).click() + + const modal = page.getByRole('dialog', { name: 'Edit IP pool' }) + await expect(modal).toBeVisible() + + await page.getByRole('textbox', { name: 'Name' }).fill('updated-pool') + await page.getByRole('textbox', { name: 'Description' }).fill('an updated description') + await page.getByRole('button', { name: 'Update IP pool' }).click() + + await expect(modal).toBeHidden() + await expect(page).toHaveURL('/system/networking/ip-pools/updated-pool') + await expect(page.getByRole('heading', { name: 'updated-pool' })).toBeVisible() +}) + test('IP range validation and add', async ({ page }) => { await page.goto('/system/networking/ip-pools/ip-pool-2') From 393207c3209b726ef396c3f84ff83ed6d52bafcf Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 30 Aug 2024 11:41:05 -0700 Subject: [PATCH 5/8] Add comment; remove unnecessary navigate function call --- app/forms/ip-pool-edit.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx index cca6326118..24aae17885 100644 --- a/app/forms/ip-pool-edit.tsx +++ b/app/forms/ip-pool-edit.tsx @@ -40,10 +40,11 @@ export function EditIpPoolSideModalForm() { onSuccess(_pool) { if (pool.name !== _pool.name) { queryClient.invalidateQueries('ipPoolList') + // as the pool's name has changed, we need to navigate to an updated URL navigate(pb.ipPool({ pool: _pool.name })) } else { queryClient.invalidateQueries('ipPoolView') - navigate(pb.ipPool({ pool: pool.name })) + onDismiss() } addToast({ content: 'Your IP pool has been updated' }) }, From 22af2df889d2f899ff97c837fd070e332cce6dbe Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 30 Aug 2024 14:37:44 -0700 Subject: [PATCH 6/8] Clean up invalidations --- app/forms/ip-pool-edit.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx index 24aae17885..5b83f20ce1 100644 --- a/app/forms/ip-pool-edit.tsx +++ b/app/forms/ip-pool-edit.tsx @@ -35,6 +35,7 @@ export function EditIpPoolSideModalForm() { const { data: pool } = usePrefetchedApiQuery('ipPoolView', { path: poolSelector }) const form = useForm({ defaultValues: pool }) + const onDismiss = () => navigate(pb.ipPool({ pool: poolSelector.pool })) const editPool = useApiMutation('ipPoolUpdate', { onSuccess(_pool) { @@ -43,13 +44,11 @@ export function EditIpPoolSideModalForm() { // as the pool's name has changed, we need to navigate to an updated URL navigate(pb.ipPool({ pool: _pool.name })) } else { - queryClient.invalidateQueries('ipPoolView') onDismiss() } addToast({ content: 'Your IP pool has been updated' }) }, }) - const onDismiss = () => navigate(pb.ipPool({ pool: poolSelector.pool })) return ( Date: Fri, 30 Aug 2024 14:42:48 -0700 Subject: [PATCH 7/8] mutateAsync update --- app/pages/system/networking/IpPoolPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pages/system/networking/IpPoolPage.tsx b/app/pages/system/networking/IpPoolPage.tsx index 33b1f25d9f..a0b0350a47 100644 --- a/app/pages/system/networking/IpPoolPage.tsx +++ b/app/pages/system/networking/IpPoolPage.tsx @@ -78,7 +78,7 @@ export function IpPoolPage() { query, }) const navigate = useNavigate() - const deletePool = useApiMutation('ipPoolDelete', { + const { mutateAsync: deletePool } = useApiMutation('ipPoolDelete', { onSuccess() { apiQueryClient.invalidateQueries('ipPoolList') navigate(pb.ipPools()) @@ -97,7 +97,7 @@ export function IpPoolPage() { { label: 'Delete', onActivate: confirmDelete({ - doDelete: () => deletePool.mutateAsync({ path: { pool: pool.name } }), + doDelete: () => deletePool({ path: { pool: pool.name } }), label: pool.name, }), disabled: From ab54174ea47a3be99229098282edfdbd584a23fb Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 30 Aug 2024 15:03:04 -0700 Subject: [PATCH 8/8] Always invalidate ipPoolList on successful edit; add ipPoolView invalidation back in --- app/forms/ip-pool-edit.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx index 5b83f20ce1..2148ea4e72 100644 --- a/app/forms/ip-pool-edit.tsx +++ b/app/forms/ip-pool-edit.tsx @@ -39,11 +39,12 @@ export function EditIpPoolSideModalForm() { const editPool = useApiMutation('ipPoolUpdate', { onSuccess(_pool) { + queryClient.invalidateQueries('ipPoolList') if (pool.name !== _pool.name) { - queryClient.invalidateQueries('ipPoolList') // as the pool's name has changed, we need to navigate to an updated URL navigate(pb.ipPool({ pool: _pool.name })) } else { + queryClient.invalidateQueries('ipPoolView') onDismiss() } addToast({ content: 'Your IP pool has been updated' })