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="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>
)
}
47 changes: 31 additions & 16 deletions app/table/cells/IpPoolCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,48 @@
* 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 { useIsInSideModal } from '~/ui/lib/modal-context'

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,
},
}
)

/**
* Renders an IP pool name. In a table cell, clicking opens a side modal with
* pool details. Inside a side modal (detected via context) it falls back to
* plain text to avoid stacking a second side modal on top of the first.
*/
export const IpPoolCell = ({ ipPoolId }: { ipPoolId: string }) => {
const inSideModal = useIsInSideModal()
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 (inSideModal) 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 dialog.locator('footer').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