diff --git a/app/components/ExternalIps.tsx b/app/components/ExternalIps.tsx index eb9e036932..f0a081666e 100644 --- a/app/components/ExternalIps.tsx +++ b/app/components/ExternalIps.tsx @@ -6,13 +6,22 @@ * Copyright Oxide Computer Company */ -import { useApiQuery } from '@oxide/api' +import { Link } from 'react-router' +import * as R from 'remeda' + +import { useApiQuery, type ExternalIp } from '@oxide/api' import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell' import { CopyableIp } from '~/ui/lib/CopyableIp' +import { Slash } from '~/ui/lib/Slash' import { intersperse } from '~/util/array' +import { pb } from '~/util/path-builder' import type * as PP from '~/util/path-params' +/** Move ephemeral IP (if present) to the end of the list of external IPs */ +export const orderIps = (ips: ExternalIp[]) => + R.sortBy(ips, (a) => (a.kind === 'ephemeral' ? 1 : -1)) + export function ExternalIps({ project, instance }: PP.Instance) { const { data, isPending } = useApiQuery('instanceExternalIpList', { path: { instance }, @@ -22,11 +31,27 @@ export function ExternalIps({ project, instance }: PP.Instance) { const ips = data?.items if (!ips || ips.length === 0) return + const orderedIps = orderIps(ips) + const ipsToShow = orderedIps.slice(0, 2) + const overflowCount = orderedIps.length - ipsToShow.length + + // create a list of CopyableIp components + const links = ipsToShow.map((eip) => ) + return ( -
- {intersperse( - ips.map((eip) => ), - / +
+ {intersperse(links, )} + {/* if there are more than 2 ips, add a link to the instance networking page */} + {overflowCount > 0 && ( + <> + + + … + + )}
) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 43e99b472f..de416e3242 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -24,6 +24,7 @@ import { IpGlobal24Icon, Networking24Icon } from '@oxide/design-system/icons/rea import { AttachEphemeralIpModal } from '~/components/AttachEphemeralIpModal' import { AttachFloatingIpModal } from '~/components/AttachFloatingIpModal' +import { orderIps } from '~/components/ExternalIps' import { HL } from '~/components/HL' import { ListPlusCell } from '~/components/ListPlusCell' import { CreateNetworkInterfaceForm } from '~/forms/network-interface-create' @@ -371,7 +372,7 @@ export function Component() { const ipTableInstance = useReactTable({ columns: useColsWithActions(staticIpCols, makeIpActions), - data: eips?.items || [], + data: useMemo(() => orderIps(eips.items), [eips]), getCoreRowModel: getCoreRowModel(), }) diff --git a/app/pages/settings/ProfilePage.tsx b/app/pages/settings/ProfilePage.tsx index 8638c24c91..faec440179 100644 --- a/app/pages/settings/ProfilePage.tsx +++ b/app/pages/settings/ProfilePage.tsx @@ -45,7 +45,7 @@ export function ProfilePage() { {me.displayName} {me.id} - + diff --git a/app/ui/lib/CopyableIp.tsx b/app/ui/lib/CopyableIp.tsx index 61201b155b..6c0f522d81 100644 --- a/app/ui/lib/CopyableIp.tsx +++ b/app/ui/lib/CopyableIp.tsx @@ -8,7 +8,7 @@ import { CopyToClipboard } from '~/ui/lib/CopyToClipboard' export const CopyableIp = ({ ip, isLinked = true }: { ip: string; isLinked?: boolean }) => ( - + {isLinked ? ( ( - / +import cn from 'classnames' + +export const Slash = ({ className }: { className?: string }) => ( + + / + ) diff --git a/app/ui/lib/Truncate.tsx b/app/ui/lib/Truncate.tsx index 92792b9813..b4dd18b464 100644 --- a/app/ui/lib/Truncate.tsx +++ b/app/ui/lib/Truncate.tsx @@ -33,7 +33,7 @@ export const Truncate = ({ // Only use the tooltip if the text is longer than maxLength return ( // overflow-hidden required to make inner truncate work -
+
{truncate(text, maxLength, position)} diff --git a/test/e2e/instance-networking.e2e.ts b/test/e2e/instance-networking.e2e.ts index f74b1b91b3..b7cd15cbe4 100644 --- a/test/e2e/instance-networking.e2e.ts +++ b/test/e2e/instance-networking.e2e.ts @@ -122,6 +122,8 @@ test('Instance networking tab — floating IPs', async ({ page }) => { await expectRowVisible(externalIpTable, { ip: '123.4.56.0', Kind: 'ephemeral' }) await expectRowVisible(externalIpTable, { ip: '123.4.56.5', Kind: 'floating' }) + await expect(page.getByText('external IPs123.4.56.5/123.4.56.0')).toBeVisible() + // Attach a new external IP await attachFloatingIpButton.click() await expectVisible(page, ['role=heading[name="Attach floating IP"]']) @@ -143,9 +145,14 @@ test('Instance networking tab — floating IPs', async ({ page }) => { // Verify that the "Attach floating IP" button is disabled, since there shouldn't be any more IPs to attach await expect(attachFloatingIpButton).toBeDisabled() + // Verify that the External IPs table row has an ellipsis link in it + await expect(page.getByText('123.4.56.5/…')).toBeVisible() + // Detach one of the external IPs await clickRowAction(page, 'cola-float', 'Detach') await page.getByRole('button', { name: 'Confirm' }).click() + await expect(page.getByText('123.4.56.5/…')).toBeHidden() + await expect(page.getByText('external IPs123.4.56.4/123.4.56.0')).toBeVisible() // Since we detached it, we don't expect to see the row any longer await expect(externalIpTable.getByRole('cell', { name: 'cola-float' })).toBeHidden()