diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx index befe790dcf..2148ea4e72 100644 --- a/app/forms/ip-pool-edit.tsx +++ b/app/forms/ip-pool-edit.tsx @@ -30,23 +30,27 @@ 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 onDismiss = () => navigate(pb.ipPool({ pool: poolSelector.pool })) + const editPool = useApiMutation('ipPoolUpdate', { onSuccess(_pool) { queryClient.invalidateQueries('ipPoolList') + if (pool.name !== _pool.name) { + // 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' }) - onDismiss() }, }) - const form = useForm({ defaultValues: pool }) - return ( [ + { + label: 'Edit', + onActivate() { + navigate(pb.ipPoolEdit(poolSelector)) + }, + }, + { + label: 'Delete', + onActivate: confirmDelete({ + doDelete: () => deletePool({ 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, ranges.items] + ) + 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' }} + /> } /> diff --git a/test/e2e/ip-pools.e2e.ts b/test/e2e/ip-pools.e2e.ts index 9470794898..df0de16b04 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() @@ -155,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')