From c62b47d150016959685dd4dc5fc37813302bcaaf Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 21 Nov 2024 15:25:47 -0800 Subject: [PATCH 01/12] List ephemeral IP last; handle overflow scrolling --- app/components/ExternalIps.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/components/ExternalIps.tsx b/app/components/ExternalIps.tsx index 2234656780..f44104b425 100644 --- a/app/components/ExternalIps.tsx +++ b/app/components/ExternalIps.tsx @@ -10,6 +10,7 @@ import { useApiQuery } 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' type InstanceSelector = { project: string; instance: string } @@ -23,11 +24,13 @@ export function ExternalIps({ project, instance }: InstanceSelector) { const ips = data?.items if (!ips || ips.length === 0) return + // create a copy of ips so we don't mutate the original; move ephemeral ip to the end + const orderedIps = [...ips].sort((a) => (a.kind === 'ephemeral' ? 1 : -1)) return ( -
+
{intersperse( - ips.map((eip) => ), - / + orderedIps.map((eip) => ), + )}
) From 091a071dbe9f8689ac6704b61e7937fdd28725ee Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 22 Nov 2024 14:40:12 -0800 Subject: [PATCH 02/12] Add overflow logic --- app/components/ExternalIps.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/components/ExternalIps.tsx b/app/components/ExternalIps.tsx index f44104b425..06d16443e3 100644 --- a/app/components/ExternalIps.tsx +++ b/app/components/ExternalIps.tsx @@ -6,12 +6,16 @@ * Copyright Oxide Computer Company */ +import { Link } from 'react-router-dom' + import { useApiQuery } from '@oxide/api' import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell' +import { Badge } from '~/ui/lib/Badge' import { CopyableIp } from '~/ui/lib/CopyableIp' import { Slash } from '~/ui/lib/Slash' import { intersperse } from '~/util/array' +import { pb } from '~/util/path-builder' type InstanceSelector = { project: string; instance: string } @@ -26,12 +30,19 @@ export function ExternalIps({ project, instance }: InstanceSelector) { if (!ips || ips.length === 0) return // create a copy of ips so we don't mutate the original; move ephemeral ip to the end const orderedIps = [...ips].sort((a) => (a.kind === 'ephemeral' ? 1 : -1)) + const toShow = orderedIps.slice(0, 2) + const overflowCount = orderedIps.length - toShow.length return ( -
+
{intersperse( - orderedIps.map((eip) => ), + toShow.map((eip) => ), )} + {overflowCount > 0 && ( + + +{overflowCount} + + )}
) } From d1724ab8d729182511da1f0aac911fff92a43809 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 22 Nov 2024 15:56:12 -0800 Subject: [PATCH 03/12] Add spacing; add tests --- app/components/ExternalIps.tsx | 2 +- test/e2e/instance-networking.e2e.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/components/ExternalIps.tsx b/app/components/ExternalIps.tsx index 06d16443e3..2c6417d1f2 100644 --- a/app/components/ExternalIps.tsx +++ b/app/components/ExternalIps.tsx @@ -39,7 +39,7 @@ export function ExternalIps({ project, instance }: InstanceSelector) { )} {overflowCount > 0 && ( - + +{overflowCount} )} diff --git a/test/e2e/instance-networking.e2e.ts b/test/e2e/instance-networking.e2e.ts index f74b1b91b3..5d9a72dbd3 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 a +1 in it + await expect(page.getByText('external IPs123.4.56.5/123.4.56.4+1')).toBeVisible() + // Detach one of the external IPs await clickRowAction(page, 'cola-float', 'Detach') await page.getByRole('button', { name: 'Confirm' }).click() + await expect(page.getByText('external IPs123.4.56.5/123.4.56.4+1')).toBeHidden() + await expect(page.getByText('external IPs123.4.56.5/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() From eaccbf1424e14abdb087db5c5883cf7448942625 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 22 Nov 2024 16:19:33 -0800 Subject: [PATCH 04/12] oops --- test/e2e/instance-networking.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/instance-networking.e2e.ts b/test/e2e/instance-networking.e2e.ts index 5d9a72dbd3..befb88fe8e 100644 --- a/test/e2e/instance-networking.e2e.ts +++ b/test/e2e/instance-networking.e2e.ts @@ -152,7 +152,7 @@ test('Instance networking tab — floating IPs', async ({ page }) => { await clickRowAction(page, 'cola-float', 'Detach') await page.getByRole('button', { name: 'Confirm' }).click() await expect(page.getByText('external IPs123.4.56.5/123.4.56.4+1')).toBeHidden() - await expect(page.getByText('external IPs123.4.56.5/123.4.56.0')).toBeVisible() + 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() From 2fc3239e07bb7ea5e934b28213143dc95b77fdaf Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 13 Dec 2024 16:30:52 -0800 Subject: [PATCH 05/12] refactoring --- app/components/ExternalIps.tsx | 36 ++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/app/components/ExternalIps.tsx b/app/components/ExternalIps.tsx index 43186893e3..918d77706a 100644 --- a/app/components/ExternalIps.tsx +++ b/app/components/ExternalIps.tsx @@ -28,25 +28,27 @@ export function ExternalIps({ project, instance }: PP.Instance) { if (!ips || ips.length === 0) return // create a copy of ips so we don't mutate the original; move ephemeral ip to the end const orderedIps = [...ips].sort((a) => (a.kind === 'ephemeral' ? 1 : -1)) - const toShow = orderedIps.slice(0, 2) - const overflowCount = orderedIps.length - toShow.length + const ipsToShow = orderedIps.slice(0, 2) + const overflowCount = orderedIps.length - ipsToShow.length + + // create a list of CopyableIp components + const links = ipsToShow.map((eip) => ) + + // if there are more than 2 ips, add a link to the instance networking page + if (overflowCount > 0) { + links.push( + + +{overflowCount} + + ) + } + return (
- {intersperse( - toShow.map((eip) => ), - - )} - {overflowCount > 0 && ( - <> - - - +{overflowCount} - - - )} + {intersperse(links, )}
) } From 07256803c43e61b750bc4c0fc61227b12adec8a8 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 7 Jan 2025 12:24:40 -0800 Subject: [PATCH 06/12] Fix import --- app/components/ExternalIps.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/ExternalIps.tsx b/app/components/ExternalIps.tsx index 918d77706a..b755a5c166 100644 --- a/app/components/ExternalIps.tsx +++ b/app/components/ExternalIps.tsx @@ -6,7 +6,7 @@ * Copyright Oxide Computer Company */ -import { Link } from 'react-router-dom' +import { Link } from 'react-router' import { useApiQuery } from '@oxide/api' From 040c046f762cad0040d422f1ed0f56de8aefb6e1 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 8 Jan 2025 16:52:34 -0800 Subject: [PATCH 07/12] Sort external IPs on instance networking table with ephemeral at end of list --- app/components/ExternalIps.tsx | 8 ++++++-- .../project/instances/instance/tabs/NetworkingTab.tsx | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/components/ExternalIps.tsx b/app/components/ExternalIps.tsx index b755a5c166..508956fd92 100644 --- a/app/components/ExternalIps.tsx +++ b/app/components/ExternalIps.tsx @@ -8,7 +8,7 @@ import { Link } from 'react-router' -import { useApiQuery } from '@oxide/api' +import { useApiQuery, type ExternalIp } from '@oxide/api' import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell' import { CopyableIp } from '~/ui/lib/CopyableIp' @@ -17,6 +17,10 @@ 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[]) => + ips.sort((a) => (a.kind === 'ephemeral' ? 1 : -1)) + export function ExternalIps({ project, instance }: PP.Instance) { const { data, isPending } = useApiQuery('instanceExternalIpList', { path: { instance }, @@ -27,7 +31,7 @@ export function ExternalIps({ project, instance }: PP.Instance) { const ips = data?.items if (!ips || ips.length === 0) return // create a copy of ips so we don't mutate the original; move ephemeral ip to the end - const orderedIps = [...ips].sort((a) => (a.kind === 'ephemeral' ? 1 : -1)) + const orderedIps = orderIps(ips) const ipsToShow = orderedIps.slice(0, 2) const overflowCount = orderedIps.length - ipsToShow.length diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 43e99b472f..a0f6375c9b 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: orderIps(eips.items), getCoreRowModel: getCoreRowModel(), }) From 0ef57d68ee65438ff8d15a3b66afce2be1610507 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 9 Jan 2025 09:19:30 -0800 Subject: [PATCH 08/12] fix test --- test/e2e/instance-networking.e2e.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/instance-networking.e2e.ts b/test/e2e/instance-networking.e2e.ts index 53f840d4ed..bd2dd38be7 100644 --- a/test/e2e/instance-networking.e2e.ts +++ b/test/e2e/instance-networking.e2e.ts @@ -146,12 +146,12 @@ test('Instance networking tab — floating IPs', async ({ page }) => { await expect(attachFloatingIpButton).toBeDisabled() // Verify that the External IPs table row has a +1 in it - await expect(page.getByText('external IPs123.4.56.5/123.4.56.4/+1')).toBeVisible() + await expect(page.getByText('123.4.56.5/+1')).toBeVisible() // Detach one of the external IPs await clickRowAction(page, 'cola-float', 'Detach') await page.getByRole('button', { name: 'Confirm' }).click() - await expect(page.getByText('external IPs123.4.56.5/123.4.56.4/+1')).toBeHidden() + await expect(page.getByText('123.4.56.5/+1')).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 From c628ff2d0c342f40e9f845f223b69804d7299696 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Fri, 10 Jan 2025 16:31:01 +0000 Subject: [PATCH 09/12] Tighten copy button and tweak overflow style --- app/components/ExternalIps.tsx | 28 ++++++++++++++-------------- app/pages/settings/ProfilePage.tsx | 2 +- app/ui/lib/CopyableIp.tsx | 2 +- app/ui/lib/Slash.tsx | 8 ++++++-- app/ui/lib/Truncate.tsx | 2 +- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/app/components/ExternalIps.tsx b/app/components/ExternalIps.tsx index 508956fd92..5096c48d86 100644 --- a/app/components/ExternalIps.tsx +++ b/app/components/ExternalIps.tsx @@ -38,21 +38,21 @@ export function ExternalIps({ project, instance }: PP.Instance) { // create a list of CopyableIp components const links = ipsToShow.map((eip) => ) - // if there are more than 2 ips, add a link to the instance networking page - if (overflowCount > 0) { - links.push( - - +{overflowCount} - - ) - } - return ( -
- {intersperse(links, )} +
+ {intersperse(links, )} + {/* if there are more than 2 ips, add a link to the instance networking page */} + {overflowCount > 0 && ( + <> + + + … + + + )}
) } 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)} From ad4ea2594276d90f4cdad10c3f21dd8d145a7c72 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 10 Jan 2025 10:04:16 -0800 Subject: [PATCH 10/12] update test --- test/e2e/instance-networking.e2e.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/instance-networking.e2e.ts b/test/e2e/instance-networking.e2e.ts index bd2dd38be7..b7cd15cbe4 100644 --- a/test/e2e/instance-networking.e2e.ts +++ b/test/e2e/instance-networking.e2e.ts @@ -145,13 +145,13 @@ 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 a +1 in it - await expect(page.getByText('123.4.56.5/+1')).toBeVisible() + // 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/+1')).toBeHidden() + 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 From 073fbf096045b3eeb06987ecc9a392805c0c2bb0 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 10 Jan 2025 11:20:30 -0800 Subject: [PATCH 11/12] Remove unneeded comment --- app/components/ExternalIps.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/ExternalIps.tsx b/app/components/ExternalIps.tsx index 5096c48d86..2d8a9b99a6 100644 --- a/app/components/ExternalIps.tsx +++ b/app/components/ExternalIps.tsx @@ -30,7 +30,6 @@ export function ExternalIps({ project, instance }: PP.Instance) { const ips = data?.items if (!ips || ips.length === 0) return - // create a copy of ips so we don't mutate the original; move ephemeral ip to the end const orderedIps = orderIps(ips) const ipsToShow = orderedIps.slice(0, 2) const overflowCount = orderedIps.length - ipsToShow.length From 1f49abe37b532442ab6017e23ad5cec0f0eaf5c8 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 10 Jan 2025 11:55:44 -0800 Subject: [PATCH 12/12] Use Remeda for sorting; memoize IP list ordering for table --- app/components/ExternalIps.tsx | 3 ++- app/pages/project/instances/instance/tabs/NetworkingTab.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/ExternalIps.tsx b/app/components/ExternalIps.tsx index 2d8a9b99a6..f0a081666e 100644 --- a/app/components/ExternalIps.tsx +++ b/app/components/ExternalIps.tsx @@ -7,6 +7,7 @@ */ import { Link } from 'react-router' +import * as R from 'remeda' import { useApiQuery, type ExternalIp } from '@oxide/api' @@ -19,7 +20,7 @@ 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[]) => - ips.sort((a) => (a.kind === 'ephemeral' ? 1 : -1)) + R.sortBy(ips, (a) => (a.kind === 'ephemeral' ? 1 : -1)) export function ExternalIps({ project, instance }: PP.Instance) { const { data, isPending } = useApiQuery('instanceExternalIpList', { diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index a0f6375c9b..de416e3242 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -372,7 +372,7 @@ export function Component() { const ipTableInstance = useReactTable({ columns: useColsWithActions(staticIpCols, makeIpActions), - data: orderIps(eips.items), + data: useMemo(() => orderIps(eips.items), [eips]), getCoreRowModel: getCoreRowModel(), })