Skip to content
56 changes: 43 additions & 13 deletions app/pages/system/networking/IpPoolPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'

import {
Expand All @@ -27,6 +27,7 @@ import { IpGlobal16Icon, IpGlobal24Icon } from '@oxide/design-system/icons/react
import { Badge } from '@oxide/design-system/ui'

import { DocsPopover } from '~/components/DocsPopover'
import { CheckboxField } from '~/components/form/fields/CheckboxField'
import { ComboboxField } from '~/components/form/fields/ComboboxField'
import { HL } from '~/components/HL'
import { IpVersionBadge } from '~/components/IpVersionBadge'
Expand All @@ -35,6 +36,10 @@ import { QueryParamTabs } from '~/components/QueryParamTabs'
import { makeCrumb } from '~/hooks/use-crumbs'
import { getIpPoolSelector, useIpPoolSelector } from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import {
ReplacedDefaultNote,
useLinkIpPoolSiloFlow,
} from '~/pages/system/useLinkPoolSiloFlow'
import { confirmAction } from '~/stores/confirm-action'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
Expand Down Expand Up @@ -462,34 +467,50 @@ function LinkedSilosTable() {

type LinkSiloFormValues = {
silo: string | undefined
isDefault: boolean
}

const defaultValues: LinkSiloFormValues = { silo: undefined }
const defaultValues: LinkSiloFormValues = { silo: undefined, isDefault: false }

function LinkSiloModal({ onDismiss }: { onDismiss: () => void }) {
const { pool } = useIpPoolSelector()
const { data: poolData } = usePrefetchedQuery(ipPoolView({ pool }))
const { control, handleSubmit } = useForm({ defaultValues })

const linkSilo = useApiMutation(api.systemIpPoolSiloLink, {
onSuccess() {
queryClient.invalidateEndpoint('systemIpPoolSiloList')
onDismiss()
},
onError(err) {
addToast({ title: 'Could not link silo', content: err.message, variant: 'error' })
},
const { linkAndMaybePromote, isPending } = useLinkIpPoolSiloFlow({
linkErrorTitle: 'Could not link silo',
promoteErrorTitle: 'Silo linked, but pool not set as default',
})

function onSubmit({ silo }: LinkSiloFormValues) {
async function onSubmit({ silo, isDefault }: LinkSiloFormValues) {
if (!silo) return // can't happen, silo is required
linkSilo.mutate({ path: { pool }, body: { silo, isDefault: false } })
const linked = await linkAndMaybePromote({ pool, silo, isDefault })
if (!linked) return
onDismiss()
}

const linkedSilos = useQuery(
q(api.systemIpPoolSiloList, { path: { pool }, query: { limit: ALL_ISH } })
)
const allSilos = useQuery(q(api.siloList, { query: { limit: ALL_ISH } }))

// The pool is fixed here, so its version+type fix the default slot. To warn
// that linking as default would replace the selected silo's current default
// of that slot, fetch that silo's pools once a silo is picked.
const selectedSilo = useWatch({ control, name: 'silo' })
const selectedSiloPools = useQuery(
getListQFn(
api.siloIpPoolList,
// silo non-null asserted because the query is disabled until one is picked
{ path: { silo: selectedSilo! }, query: { limit: ALL_ISH } },
{ enabled: !!selectedSilo }
).optionsFn()
)
const replacedDefault = selectedSiloPools.data?.items.find(
(p) =>
p.isDefault && p.ipVersion === poolData.ipVersion && p.poolType === poolData.poolType
)?.name

// in order to get the list of remaining unlinked silos, we have to get the
// list of all silos and remove the already linked ones

Expand Down Expand Up @@ -532,13 +553,22 @@ function LinkSiloModal({ onDismiss }: { onDismiss: () => void }) {
required
control={control}
/>

<CheckboxField name="isDefault" control={control}>
{`Make default IP${poolData.ipVersion} ${poolData.poolType} pool for silo`}
{replacedDefault && (
<ReplacedDefaultNote>
Replaces {replacedDefault}, which stays linked
</ReplacedDefaultNote>
)}
</CheckboxField>
</form>
</Modal.Section>
</Modal.Body>
<Modal.Footer
onDismiss={onDismiss}
onAction={handleSubmit(onSubmit)}
actionLoading={linkSilo.isPending}
actionLoading={isPending}
actionText="Link"
/>
</Modal>
Expand Down
58 changes: 44 additions & 14 deletions app/pages/system/networking/SubnetPoolPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'

import {
Expand All @@ -27,6 +27,7 @@ import { Subnet16Icon, Subnet24Icon } from '@oxide/design-system/icons/react'
import { Badge } from '@oxide/design-system/ui'

import { DocsPopover } from '~/components/DocsPopover'
import { CheckboxField } from '~/components/form/fields/CheckboxField'
import { ComboboxField } from '~/components/form/fields/ComboboxField'
import { HL } from '~/components/HL'
import { IpVersionBadge } from '~/components/IpVersionBadge'
Expand All @@ -35,6 +36,10 @@ import { QueryParamTabs } from '~/components/QueryParamTabs'
import { makeCrumb } from '~/hooks/use-crumbs'
import { getSubnetPoolSelector, useSubnetPoolSelector } from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import {
ReplacedDefaultNote,
useLinkSubnetPoolSiloFlow,
} from '~/pages/system/useLinkPoolSiloFlow'
import { confirmAction } from '~/stores/confirm-action'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
Expand Down Expand Up @@ -448,27 +453,27 @@ function LinkedSilosTable() {

type LinkSiloFormValues = {
silo: string | undefined
isDefault: boolean
}

const defaultValues: LinkSiloFormValues = { silo: undefined }
const defaultValues: LinkSiloFormValues = { silo: undefined, isDefault: false }

function LinkSiloModal({ onDismiss }: { onDismiss: () => void }) {
const { subnetPool } = useSubnetPoolSelector()
const poolSelector = useSubnetPoolSelector()
const { subnetPool } = poolSelector
const { data: poolData } = usePrefetchedQuery(subnetPoolView(poolSelector))
const { control, handleSubmit } = useForm({ defaultValues })

const linkSilo = useApiMutation(api.systemSubnetPoolSiloLink, {
onSuccess() {
queryClient.invalidateEndpoint('systemSubnetPoolSiloList')
onDismiss()
},
onError(err) {
addToast({ title: 'Could not link silo', content: err.message, variant: 'error' })
},
const { linkAndMaybePromote, isPending } = useLinkSubnetPoolSiloFlow({
linkErrorTitle: 'Could not link silo',
promoteErrorTitle: 'Silo linked, but pool not set as default',
})

function onSubmit({ silo }: LinkSiloFormValues) {
async function onSubmit({ silo, isDefault }: LinkSiloFormValues) {
if (!silo) return
linkSilo.mutate({ path: { pool: subnetPool }, body: { silo, isDefault: false } })
const linked = await linkAndMaybePromote({ pool: subnetPool, silo, isDefault })
if (!linked) return
onDismiss()
}

const linkedSilos = useQuery(
Expand All @@ -479,6 +484,22 @@ function LinkSiloModal({ onDismiss }: { onDismiss: () => void }) {
)
const allSilos = useQuery(q(api.siloList, { query: { limit: ALL_ISH } }))

// The pool is fixed here, so its version fixes the default slot. To warn that
// linking as default would replace the selected silo's current default of that
// version, fetch that silo's subnet pools once a silo is picked.
const selectedSilo = useWatch({ control, name: 'silo' })
const selectedSiloPools = useQuery(
getListQFn(
api.siloSubnetPoolList,
// silo non-null asserted because the query is disabled until one is picked
{ path: { silo: selectedSilo! }, query: { limit: ALL_ISH } },
{ enabled: !!selectedSilo }
).optionsFn()
)
const replacedDefault = selectedSiloPools.data?.items.find(
(p) => p.isDefault && p.ipVersion === poolData.ipVersion
)?.name

const linkedSiloIds = useMemo(
() =>
linkedSilos.data ? new Set(linkedSilos.data.items.map((s) => s.siloId)) : undefined,
Expand Down Expand Up @@ -518,13 +539,22 @@ function LinkSiloModal({ onDismiss }: { onDismiss: () => void }) {
required
control={control}
/>

<CheckboxField name="isDefault" control={control}>
{`Make default IP${poolData.ipVersion} subnet pool for silo`}
{replacedDefault && (
<ReplacedDefaultNote>
Replaces {replacedDefault}, which stays linked
</ReplacedDefaultNote>
)}
</CheckboxField>
</form>
</Modal.Section>
</Modal.Body>
<Modal.Footer
onDismiss={onDismiss}
onAction={handleSubmit(onSubmit)}
actionLoading={linkSilo.isPending}
actionLoading={isPending}
actionText="Link"
/>
</Modal>
Expand Down
56 changes: 41 additions & 15 deletions app/pages/system/silos/SiloIpPoolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -24,11 +24,16 @@ 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'
import { makeCrumb } from '~/hooks/use-crumbs'
import { getSiloSelector, useSiloSelector } from '~/hooks/use-params'
import {
ReplacedDefaultNote,
useLinkIpPoolSiloFlow,
} from '~/pages/system/useLinkPoolSiloFlow'
import { confirmAction } from '~/stores/confirm-action'
import { addToast } from '~/stores/toast'
import { LinkCell } from '~/table/cells/LinkCell'
Expand Down Expand Up @@ -263,33 +268,43 @@ 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()
const { control, handleSubmit } = useForm({ defaultValues })

const linkPool = useApiMutation(api.systemIpPoolSiloLink, {
onSuccess() {
queryClient.invalidateEndpoint('siloIpPoolList')
queryClient.invalidateEndpoint('systemIpPoolSiloList')
onDismiss()
},
onError(err) {
addToast({ title: 'Could not link pool', content: err.message, variant: 'error' })
},
const { linkAndMaybePromote, isPending } = useLinkIpPoolSiloFlow({
linkErrorTitle: 'Could not link pool',
promoteErrorTitle: 'Pool linked, but not set as default',
})

function onSubmit({ pool }: LinkPoolFormValues) {
if (!pool) return // can't happen, silo is required
linkPool.mutate({ path: { pool }, body: { silo, isDefault: false } })
async function onSubmit({ pool, isDefault }: LinkPoolFormValues) {
if (!pool) return // can't happen, pool is required
const linked = await linkAndMaybePromote({ pool, silo, isDefault })
if (!linked) return
onDismiss()
}

const allLinkedPools = useQuery(allSiloPoolsQuery(silo).optionsFn())
const allPools = useQuery(allPoolsQuery.optionsFn())

// The selected pool's version+type determine which default slot the checkbox
// fills, and whether linking as default would replace an existing default.
const selectedPoolName = useWatch({ control, name: 'pool' })
const selectedPool = allPools.data?.items.find((p) => p.name === selectedPoolName)
const replacedDefault =
selectedPool &&
allLinkedPools.data?.items.find(
(p) =>
p.isDefault &&
p.ipVersion === selectedPool.ipVersion &&
p.poolType === selectedPool.poolType
)?.name

// 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

Expand Down Expand Up @@ -334,14 +349,25 @@ function LinkPoolModal({ onDismiss }: { onDismiss: () => void }) {
required
control={control}
/>

<CheckboxField name="isDefault" control={control}>
{selectedPool
? `Make default IP${selectedPool.ipVersion} ${selectedPool.poolType} pool for silo`
: 'Make default pool for silo'}
{replacedDefault && (
<ReplacedDefaultNote>
Replaces {replacedDefault}, which stays linked
</ReplacedDefaultNote>
)}
</CheckboxField>
Comment thread
sudomateo marked this conversation as resolved.
</form>
</Modal.Section>
</Modal.Body>
<Modal.Footer
onDismiss={onDismiss}
onAction={handleSubmit(onSubmit)}
actionText="Link"
actionLoading={linkPool.isPending}
actionLoading={isPending}
/>
</Modal>
)
Expand Down
Loading
Loading