From 4b2a4839da370f76c3deba9e35942e8fdc3d772c Mon Sep 17 00:00:00 2001 From: Matthew Sanabria Date: Sun, 28 Jun 2026 22:22:27 -0400 Subject: [PATCH] feat: checkbox to make pool default when linking to silo Added a checkbox to the IP pool and subnet pool linking modals to make the pool the default pool for the silo. Closes https://github.com/oxidecomputer/customer-support/issues/413. Amp-Thread: https://ampcode.com/threads/T-019f1129-f325-73e9-96da-94a8a24fa617 --- app/pages/system/silos/SiloIpPoolsTab.tsx | 20 +++++-- app/pages/system/silos/SiloSubnetPoolsTab.tsx | 20 +++++-- test/e2e/silos.e2e.ts | 54 +++++++++++++++++-- 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/app/pages/system/silos/SiloIpPoolsTab.tsx b/app/pages/system/silos/SiloIpPoolsTab.tsx index 7750d816c..7ff0193fc 100644 --- a/app/pages/system/silos/SiloIpPoolsTab.tsx +++ b/app/pages/system/silos/SiloIpPoolsTab.tsx @@ -9,7 +9,7 @@ import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo, useState } from 'react' -import { useForm } from 'react-hook-form' +import { useForm, useWatch } from 'react-hook-form' import { type LoaderFunctionArgs } from 'react-router' import { @@ -24,6 +24,7 @@ import { import { Networking24Icon } from '@oxide/design-system/icons/react' import { Badge } from '@oxide/design-system/ui' +import { CheckboxField } from '~/components/form/fields/CheckboxField' import { ComboboxField } from '~/components/form/fields/ComboboxField' import { HL } from '~/components/HL' import { IpVersionBadge } from '~/components/IpVersionBadge' @@ -263,9 +264,10 @@ export const handle = makeCrumb('IP Pools') type LinkPoolFormValues = { pool: string | undefined + isDefault: boolean } -const defaultValues: LinkPoolFormValues = { pool: undefined } +const defaultValues: LinkPoolFormValues = { pool: undefined, isDefault: false } function LinkPoolModal({ onDismiss }: { onDismiss: () => void }) { const { silo } = useSiloSelector() @@ -282,14 +284,18 @@ function LinkPoolModal({ onDismiss }: { onDismiss: () => void }) { }, }) - function onSubmit({ pool }: LinkPoolFormValues) { + function onSubmit({ pool, isDefault }: LinkPoolFormValues) { if (!pool) return // can't happen, silo is required - linkPool.mutate({ path: { pool }, body: { silo, isDefault: false } }) + linkPool.mutate({ path: { pool }, body: { silo, isDefault } }) } const allLinkedPools = useQuery(allSiloPoolsQuery(silo).optionsFn()) const allPools = useQuery(allPoolsQuery.optionsFn()) + // Fetch the selected pool details so we can update the checkbox label. + const selectedPoolName = useWatch({ control, name: 'pool' }) + const selectedPool = allPools.data?.items.find((p) => p.name === selectedPoolName) + // in order to get the list of remaining unlinked pools, we have to get the // list of all pools and remove the already linked ones @@ -334,6 +340,12 @@ function LinkPoolModal({ onDismiss }: { onDismiss: () => void }) { required control={control} /> + + + {selectedPool + ? `Make default IP${selectedPool.ipVersion} ${selectedPool.poolType} pool for silo` + : 'Make default pool for silo'} + diff --git a/app/pages/system/silos/SiloSubnetPoolsTab.tsx b/app/pages/system/silos/SiloSubnetPoolsTab.tsx index 349d42fb4..7820f431f 100644 --- a/app/pages/system/silos/SiloSubnetPoolsTab.tsx +++ b/app/pages/system/silos/SiloSubnetPoolsTab.tsx @@ -9,7 +9,7 @@ import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo, useState } from 'react' -import { useForm } from 'react-hook-form' +import { useForm, useWatch } from 'react-hook-form' import { type LoaderFunctionArgs } from 'react-router' import { @@ -24,6 +24,7 @@ import { import { Networking24Icon } from '@oxide/design-system/icons/react' import { Badge } from '@oxide/design-system/ui' +import { CheckboxField } from '~/components/form/fields/CheckboxField' import { ComboboxField } from '~/components/form/fields/ComboboxField' import { HL } from '~/components/HL' import { IpVersionBadge } from '~/components/IpVersionBadge' @@ -252,9 +253,10 @@ export const handle = makeCrumb('Subnet Pools') type LinkPoolFormValues = { pool: string | undefined + isDefault: boolean } -const defaultValues: LinkPoolFormValues = { pool: undefined } +const defaultValues: LinkPoolFormValues = { pool: undefined, isDefault: false } function LinkPoolModal({ onDismiss }: { onDismiss: () => void }) { const { silo } = useSiloSelector() @@ -271,14 +273,18 @@ function LinkPoolModal({ onDismiss }: { onDismiss: () => void }) { }, }) - function onSubmit({ pool }: LinkPoolFormValues) { + function onSubmit({ pool, isDefault }: LinkPoolFormValues) { if (!pool) return - linkPool.mutate({ path: { pool }, body: { silo, isDefault: false } }) + linkPool.mutate({ path: { pool }, body: { silo, isDefault } }) } const allLinkedPools = useQuery(allSiloPoolsQuery(silo).optionsFn()) const allPools = useQuery(allPoolsQuery.optionsFn()) + // Fetch the selected pool details so we can update the checkbox label. + const selectedPoolName = useWatch({ control, name: 'pool' }) + const selectedPool = allPools.data?.items.find((p) => p.name === selectedPoolName) + const linkedPoolIds = useMemo( () => allLinkedPools.data ? new Set(allLinkedPools.data.items.map((p) => p.id)) : undefined, @@ -320,6 +326,12 @@ function LinkPoolModal({ onDismiss }: { onDismiss: () => void }) { required control={control} /> + + + {selectedPool + ? `Make default IP${selectedPool.ipVersion} subnet pool for silo` + : 'Make default subnet pool for silo'} + diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts index 72192517f..d97afe758 100644 --- a/test/e2e/silos.e2e.ts +++ b/test/e2e/silos.e2e.ts @@ -370,14 +370,62 @@ test('Silo IP pools link pool', async ({ page }) => { await page.getByPlaceholder('Select a pool').fill('x') await expect(page.getByText('No items match')).toBeVisible() - // select silo in combobox and click link + // before a pool is selected, the default checkbox label is generic + await expect( + page.getByRole('checkbox', { name: 'Make default pool for silo' }) + ).toBeVisible() + + // select pool in combobox await page.getByPlaceholder('Select a pool').fill('ip-pool') await page.getByRole('option', { name: 'ip-pool-3' }).click() + + // checkbox label now reflects the selected pool's version and type + const defaultCheckbox = page.getByRole('checkbox', { + name: 'Make default IPv4 unicast pool for silo', + }) + await expect(defaultCheckbox).toBeVisible() + await defaultCheckbox.check() + + await modal.getByRole('button', { name: 'Link' }).click() + + // modal closes and we see the pool linked as default in the table + await expect(modal).toBeHidden() + await expectRowVisible(table, { name: 'ip-pool-3default', Version: 'v4' }) +}) + +test('Silo subnet pools link pool', async ({ page }) => { + await page.goto('/system/silos/maze-war/subnet-pools') + + const table = page.getByRole('table') + await expectRowVisible(table, { name: 'default-v4-subnet-pooldefault', Version: 'v4' }) + + const modal = page.getByRole('dialog', { name: 'Link pool' }) + await expect(modal).toBeHidden() + + await page.getByRole('button', { name: 'Link pool' }).click() + await expect(modal).toBeVisible() + + // before a pool is selected, the default checkbox label is generic + await expect( + page.getByRole('checkbox', { name: 'Make default subnet pool for silo' }) + ).toBeVisible() + + // select pool in combobox + await page.getByPlaceholder('Select a pool').fill('myriad') + await page.getByRole('option', { name: 'myriad-v4-subnet-pool' }).click() + + // checkbox label now reflects the selected pool's version + const defaultCheckbox = page.getByRole('checkbox', { + name: 'Make default IPv4 subnet pool for silo', + }) + await expect(defaultCheckbox).toBeVisible() + await defaultCheckbox.check() + await modal.getByRole('button', { name: 'Link' }).click() - // modal closes and we see the thing in the table + // modal closes and we see the pool linked as default in the table await expect(modal).toBeHidden() - await expectRowVisible(table, { name: 'ip-pool-3', Version: 'v4' }) + await expectRowVisible(table, { name: 'myriad-v4-subnet-pooldefault', Version: 'v4' }) }) // just a convenient form to test this with because it's tall