Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions app/components/IpPoolDetailSideModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 { 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
onDismiss: () => void
}

export function IpPoolDetailSideModal({ pool, onDismiss }: IpPoolDetailSideModalProps) {
return (
<ReadOnlySideModalForm
title="IP pool details"
onDismiss={onDismiss}
animate
subtitle={
<ResourceLabel>
<IpGlobal16Icon /> {pool.name}
</ResourceLabel>
}
>
<PropertiesTable>
<PropertiesTable.IdRow id={pool.id} />
<PropertiesTable.DescriptionRow description={pool.description} sideModal />
<PropertiesTable.Row label="IP version">
<IpVersionBadge ipVersion={pool.ipVersion} />
</PropertiesTable.Row>
<PropertiesTable.Row label="Pool type">
<Badge color="neutral">{pool.poolType}</Badge>
</PropertiesTable.Row>
<PropertiesTable.DateRow label="Created" date={pool.timeCreated} />
<PropertiesTable.DateRow label="Last Modified" date={pool.timeModified} />
</PropertiesTable>
<SideModalFormDocs docs={[docLinks.systemIpPools]} />
</ReadOnlySideModalForm>
)
}
2 changes: 1 addition & 1 deletion app/forms/floating-ip-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default function EditFloatingIpSideModalForm() {
<CopyableIp ip={floatingIp.ip} isLinked={false} />
</PropertiesTable.Row>
<PropertiesTable.Row label="IP Pool">
<IpPoolCell ipPoolId={floatingIp.ipPoolId} />
<IpPoolCell ipPoolId={floatingIp.ipPoolId} showPoolInfo={false} />
</PropertiesTable.Row>
<PropertiesTable.Row label="Instance">
<InstanceLink instanceId={floatingIp.instanceId} tab="networking" />
Expand Down
2 changes: 1 addition & 1 deletion app/pages/project/vpcs/internet-gateway-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export default function EditInternetGatewayForm() {
<PropertiesTable.Row label="Name">{gatewayIpPool.name}</PropertiesTable.Row>
<PropertiesTable.DescriptionRow description={gatewayIpPool.description} />
<PropertiesTable.Row label="IP Pool Name">
<IpPoolCell ipPoolId={gatewayIpPool.ipPoolId} />
<IpPoolCell ipPoolId={gatewayIpPool.ipPoolId} showPoolInfo={false} />
</PropertiesTable.Row>
</PropertiesTable>
))
Expand Down
46 changes: 30 additions & 16 deletions app/table/cells/IpPoolCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +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 <SkeletonCell />
// 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 <EmptyCell />
const pool = result.data
if (!showPoolInfo) return <>{pool.name}</>
return (
<Tooltip content={pool.description} placement="right">
<span>{pool.name}</span>
</Tooltip>
<>
<ButtonCell onClick={() => setShowDetail(true)}>{pool.name}</ButtonCell>
{showDetail && (
<IpPoolDetailSideModal pool={pool} onDismiss={() => setShowDetail(false)} />
)}
</>
)
}
17 changes: 17 additions & 0 deletions test/e2e/floating-ip-create.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Loading