Skip to content

Commit

Permalink
Move IP Pools edit form to view page (#2405)
Browse files Browse the repository at this point in the history
  • Loading branch information
charliepark authored Aug 30, 2024
1 parent bdb55b8 commit e8175d3
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 21 deletions.
16 changes: 10 additions & 6 deletions app/forms/ip-pool-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<SideModalForm
form={form}
Expand Down
57 changes: 49 additions & 8 deletions app/pages/system/networking/IpPoolPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { createColumnHelper } from '@tanstack/react-table'
import { useCallback, useMemo, useState } from 'react'
import { Outlet, type LoaderFunctionArgs } from 'react-router-dom'
import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
Expand All @@ -26,9 +26,11 @@ import { CapacityBar } from '~/components/CapacityBar'
import { DocsPopover } from '~/components/DocsPopover'
import { ComboboxField } from '~/components/form/fields/ComboboxField'
import { HL } from '~/components/HL'
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import { QueryParamTabs } from '~/components/QueryParamTabs'
import { getIpPoolSelector, useForm, useIpPoolSelector } from '~/hooks'
import { confirmAction } from '~/stores/confirm-action'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import { DefaultPoolCell } from '~/table/cells/DefaultPoolCell'
import { SkeletonCell } from '~/table/cells/EmptyCell'
Expand All @@ -46,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 }),
Expand All @@ -70,16 +73,54 @@ 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 { mutateAsync: deletePool } = useApiMutation('ipPoolDelete', {
onSuccess() {
apiQueryClient.invalidateQueries('ipPoolList')
navigate(pb.ipPools())
addToast({ content: 'IP pool deleted' })
},
})

const actions = useMemo(
() => [
{
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 (
<>
<PageHeader>
<PageTitle icon={<IpGlobal24Icon />}>{pool.name}</PageTitle>
<DocsPopover
heading="IP pools"
icon={<IpGlobal16Icon />}
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]}
/>
<div className="inline-flex gap-2">
<DocsPopover
heading="IP pools"
icon={<IpGlobal16Icon />}
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]}
/>
<MoreActionsMenu label="IP pool actions" actions={actions} />
</div>
</PageHeader>
<UtilizationBars />
<QueryParamTabs className="full-width" defaultValue="ranges">
Expand Down
2 changes: 2 additions & 0 deletions app/pages/system/networking/IpPoolsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -79,6 +80,7 @@ export function IpPoolsPage() {
const deletePool = useApiMutation('ipPoolDelete', {
onSuccess() {
apiQueryClient.invalidateQueries('ipPoolList')
addToast({ content: 'IP pool deleted' })
},
})

Expand Down
12 changes: 6 additions & 6 deletions app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,6 @@ export const routes = createRoutesFromElements(
>
<Route path="ip-pools" element={null} />
<Route path="ip-pools-new" element={<CreateIpPoolSideModalForm />} />
<Route
path="ip-pools/:pool/edit"
element={<EditIpPoolSideModalForm />}
loader={EditIpPoolSideModalForm.loader}
handle={{ crumb: 'Edit IP pool' }}
/>
</Route>
</Route>
<Route path="networking/ip-pools" handle={{ crumb: 'IP pools' }}>
Expand All @@ -213,6 +207,12 @@ export const routes = createRoutesFromElements(
loader={IpPoolPage.loader}
handle={{ crumb: poolCrumb }}
>
<Route
path="edit"
element={<EditIpPoolSideModalForm />}
loader={EditIpPoolSideModalForm.loader}
handle={{ crumb: 'Edit IP pool' }}
/>
<Route path="ranges-add" element={<IpPoolAddRangeSideModalForm />} />
</Route>
</Route>
Expand Down
37 changes: 36 additions & 1 deletion test/e2e/ip-pools.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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')

Expand Down

0 comments on commit e8175d3

Please sign in to comment.