From 7ee480b66cd8cfe733a80438740e6d14e3e9a127 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 27 Mar 2026 16:51:33 -0700 Subject: [PATCH 1/4] Add IP pool sidebar --- app/components/IpPoolDetailSideModal.tsx | 48 +++++++++++++++++++ app/forms/floating-ip-edit.tsx | 2 +- .../project/vpcs/internet-gateway-edit.tsx | 2 +- app/table/cells/IpPoolCell.tsx | 48 ++++++++++++------- test/e2e/floating-ip-create.e2e.ts | 17 +++++++ 5 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 app/components/IpPoolDetailSideModal.tsx diff --git a/app/components/IpPoolDetailSideModal.tsx b/app/components/IpPoolDetailSideModal.tsx new file mode 100644 index 000000000..5b07a21c7 --- /dev/null +++ b/app/components/IpPoolDetailSideModal.tsx @@ -0,0 +1,48 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { type IpPool } from '@oxide/api' +import { IpGlobal16Icon } from '@oxide/design-system/icons/react' +import { Badge } from '@oxide/design-system/ui' + +import { ReadOnlySideModalForm } from '~/components/form/ReadOnlySideModalForm' +import { IpVersionBadge } from '~/components/IpVersionBadge' +import { PropertiesTable } from '~/ui/lib/PropertiesTable' +import { ResourceLabel } from '~/ui/lib/SideModal' + +type IpPoolDetailSideModalProps = { + pool: IpPool + onDismiss: () => void +} + +export function IpPoolDetailSideModal({ pool, onDismiss }: IpPoolDetailSideModalProps) { + return ( + + {pool.name} + + } + > + + + + + + + + {pool.poolType} + + + + + + ) +} diff --git a/app/forms/floating-ip-edit.tsx b/app/forms/floating-ip-edit.tsx index 960f2e71e..02b522ed7 100644 --- a/app/forms/floating-ip-edit.tsx +++ b/app/forms/floating-ip-edit.tsx @@ -107,7 +107,7 @@ export default function EditFloatingIpSideModalForm() { - + diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx index 5d5ac415d..19e382107 100644 --- a/app/pages/project/vpcs/internet-gateway-edit.tsx +++ b/app/pages/project/vpcs/internet-gateway-edit.tsx @@ -173,7 +173,7 @@ export default function EditInternetGatewayForm() { {gatewayIpPool.name} - + )) diff --git a/app/table/cells/IpPoolCell.tsx b/app/table/cells/IpPoolCell.tsx index fc234191b..155910c83 100644 --- a/app/table/cells/IpPoolCell.tsx +++ b/app/table/cells/IpPoolCell.tsx @@ -1,3 +1,4 @@ +import { useQuery } from '@tanstack/react-query' /* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -5,34 +6,47 @@ * * Copyright Oxide Computer Company */ -import { useQuery } from '@tanstack/react-query' +import { useState } from 'react' import { api, qErrorsAllowed } from '~/api' -import { Tooltip } from '~/ui/lib/Tooltip' +import { IpPoolDetailSideModal } from '~/components/IpPoolDetailSideModal' import { EmptyCell, SkeletonCell } from './EmptyCell' +import { ButtonCell } from './LinkCell' -export const IpPoolCell = ({ ipPoolId }: { ipPoolId: string }) => { - const { data: result } = useQuery( - qErrorsAllowed( - api.ipPoolView, - { path: { pool: ipPoolId } }, - { - errorsExpected: { - explanation: 'the referenced IP pool may have been deleted.', - statusCode: 404, - }, - } - ) +const ipPoolQuery = (ipPoolId: string) => + qErrorsAllowed( + api.ipPoolView, + { path: { pool: ipPoolId } }, + { + errorsExpected: { + explanation: 'the referenced IP pool may have been deleted.', + statusCode: 404, + }, + } ) + +type IpPoolCellProps = { + ipPoolId: string + /** Show the IP pool detail sidebar on click. Defaults to true. Pass false to render as plain text. */ + showPoolInfo?: boolean +} + +export const IpPoolCell = ({ ipPoolId, showPoolInfo = true }: IpPoolCellProps) => { + const [showDetail, setShowDetail] = useState(false) + const { data: result } = useQuery(ipPoolQuery(ipPoolId)) if (!result) return // Defensive: the error case should never happen in practice. It should not be // possible for a resource to reference a pool without that pool existing. if (result.type === 'error') return const pool = result.data + if (!showPoolInfo) return <>{pool.name} return ( - - {pool.name} - + <> + setShowDetail(true)}>{pool.name} + {showDetail && ( + setShowDetail(false)} /> + )} + ) } diff --git a/test/e2e/floating-ip-create.e2e.ts b/test/e2e/floating-ip-create.e2e.ts index c33bfeef4..44eae2767 100644 --- a/test/e2e/floating-ip-create.e2e.ts +++ b/test/e2e/floating-ip-create.e2e.ts @@ -50,6 +50,23 @@ test('can create a floating IP', async ({ page }) => { }) }) +test('can view IP pool details from floating IP table', async ({ page }) => { + await page.goto(floatingIpsPage) + + // cola-float is in ip-pool-1; click the pool cell to open the detail modal + const row = page.getByRole('row', { name: /cola-float/ }) + await row.getByRole('button', { name: 'ip-pool-1' }).click() + + const dialog = page.getByRole('dialog', { name: 'IP pool details' }) + await expect(dialog).toBeVisible() + await expect(dialog.getByText('public IPs')).toBeVisible() + await expect(dialog.getByText('v4')).toBeVisible() + await expect(dialog.getByText('unicast')).toBeVisible() + + await page.getByRole('contentinfo').getByRole('button', { name: 'Close' }).click() + await expect(dialog).toBeHidden() +}) + test('can detach and attach a floating IP', async ({ page }) => { // check floating IP is visible on instance detail await page.goto('/projects/mock-project/instances/db1') From e33b2459cdcab46a6b358895df5ab6481e4ddbd0 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 27 Mar 2026 16:54:13 -0700 Subject: [PATCH 2/4] Add relevant docs section --- app/components/IpPoolDetailSideModal.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/components/IpPoolDetailSideModal.tsx b/app/components/IpPoolDetailSideModal.tsx index 5b07a21c7..90f044b94 100644 --- a/app/components/IpPoolDetailSideModal.tsx +++ b/app/components/IpPoolDetailSideModal.tsx @@ -11,8 +11,10 @@ import { Badge } from '@oxide/design-system/ui' import { ReadOnlySideModalForm } from '~/components/form/ReadOnlySideModalForm' import { IpVersionBadge } from '~/components/IpVersionBadge' +import { SideModalFormDocs } from '~/ui/lib/ModalLinks' import { PropertiesTable } from '~/ui/lib/PropertiesTable' import { ResourceLabel } from '~/ui/lib/SideModal' +import { docLinks } from '~/util/links' type IpPoolDetailSideModalProps = { pool: IpPool @@ -43,6 +45,7 @@ export function IpPoolDetailSideModal({ pool, onDismiss }: IpPoolDetailSideModal + ) } From 765d2507c9e8c5d461a8439d089a253001d1d86b Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 27 Mar 2026 17:17:33 -0700 Subject: [PATCH 3/4] linter fix --- app/table/cells/IpPoolCell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/table/cells/IpPoolCell.tsx b/app/table/cells/IpPoolCell.tsx index 155910c83..7a383e178 100644 --- a/app/table/cells/IpPoolCell.tsx +++ b/app/table/cells/IpPoolCell.tsx @@ -1,4 +1,3 @@ -import { useQuery } from '@tanstack/react-query' /* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -6,6 +5,7 @@ import { useQuery } from '@tanstack/react-query' * * Copyright Oxide Computer Company */ +import { useQuery } from '@tanstack/react-query' import { useState } from 'react' import { api, qErrorsAllowed } from '~/api' From 349fe40b9b20db7c761928f80d8da92dc1ec31b3 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 6 Apr 2026 15:51:57 +0200 Subject: [PATCH 4/4] Update label: Pool type -> Type --- app/components/IpPoolDetailSideModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/IpPoolDetailSideModal.tsx b/app/components/IpPoolDetailSideModal.tsx index 90f044b94..7b74063fb 100644 --- a/app/components/IpPoolDetailSideModal.tsx +++ b/app/components/IpPoolDetailSideModal.tsx @@ -39,7 +39,7 @@ export function IpPoolDetailSideModal({ pool, onDismiss }: IpPoolDetailSideModal - + {pool.poolType}