From f45790e07e45b798c6745eba8eb32c5ff178eb7a Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 2 Apr 2024 17:21:37 -0700 Subject: [PATCH 01/23] Change tab to networking --- app/pages/project/instances/instance/InstancePage.tsx | 2 +- app/routes.tsx | 2 +- test/e2e/click-everything.e2e.ts | 2 +- test/e2e/instance-create.e2e.ts | 2 +- test/e2e/instance/networking.e2e.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index b3c5db376..1d5a36fe8 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -192,7 +192,7 @@ export function InstancePage() { Storage Metrics - Network Interfaces + Networking Connect diff --git a/app/routes.tsx b/app/routes.tsx index 367a8e729..3081e5aee 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -310,7 +310,7 @@ export const routes = createRoutesFromElements( path="network-interfaces" element={} loader={NetworkingTab.loader} - handle={{ crumb: 'Network interfaces' }} + handle={{ crumb: 'Networking' }} /> { 'role=heading[name*=db1]', 'role=tab[name="Storage"]', 'role=tab[name="Metrics"]', - 'role=tab[name="Network Interfaces"]', + 'role=tab[name="Networking"]', 'role=cell[name="disk-1"]', 'role=cell[name="disk-2"]', // buttons disabled while instance is running diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts index 22276be89..fde31e6c9 100644 --- a/test/e2e/instance-create.e2e.ts +++ b/test/e2e/instance-create.e2e.ts @@ -70,7 +70,7 @@ test('can create an instance', async ({ page }) => { ]) // network tab works - await page.getByRole('tab', { name: 'Network Interfaces' }).click() + await page.getByRole('tab', { name: 'Networking' }).click() const table = page.getByRole('table') await expectRowVisible(table, { name: 'defaultprimary', diff --git a/test/e2e/instance/networking.e2e.ts b/test/e2e/instance/networking.e2e.ts index 43ef5064d..98bbb7de5 100644 --- a/test/e2e/instance/networking.e2e.ts +++ b/test/e2e/instance/networking.e2e.ts @@ -23,7 +23,7 @@ test('Instance networking tab', async ({ page }) => { await expect(page.getByRole('link', { name: '123.4.56.0' })).toBeVisible() // Instance networking tab - await page.click('role=tab[name="Network Interfaces"]') + await page.click('role=tab[name="Networking"]') const table = page.locator('table') await expectRowVisible(table, { name: 'my-nicprimary' }) From 693d02544fba70aa4aad73e6a5b804d8b10384df Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 3 Apr 2024 09:15:58 -0700 Subject: [PATCH 02/23] Move Add Network Interface button above table --- .../instances/instance/tabs/NetworkingTab.tsx | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 21d890427..635746ece 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -212,38 +212,32 @@ export function NetworkingTab() { return ( <> -

- Network Interfaces -

- -
-
- -
- {!canUpdateNic && ( - - A network interface cannot be created or edited unless the instance is{' '} - {updateNicStates}. - +
+

+ Network Interfaces +

+ + {createModalOpen && ( + setCreateModalOpen(false)} + onSubmit={(body) => createNic.mutate({ query: instanceSelector, body })} + submitError={createNic.error} + /> )}
- - {createModalOpen && ( - setCreateModalOpen(false)} - onSubmit={(body) => createNic.mutate({ query: instanceSelector, body })} - submitError={createNic.error} - /> - )} +
{editing && ( setEditing(null)} /> )} From c8bc25df49448196106fddca1a395f854a1d11d6 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 3 Apr 2024 17:15:03 -0700 Subject: [PATCH 03/23] Move to using useReactTable directly for Network Interfaces --- .../instances/instance/tabs/NetworkingTab.tsx | 66 +++++++++++++------ 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 44fb5e674..537c0d3c7 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -5,7 +5,7 @@ * * Copyright Oxide Computer Company */ -import { createColumnHelper } from '@tanstack/react-table' +import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table' import { useCallback, useState } from 'react' import { type LoaderFunctionArgs } from 'react-router-dom' @@ -33,10 +33,11 @@ import { SkeletonCell } from '~/table/cells/EmptyCell' import { LinkCell } from '~/table/cells/LinkCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' import { Columns } from '~/table/columns/common' -import { useQueryTable } from '~/table/QueryTable' +import { Table } from '~/table/Table' import { Badge } from '~/ui/lib/Badge' import { Button } from '~/ui/lib/Button' import { EmptyMessage } from '~/ui/lib/EmptyMessage' +import { TableEmptyBox } from '~/ui/lib/Table' import { pb } from '~/util/path-builder' import { fancifyStates } from './common' @@ -75,7 +76,8 @@ NetworkingTab.loader = async ({ params }: LoaderFunctionArgs) => { const { project, instance } = getInstanceSelector(params) await Promise.all([ apiQueryClient.prefetchQuery('instanceNetworkInterfaceList', { - query: { project, instance, limit: 25 }, + // we want this to cover all NICs; TODO: determine actual limit? + query: { project, instance, limit: 1000 }, }), // This is covered by the InstancePage loader but there's no downside to // being redundant. If it were removed there, we'd still want it here. @@ -89,12 +91,12 @@ NetworkingTab.loader = async ({ params }: LoaderFunctionArgs) => { const colHelper = createColumnHelper() const staticCols = [ - colHelper.accessor((i) => ({ name: i.name, primary: i.primary }), { + colHelper.accessor('name', { header: 'name', cell: (info) => ( <> - {info.getValue().name} - {info.getValue().primary ? primary : null} + {info.getValue()} + {info.row.original.primary && primary} ), }), @@ -127,14 +129,12 @@ export function NetworkingTab() { setCreateModalOpen(false) }, }) - const deleteNic = useApiMutation('instanceNetworkInterfaceDelete', { onSuccess() { queryClient.invalidateQueries('instanceNetworkInterfaceList') addToast({ content: 'Network interface deleted' }) }, }) - const editNic = useApiMutation('instanceNetworkInterfaceUpdate', { onSuccess() { queryClient.invalidateQueries('instanceNetworkInterfaceList') @@ -197,19 +197,17 @@ export function NetworkingTab() { [canUpdateNic, deleteNic, editNic, instanceSelector] ) - const emptyState = ( - } - title="No network interfaces" - body="You need to create a network interface to be able to see it here" - /> - ) + const columns = useColsWithActions(staticCols, makeActions) - const { Table } = useQueryTable('instanceNetworkInterfaceList', { + const rows = useApiQuery('instanceNetworkInterfaceList', { query: instanceSelector, - }) + }).data?.items - const columns = useColsWithActions(staticCols, makeActions) + const tableInstance = useReactTable({ + columns, + data: rows || [], + getCoreRowModel: getCoreRowModel(), + }) return ( <> @@ -238,10 +236,40 @@ export function NetworkingTab() { /> )} -
+ {rows?.length && rows.length > 0 ? ( +
+ ) : ( + + } + title="No network interfaces" + body="You need to create a network interface to be able to see it here" + /> + + )} + {editing && ( setEditing(null)} /> )} + +
+

+ Attached IPs +

+ +
) } From 8985fd6a2877fdcafd5829133bbd9ba6b163848b Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 5 Apr 2024 17:00:23 -0700 Subject: [PATCH 04/23] Table and modal to attach / detach working --- app/components/TableTitle.tsx | 13 ++ .../floating-ips/AttachFloatingIpModal.tsx | 107 +++++++++++++ .../instances/instance/tabs/NetworkingTab.tsx | 142 +++++++++++++++--- .../instances/instance/tabs/StorageTab.tsx | 5 +- app/table/columns/common.tsx | 6 +- 5 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 app/components/TableTitle.tsx create mode 100644 app/pages/project/floating-ips/AttachFloatingIpModal.tsx diff --git a/app/components/TableTitle.tsx b/app/components/TableTitle.tsx new file mode 100644 index 000000000..427fa159c --- /dev/null +++ b/app/components/TableTitle.tsx @@ -0,0 +1,13 @@ +/* + * 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 + */ + +export const TableTitle = ({ id, text }: { id?: string; text: string }) => ( +

+ {text} +

+) diff --git a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx new file mode 100644 index 000000000..0ce7645e9 --- /dev/null +++ b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx @@ -0,0 +1,107 @@ +/* + * 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 { useForm } from 'react-hook-form' + +import { useApiMutation, useApiQueryClient, type FloatingIp, type Instance } from '~/api' +import { addToast } from '~/stores/toast' +import { Listbox } from '~/ui/lib/Listbox' +import { Message } from '~/ui/lib/Message' +import { Modal } from '~/ui/lib/Modal' + +export const AttachFloatingIpModal = ({ + floatingIp, + floatingIps, + instance, + project, + onDismiss, +}: { + floatingIp?: string + floatingIps?: Array + instance: Instance + project: string + onDismiss: () => void +}) => { + const queryClient = useApiQueryClient() + const floatingIpAttach = useApiMutation('floatingIpAttach', { + onSuccess() { + queryClient.invalidateQueries('floatingIpList') + queryClient.invalidateQueries('instanceExternalIpList') + addToast({ content: 'Your Floating IP has been attached' }) + onDismiss() + }, + onError: (err) => { + addToast({ title: 'Error', content: err.message, variant: 'error' }) + }, + }) + const form = useForm({ defaultValues: { floatingIp, instanceId: instance?.id } }) + + return ( + + + + + The ‘{instance.name}’ instance will be reachable at the selected IP address + + } + > +
+ {instance && ( + + )} + {floatingIps && ( + ({ + value: ip.id, + label: ( +
+
{ip.name}
+
+
{ip.ip}
+ + / + +
+ {ip.description || '—'} +
+
+
+ ), + labelString: ip.name, + }))} + label="Floating IP" + onChange={(e) => { + form.setValue('floatingIp', e) + }} + required + placeholder="Select floating IP" + selected={form.watch('floatingIp') || floatingIps[0].id} + /> + )} + +
+
+ + floatingIpAttach.mutate({ + path: { floatingIp: form.getValues('floatingIp')! }, + query: { project }, + body: { kind: 'instance', parent: form.getValues('instanceId') }, + }) + } + onDismiss={onDismiss} + > +
+ ) +} diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 094450877..df13abdd5 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -16,10 +16,13 @@ import { useApiQuery, useApiQueryClient, usePrefetchedApiQuery, + type ExternalIp, type InstanceNetworkInterface, } from '@oxide/api' import { Networking24Icon } from '@oxide/design-system/icons/react' +import { HL } from '~/components/HL' +import { TableTitle } from '~/components/TableTitle' import { CreateNetworkInterfaceForm } from '~/forms/network-interface-create' import { EditNetworkInterfaceForm } from '~/forms/network-interface-edit' import { @@ -28,11 +31,13 @@ import { useProjectSelector, useToast, } from '~/hooks' +import { AttachFloatingIpModal } from '~/pages/project/floating-ips/AttachFloatingIpModal' +import { confirmAction } from '~/stores/confirm-action' import { confirmDelete } from '~/stores/confirm-delete' -import { SkeletonCell } from '~/table/cells/EmptyCell' +import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell' import { LinkCell } from '~/table/cells/LinkCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' -import { Columns } from '~/table/columns/common' +import { Columns, DescriptionCell } from '~/table/columns/common' import { Table } from '~/table/Table' import { Badge } from '~/ui/lib/Badge' import { Button } from '~/ui/lib/Button' @@ -79,6 +84,9 @@ NetworkingTab.loader = async ({ params }: LoaderFunctionArgs) => { // we want this to cover all NICs; TODO: determine actual limit? query: { project, instance, limit: 1000 }, }), + apiQueryClient.prefetchQuery('floatingIpList', { + query: { project, limit: 1000 }, + }), // This is covered by the InstancePage loader but there's no downside to // being redundant. If it were removed there, we'd still want it here. apiQueryClient.prefetchQuery('instanceView', { @@ -101,7 +109,8 @@ const staticCols = [ ), }), colHelper.accessor('description', Columns.description), - colHelper.accessor('ip', {}), + // TODO: Revisit title of 'Internal' vs. 'Private', etc. + colHelper.accessor('ip', { header: 'Internal IP' }), colHelper.accessor('vpcId', { header: 'vpc', cell: (info) => , @@ -116,12 +125,21 @@ const updateNicStates = fancifyStates(instanceCan.updateNic.states) export function NetworkingTab() { const instanceSelector = useInstanceSelector() + const { instance: instanceName, project } = instanceSelector const queryClient = useApiQueryClient() const addToast = useToast() const [createModalOpen, setCreateModalOpen] = useState(false) const [editing, setEditing] = useState(null) + const [attachModalOpen, setAttachModalOpen] = useState(false) + + // Fetch the floating IPs to show in the "Attach floating IP" modal + const { data: ips } = usePrefetchedApiQuery('floatingIpList', { + query: { project, limit: 1000 }, + }) + // Filter out the IPs that are already attached to an instance + const availableIps = ips?.items.filter((ip) => !ip.instanceId) const createNic = useApiMutation('instanceNetworkInterfaceCreate', { onSuccess() { @@ -142,8 +160,8 @@ export function NetworkingTab() { }) const { data: instance } = usePrefetchedApiQuery('instanceView', { - path: { instance: instanceSelector.instance }, - query: { project: instanceSelector.project }, + path: { instance: instanceName }, + query: { project }, }) const canUpdateNic = instanceCan.updateNic(instance) @@ -209,12 +227,91 @@ export function NetworkingTab() { getCoreRowModel: getCoreRowModel(), }) + // Attached IPs Table + const { data: ipData } = useApiQuery('instanceExternalIpList', { + path: { instance: instanceName }, + query: { project }, + }) + + const ipColHelper = createColumnHelper() + const staticIpCols = [ + ipColHelper.accessor('ip', { + cell: (info) => { + // see if info.row.original is an ExternalIp or a FloatingIp + const isEphemeral = info.row.original.kind === 'ephemeral' + // if the type of the original row is ExternalIp, print the IP + return ( + <> + {info.getValue()} + {isEphemeral && ( + + ephemeral + + )} + + ) + }, + }), + ipColHelper.accessor('name', { + cell: (info) => (info.getValue() ? info.getValue() : ), + }), + ipColHelper.accessor((row) => ('description' in row ? row.description : undefined), { + header: 'description', + cell: (info) => , + }), + ] + + const floatingIpDetach = useApiMutation('floatingIpDetach', { + onSuccess() { + queryClient.invalidateQueries('floatingIpList') + queryClient.invalidateQueries('instanceExternalIpList') + addToast({ content: 'Your floating IP has been detached' }) + }, + onError: (err) => { + addToast({ title: 'Error', content: err.message, variant: 'error' }) + }, + }) + + const makeIpActions = useCallback( + (externalIp: ExternalIp): MenuAction[] => { + if (externalIp.kind === 'floating') + return [ + { + label: 'Detach', + onActivate: () => + confirmAction({ + actionType: 'danger', + doAction: () => + floatingIpDetach.mutateAsync({ + path: { floatingIp: externalIp.name }, + query: { project }, + }), + modalTitle: 'Detach Floating IP', + modalContent: ( +

+ Are you sure you want to detach floating IP {externalIp.name}?{' '} + The instance {instanceName} will no longer be reachable at{' '} + {externalIp.ip}. +

+ ), + errorTitle: 'Error detaching floating IP', + }), + }, + ] + return [] + }, + [floatingIpDetach, instanceName, project] + ) + + const ipTableInstance = useReactTable({ + columns: useColsWithActions(staticIpCols, makeIpActions), + data: ipData?.items || [], + getCoreRowModel: getCoreRowModel(), + }) return ( <>
-

- Network Interfaces -

+ + {attachModalOpen && ( + setAttachModalOpen(false)} + project={project} + /> + )}
+
) } diff --git a/app/pages/project/instances/instance/tabs/StorageTab.tsx b/app/pages/project/instances/instance/tabs/StorageTab.tsx index e0e3ae7b9..4f4249577 100644 --- a/app/pages/project/instances/instance/tabs/StorageTab.tsx +++ b/app/pages/project/instances/instance/tabs/StorageTab.tsx @@ -22,6 +22,7 @@ import { import { Storage24Icon } from '@oxide/design-system/icons/react' import { DiskStatusBadge } from '~/components/StatusBadge' +import { TableTitle } from '~/components/TableTitle' import { AttachDiskSideModalForm } from '~/forms/disk-attach' import { CreateDiskSideModalForm } from '~/forms/disk-create' import { getInstanceSelector, useInstanceSelector } from '~/hooks' @@ -168,9 +169,7 @@ export function StorageTab() { return ( <> -

- Disks -

+ {/* TODO: need 40px high rows. another table or a flag on Table (ew) */}
diff --git a/app/table/columns/common.tsx b/app/table/columns/common.tsx index b9bbe6518..727268463 100644 --- a/app/table/columns/common.tsx +++ b/app/table/columns/common.tsx @@ -11,6 +11,7 @@ import { filesize } from 'filesize' import { Truncate } from '~/ui/lib/Truncate' +import { EmptyCell } from '../cells/EmptyCell' import { TwoLineCell } from '../cells/TwoLineCell' // the full type of the info arg is CellContext from RT, but in these @@ -31,11 +32,14 @@ function sizeCell(info: Info) { ) } +export const DescriptionCell = ({ text }: { text?: string }) => + text ? : + /** Columns used in a bunch of tables */ export const Columns = { /** Truncates text if too long, full text in tooltip */ description: { - cell: (info: Info) => , + cell: (info: Info) => , }, size: { cell: sizeCell }, timeCreated: { header: 'created', cell: dateCell }, From 556824bec93a4236c16bb88073ce40c9a029f80b Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 5 Apr 2024 18:40:51 -0700 Subject: [PATCH 05/23] Add tests --- .../floating-ips/AttachFloatingIpModal.tsx | 60 +++++++++---------- .../instances/instance/tabs/NetworkingTab.tsx | 29 ++++----- test/e2e/instance/networking.e2e.ts | 51 ++++++++++++++-- 3 files changed, 87 insertions(+), 53 deletions(-) diff --git a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx index 0ce7645e9..c639dffc5 100644 --- a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx +++ b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx @@ -15,14 +15,12 @@ import { Message } from '~/ui/lib/Message' import { Modal } from '~/ui/lib/Modal' export const AttachFloatingIpModal = ({ - floatingIp, floatingIps, instance, project, onDismiss, }: { - floatingIp?: string - floatingIps?: Array + floatingIps: Array instance: Instance project: string onDismiss: () => void @@ -39,7 +37,7 @@ export const AttachFloatingIpModal = ({ addToast({ title: 'Error', content: err.message, variant: 'error' }) }, }) - const form = useForm({ defaultValues: { floatingIp, instanceId: instance?.id } }) + const form = useForm({ defaultValues: { floatingIp: '', instanceId: instance?.id } }) return ( @@ -57,36 +55,34 @@ export const AttachFloatingIpModal = ({ {instance && ( )} - {floatingIps && ( - ({ - value: ip.id, - label: ( -
-
{ip.name}
-
-
{ip.ip}
- - / - -
- {ip.description || '—'} -
+ ({ + value: ip.id, + label: ( +
+
{ip.name}
+
+
{ip.ip}
+ + / + +
+ {ip.description || '—'}
- ), - labelString: ip.name, - }))} - label="Floating IP" - onChange={(e) => { - form.setValue('floatingIp', e) - }} - required - placeholder="Select floating IP" - selected={form.watch('floatingIp') || floatingIps[0].id} - /> - )} +
+ ), + labelString: ip.name, + }))} + label="Floating IP" + onChange={(e) => { + form.setValue('floatingIp', e) + }} + required + placeholder="Select floating IP" + selected={form.watch('floatingIp')} + /> diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index df13abdd5..65cae3a96 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -236,21 +236,16 @@ export function NetworkingTab() { const ipColHelper = createColumnHelper() const staticIpCols = [ ipColHelper.accessor('ip', { - cell: (info) => { - // see if info.row.original is an ExternalIp or a FloatingIp - const isEphemeral = info.row.original.kind === 'ephemeral' - // if the type of the original row is ExternalIp, print the IP - return ( - <> - {info.getValue()} - {isEphemeral && ( - - ephemeral - - )} - - ) - }, + cell: (info) => ( + <> + {info.getValue()} + {info.row.original.kind === 'ephemeral' && ( + + ephemeral + + )} + + ), }), ipColHelper.accessor('name', { cell: (info) => (info.getValue() ? info.getValue() : ), @@ -274,7 +269,7 @@ export function NetworkingTab() { const makeIpActions = useCallback( (externalIp: ExternalIp): MenuAction[] => { - if (externalIp.kind === 'floating') + if (externalIp.kind === 'floating') { return [ { label: 'Detach', @@ -298,6 +293,8 @@ export function NetworkingTab() { }), }, ] + } + // TODO: Add actions for ephemeral IPs, or hide actions from column return [] }, [floatingIpDetach, instanceName, project] diff --git a/test/e2e/instance/networking.e2e.ts b/test/e2e/instance/networking.e2e.ts index 98bbb7de5..a26b9d499 100644 --- a/test/e2e/instance/networking.e2e.ts +++ b/test/e2e/instance/networking.e2e.ts @@ -15,7 +15,7 @@ import { stopInstance, } from '../utils' -test('Instance networking tab', async ({ page }) => { +test('Instance networking tab — NIC table', async ({ page }) => { await page.goto('/projects/mock-project/instances/db1') // links to VPC and external IPs appear in table @@ -25,8 +25,9 @@ test('Instance networking tab', async ({ page }) => { // Instance networking tab await page.click('role=tab[name="Networking"]') - const table = page.locator('table') - await expectRowVisible(table, { name: 'my-nicprimary' }) + const nicTable = page.getByRole('table', { name: 'Network interfaces' }) + + await expectRowVisible(nicTable, { name: 'my-nicprimary' }) // check VPC link in table points to the right page await expect(page.locator('role=cell >> role=link[name="mock-vpc"]')).toHaveAttribute( @@ -60,8 +61,8 @@ test('Instance networking tab', async ({ page }) => { // Make this interface primary await clickRowAction(page, 'nic-2', 'Make primary') - await expectRowVisible(table, { name: 'my-nic' }) - await expectRowVisible(table, { name: 'nic-2primary' }) + await expectRowVisible(nicTable, { name: 'my-nic' }) + await expectRowVisible(nicTable, { name: 'nic-2primary' }) // Make an edit to the network interface await clickRowAction(page, 'nic-2', 'Edit') @@ -76,3 +77,43 @@ test('Instance networking tab', async ({ page }) => { await page.getByRole('button', { name: 'Confirm' }).click() await expect(nic3).toBeHidden() }) + +test('Instance networking tab — External IPs', async ({ page }) => { + await page.goto('/projects/mock-project/instances/db1/network-interfaces') + const externalIpTable = page.getByRole('table', { name: 'External IPs' }) + + // See list of external IPs + await expectRowVisible(externalIpTable, { ip: '123.4.56.0ephemeral' }) + await expectRowVisible(externalIpTable, { ip: '123.4.56.5' }) + + // Attach a new external IP + await page.click('role=button[name="Attach floating IP"]') + await expectVisible(page, ['role=heading[name="Attach Floating IP"]']) + + // Select the 'rootbeer-float' option + const dialog = page.getByRole('dialog') + // TODO: this "select the option" syntax is awkward; it's working, but I suspect there's a better way + await dialog.getByRole('button', { name: 'Select floating IP' }).click() + await page.keyboard.press('ArrowDown') + await page.keyboard.press('Enter') + // await dialog.getByRole('button', { name: 'rootbeer-float' }).click() + // await dialog.getByRole('button', { name: 'rootbeer-float123.4.56.4/A classic.' }).click() + await dialog.getByRole('button', { name: 'Attach' }).click() + + // Confirm the modal is gone and the new row is showing on the page + await expect(page.getByRole('dialog')).toBeHidden() + await expectRowVisible(externalIpTable, { name: 'rootbeer-float' }) + + // Verify that the "Attach floating IP" button is disabled, since there shouldn't be any more IPs to attach + await expect(page.getByRole('button', { name: 'Attach floating IP' })).toBeDisabled() + + // Detach one of the external IPs + await clickRowAction(page, 'cola-float', 'Detach') + await page.getByRole('button', { name: 'Confirm' }).click() + + // Since we detached it, we don't expect to see db1 any longer + await expect(externalIpTable.getByRole('cell', { name: 'cola-float' })).toBeHidden() + + // And that button shouldbe enabled again + await expect(page.getByRole('button', { name: 'Attach floating IP' })).toBeEnabled() +}) From 7b787dd32eb0aa1e5413ff6c0c7666f53d523634 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 5 Apr 2024 21:12:54 -0700 Subject: [PATCH 06/23] refactor button --- .../instances/instance/tabs/NetworkingTab.tsx | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 65cae3a96..f306c60e5 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -232,6 +232,7 @@ export function NetworkingTab() { path: { instance: instanceName }, query: { project }, }) + const attachedIpCount = ipData?.items?.length || 0 const ipColHelper = createColumnHelper() const staticIpCols = [ @@ -295,9 +296,18 @@ export function NetworkingTab() { ] } // TODO: Add actions for ephemeral IPs, or hide actions from column - return [] + // Below is a placeholder for now + return [ + { + label: 'Copy IP address', + onActivate: () => { + window.navigator.clipboard.writeText(externalIp.ip) + addToast({ content: 'IP address copied to clipboard' }) + }, + }, + ] }, - [floatingIpDetach, instanceName, project] + [addToast, floatingIpDetach, instanceName, project] ) const ipTableInstance = useReactTable({ @@ -305,6 +315,17 @@ export function NetworkingTab() { data: ipData?.items || [], getCoreRowModel: getCoreRowModel(), }) + + const disabledReason = + attachedIpCount >= 32 ? ( + <> + IP address limit reached for this instance. You can have up to 32 total, including 1 + ephemeral IP. + + ) : availableIps?.length === 0 ? ( + <>No available floating IPs. + ) : null + return ( <>
@@ -351,17 +372,8 @@ export function NetworkingTab() { From b8e9238b44db3fc3f5252b718ae611e7d8e9f6ea Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 5 Apr 2024 21:53:15 -0700 Subject: [PATCH 07/23] fix test --- test/e2e/network-interface-create.e2e.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/e2e/network-interface-create.e2e.ts b/test/e2e/network-interface-create.e2e.ts index 21c01ae93..f0e3a871d 100644 --- a/test/e2e/network-interface-create.e2e.ts +++ b/test/e2e/network-interface-create.e2e.ts @@ -34,7 +34,10 @@ test('can create a NIC with a specified IP address', async ({ page }) => { await sidebar.getByRole('button', { name: 'Add network interface' }).click() await expect(sidebar).toBeHidden() - await expectRowVisible(page.getByRole('table'), { name: 'nic-1', ip: '1.2.3.4' }) + await expectRowVisible(page.getByRole('table'), { + name: 'nic-1', + 'Internal IP': '1.2.3.4', + }) }) test('can create a NIC with a blank IP address', async ({ page }) => { @@ -74,5 +77,8 @@ test('can create a NIC with a blank IP address', async ({ page }) => { await expect(sidebar).toBeHidden() // ip address is auto-assigned - await expectRowVisible(page.getByRole('table'), { name: 'nic-2', ip: '123.45.68.8' }) + await expectRowVisible(page.getByRole('table'), { + name: 'nic-2', + 'Internal IP': '123.45.68.8', + }) }) From 1e25de917521d5d88a1ce41238619fee85012298 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 8 Apr 2024 16:38:46 -0500 Subject: [PATCH 08/23] roll TableTitle into UI table file --- app/components/TableTitle.tsx | 13 ------------- .../instances/instance/tabs/NetworkingTab.tsx | 7 +++---- .../project/instances/instance/tabs/StorageTab.tsx | 4 ++-- app/ui/lib/Table.tsx | 2 ++ 4 files changed, 7 insertions(+), 19 deletions(-) delete mode 100644 app/components/TableTitle.tsx diff --git a/app/components/TableTitle.tsx b/app/components/TableTitle.tsx deleted file mode 100644 index 427fa159c..000000000 --- a/app/components/TableTitle.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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 - */ - -export const TableTitle = ({ id, text }: { id?: string; text: string }) => ( -

- {text} -

-) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index f306c60e5..d6c42190c 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -22,7 +22,6 @@ import { import { Networking24Icon } from '@oxide/design-system/icons/react' import { HL } from '~/components/HL' -import { TableTitle } from '~/components/TableTitle' import { CreateNetworkInterfaceForm } from '~/forms/network-interface-create' import { EditNetworkInterfaceForm } from '~/forms/network-interface-edit' import { @@ -42,7 +41,7 @@ import { Table } from '~/table/Table' import { Badge } from '~/ui/lib/Badge' import { Button } from '~/ui/lib/Button' import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import { TableEmptyBox } from '~/ui/lib/Table' +import { TableEmptyBox, TableTitle } from '~/ui/lib/Table' import { pb } from '~/util/path-builder' import { fancifyStates } from './common' @@ -329,7 +328,7 @@ export function NetworkingTab() { return ( <>
- + Network Interfaces
diff --git a/app/ui/lib/Table.tsx b/app/ui/lib/Table.tsx index 08c88239e..ca847c239 100644 --- a/app/ui/lib/Table.tsx +++ b/app/ui/lib/Table.tsx @@ -140,3 +140,5 @@ export const TableControlsButton = (props: ButtonProps) => ( export const TableControlsLink = (props: LinkProps) => ( ) + +export const TableTitle = classed.div`mb-4 text-mono-sm text-secondary` From b26adbcb02a3a1494b6e097c0be19d00652fbb7b Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 10 Apr 2024 13:23:18 -0500 Subject: [PATCH 09/23] change table title to text-sans-lg --- app/pages/project/instances/instance/tabs/NetworkingTab.tsx | 4 ++-- app/ui/lib/Table.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index d6c42190c..45b1410d3 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -328,7 +328,7 @@ export function NetworkingTab() { return ( <>
- Network Interfaces + Network Interfaces
{rows?.length && rows.length > 0 ? ( -
+
) : ( ( ) -export const TableTitle = classed.div`mb-4 text-mono-sm text-secondary` +export const TableTitle = classed.div`mb-4 text-sans-lg text-secondary` From 8e500650f3b04661a6713d8dc5279041fbc9888e Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 11 Apr 2024 11:42:04 -0500 Subject: [PATCH 10/23] tweak spacing --- .../instances/instance/tabs/NetworkingTab.tsx | 12 ++++++------ .../project/instances/instance/tabs/StorageTab.tsx | 6 ++++-- app/ui/lib/Table.tsx | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 45b1410d3..5cd0cf297 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -41,7 +41,7 @@ import { Table } from '~/table/Table' import { Badge } from '~/ui/lib/Badge' import { Button } from '~/ui/lib/Button' import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import { TableEmptyBox, TableTitle } from '~/ui/lib/Table' +import { TableControls, TableEmptyBox, TableTitle } from '~/ui/lib/Table' import { pb } from '~/util/path-builder' import { fancifyStates } from './common' @@ -327,8 +327,8 @@ export function NetworkingTab() { return ( <> -
- Network Interfaces + + Network interfaces
+ {rows?.length && rows.length > 0 ? (
) : ( @@ -366,7 +366,7 @@ export function NetworkingTab() { setEditing(null)} /> )} -
+ External IPs
+
) diff --git a/app/pages/project/instances/instance/tabs/StorageTab.tsx b/app/pages/project/instances/instance/tabs/StorageTab.tsx index 06c8c071a..9598d0ebc 100644 --- a/app/pages/project/instances/instance/tabs/StorageTab.tsx +++ b/app/pages/project/instances/instance/tabs/StorageTab.tsx @@ -31,7 +31,7 @@ import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' import { Button } from '~/ui/lib/Button' import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import { TableTitle } from '~/ui/lib/Table' +import { TableControls, TableTitle } from '~/ui/lib/Table' import { fancifyStates } from './common' @@ -169,7 +169,9 @@ export function StorageTab() { return ( <> - Disks + + Disks + {/* TODO: need 40px high rows. another table or a flag on Table (ew) */}
diff --git a/app/ui/lib/Table.tsx b/app/ui/lib/Table.tsx index f45900e43..cf3c50125 100644 --- a/app/ui/lib/Table.tsx +++ b/app/ui/lib/Table.tsx @@ -131,7 +131,7 @@ export const TableEmptyBox = classed.div`flex h-full max-h-[480px] items-center * Used _outside_ of the `Table`, this element includes a soon-to-be-removed description of the resource inside the table, * along with a link to more info, and a button to take action on the resource listed in the table. */ -export const TableControls = classed.div`mb-4 flex items-end justify-between space-x-8` +export const TableControls = classed.div`mb-6 flex items-end justify-between space-x-8` export const TableControlsText = classed.p`max-w-2xl text-sans-md text-secondary` export const TableControlsButton = (props: ButtonProps) => ( @@ -141,4 +141,4 @@ export const TableControlsLink = (props: LinkProps) => ( ) -export const TableTitle = classed.div`mb-4 text-sans-lg text-secondary` +export const TableTitle = classed.div`text-sans-lg text-secondary` From 8d60df486d080eee8cd80bed54bd8a9b35b99002 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 11 Apr 2024 11:43:19 -0500 Subject: [PATCH 11/23] put external IPs on top --- .../instances/instance/tabs/NetworkingTab.tsx | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 5cd0cf297..681fd63c7 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -108,8 +108,7 @@ const staticCols = [ ), }), colHelper.accessor('description', Columns.description), - // TODO: Revisit title of 'Internal' vs. 'Private', etc. - colHelper.accessor('ip', { header: 'Internal IP' }), + colHelper.accessor('ip', { header: 'Private IP' }), colHelper.accessor('vpcId', { header: 'vpc', cell: (info) => , @@ -328,6 +327,27 @@ export function NetworkingTab() { return ( <> + External IPs + + {attachModalOpen && ( + setAttachModalOpen(false)} + project={project} + /> + )} + +
+ + Network interfaces - {attachModalOpen && ( - setAttachModalOpen(false)} - project={project} - /> - )} - -
) } From 38cc08f31a69dc1e88db8105a4914a768b0fb117 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 11 Apr 2024 12:18:41 -0500 Subject: [PATCH 12/23] fix e2e tests --- .../instances/instance/tabs/NetworkingTab.tsx | 22 +++++++++---------- test/e2e/instance-create.e2e.ts | 2 +- test/e2e/network-interface-create.e2e.ts | 8 +++---- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 681fd63c7..51bc37940 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -268,8 +268,16 @@ export function NetworkingTab() { const makeIpActions = useCallback( (externalIp: ExternalIp): MenuAction[] => { + const copyAction = { + label: 'Copy IP address', + onActivate: () => { + window.navigator.clipboard.writeText(externalIp.ip) + }, + } + if (externalIp.kind === 'floating') { return [ + copyAction, { label: 'Detach', onActivate: () => @@ -293,19 +301,9 @@ export function NetworkingTab() { }, ] } - // TODO: Add actions for ephemeral IPs, or hide actions from column - // Below is a placeholder for now - return [ - { - label: 'Copy IP address', - onActivate: () => { - window.navigator.clipboard.writeText(externalIp.ip) - addToast({ content: 'IP address copied to clipboard' }) - }, - }, - ] + return [copyAction] }, - [addToast, floatingIpDetach, instanceName, project] + [floatingIpDetach, instanceName, project] ) const ipTableInstance = useReactTable({ diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts index 5dbe9e259..83902d983 100644 --- a/test/e2e/instance-create.e2e.ts +++ b/test/e2e/instance-create.e2e.ts @@ -71,7 +71,7 @@ test('can create an instance', async ({ page }) => { // network tab works await page.getByRole('tab', { name: 'Networking' }).click() - const table = page.getByRole('table') + const table = page.getByRole('table', { name: 'Network interfaces' }) await expectRowVisible(table, { name: 'defaultprimary', vpc: 'mock-vpc', diff --git a/test/e2e/network-interface-create.e2e.ts b/test/e2e/network-interface-create.e2e.ts index f0e3a871d..738543d00 100644 --- a/test/e2e/network-interface-create.e2e.ts +++ b/test/e2e/network-interface-create.e2e.ts @@ -34,9 +34,9 @@ test('can create a NIC with a specified IP address', async ({ page }) => { await sidebar.getByRole('button', { name: 'Add network interface' }).click() await expect(sidebar).toBeHidden() - await expectRowVisible(page.getByRole('table'), { + await expectRowVisible(page.getByRole('table', { name: 'Network interfaces' }), { name: 'nic-1', - 'Internal IP': '1.2.3.4', + 'Private IP': '1.2.3.4', }) }) @@ -77,8 +77,8 @@ test('can create a NIC with a blank IP address', async ({ page }) => { await expect(sidebar).toBeHidden() // ip address is auto-assigned - await expectRowVisible(page.getByRole('table'), { + await expectRowVisible(page.getByRole('table', { name: 'Network interfaces' }), { name: 'nic-2', - 'Internal IP': '123.45.68.8', + 'Private IP': '123.45.68.8', }) }) From 7b6802661e45be2fa5e1aff31abf40606fa5898c Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 11 Apr 2024 12:24:45 -0500 Subject: [PATCH 13/23] use non-hook version of addToast --- .../instances/instance/tabs/NetworkingTab.tsx | 9 ++------- test/e2e/network-interface-create.e2e.ts | 12 ++++-------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx index 51bc37940..89fb49925 100644 --- a/app/pages/project/instances/instance/tabs/NetworkingTab.tsx +++ b/app/pages/project/instances/instance/tabs/NetworkingTab.tsx @@ -24,15 +24,11 @@ import { Networking24Icon } from '@oxide/design-system/icons/react' import { HL } from '~/components/HL' import { CreateNetworkInterfaceForm } from '~/forms/network-interface-create' import { EditNetworkInterfaceForm } from '~/forms/network-interface-edit' -import { - getInstanceSelector, - useInstanceSelector, - useProjectSelector, - useToast, -} from '~/hooks' +import { getInstanceSelector, useInstanceSelector, useProjectSelector } from '~/hooks' import { AttachFloatingIpModal } from '~/pages/project/floating-ips/AttachFloatingIpModal' import { confirmAction } from '~/stores/confirm-action' import { confirmDelete } from '~/stores/confirm-delete' +import { addToast } from '~/stores/toast' import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell' import { LinkCell } from '~/table/cells/LinkCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' @@ -126,7 +122,6 @@ export function NetworkingTab() { const { instance: instanceName, project } = instanceSelector const queryClient = useApiQueryClient() - const addToast = useToast() const [createModalOpen, setCreateModalOpen] = useState(false) const [editing, setEditing] = useState(null) diff --git a/test/e2e/network-interface-create.e2e.ts b/test/e2e/network-interface-create.e2e.ts index 738543d00..888806c0a 100644 --- a/test/e2e/network-interface-create.e2e.ts +++ b/test/e2e/network-interface-create.e2e.ts @@ -34,10 +34,8 @@ test('can create a NIC with a specified IP address', async ({ page }) => { await sidebar.getByRole('button', { name: 'Add network interface' }).click() await expect(sidebar).toBeHidden() - await expectRowVisible(page.getByRole('table', { name: 'Network interfaces' }), { - name: 'nic-1', - 'Private IP': '1.2.3.4', - }) + const table = page.getByRole('table', { name: 'Network interfaces' }) + await expectRowVisible(table, { name: 'nic-1', 'Private IP': '1.2.3.4' }) }) test('can create a NIC with a blank IP address', async ({ page }) => { @@ -77,8 +75,6 @@ test('can create a NIC with a blank IP address', async ({ page }) => { await expect(sidebar).toBeHidden() // ip address is auto-assigned - await expectRowVisible(page.getByRole('table', { name: 'Network interfaces' }), { - name: 'nic-2', - 'Private IP': '123.45.68.8', - }) + const table = page.getByRole('table', { name: 'Network interfaces' }) + await expectRowVisible(table, { name: 'nic-2', 'Private IP': '123.45.68.8' }) }) From 71bf1ad5fb780d7918f93a15455164ba52f7dbf4 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 11 Apr 2024 12:29:47 -0500 Subject: [PATCH 14/23] simplify attach modal a bit --- .../floating-ips/AttachFloatingIpModal.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx index c639dffc5..bb2af7967 100644 --- a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx +++ b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx @@ -37,7 +37,7 @@ export const AttachFloatingIpModal = ({ addToast({ title: 'Error', content: err.message, variant: 'error' }) }, }) - const form = useForm({ defaultValues: { floatingIp: '', instanceId: instance?.id } }) + const form = useForm({ defaultValues: { floatingIp: '' } }) return ( @@ -45,16 +45,9 @@ export const AttachFloatingIpModal = ({ - The ‘{instance.name}’ instance will be reachable at the selected IP address - - } - > + content={`Instance ‘${instance.name}’ will be reachable at the selected IP address`} + />
- {instance && ( - - )} ({ @@ -88,12 +81,12 @@ export const AttachFloatingIpModal = ({ floatingIpAttach.mutate({ path: { floatingIp: form.getValues('floatingIp')! }, query: { project }, - body: { kind: 'instance', parent: form.getValues('instanceId') }, + body: { kind: 'instance', parent: instance.id }, }) } onDismiss={onDismiss} From aa95fca2b48dfa3fc3e4dca26c01c8e657eefec7 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 11 Apr 2024 12:36:08 -0500 Subject: [PATCH 15/23] casing --- app/pages/project/floating-ips/AttachFloatingIpModal.tsx | 2 +- app/pages/project/floating-ips/FloatingIpsPage.tsx | 2 +- test/e2e/instance/networking.e2e.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx index bb2af7967..91fc385ba 100644 --- a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx +++ b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx @@ -40,7 +40,7 @@ export const AttachFloatingIpModal = ({ const form = useForm({ defaultValues: { floatingIp: '' } }) return ( - + + { await expect(nic3).toBeHidden() }) -test('Instance networking tab — External IPs', async ({ page }) => { +test('Instance networking tab — External IPs', async ({ page }) => { await page.goto('/projects/mock-project/instances/db1/network-interfaces') const externalIpTable = page.getByRole('table', { name: 'External IPs' }) @@ -88,7 +88,7 @@ test('Instance networking tab — External IPs', async ({ page }) => { // Attach a new external IP await page.click('role=button[name="Attach floating IP"]') - await expectVisible(page, ['role=heading[name="Attach Floating IP"]']) + await expectVisible(page, ['role=heading[name="Attach floating IP"]']) // Select the 'rootbeer-float' option const dialog = page.getByRole('dialog') From 05f769e44017f0c511b599017859e61800de6df2 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 11 Apr 2024 12:39:03 -0500 Subject: [PATCH 16/23] extract FloatingIpLabel --- .../floating-ips/AttachFloatingIpModal.tsx | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx index 91fc385ba..bea7502f8 100644 --- a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx +++ b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx @@ -14,6 +14,21 @@ import { Listbox } from '~/ui/lib/Listbox' import { Message } from '~/ui/lib/Message' import { Modal } from '~/ui/lib/Modal' +function FloatingIpLabel({ fip }: { fip: FloatingIp }) { + return ( +
+
{fip.name}
+
+
{fip.ip}
+ / +
+ {fip.description || '—'} +
+
+
+ ) +} + export const AttachFloatingIpModal = ({ floatingIps, instance, @@ -52,20 +67,7 @@ export const AttachFloatingIpModal = ({ name="floatingIp" items={floatingIps.map((ip) => ({ value: ip.id, - label: ( -
-
{ip.name}
-
-
{ip.ip}
- - / - -
- {ip.description || '—'} -
-
-
- ), + label: , labelString: ip.name, }))} label="Floating IP" From fa03232032515789402a9dfd1f31c7efa765a347 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 11 Apr 2024 12:47:04 -0500 Subject: [PATCH 17/23] use watch instead of getValue and ListboxField instead of setValue --- .../floating-ips/AttachFloatingIpModal.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx index bea7502f8..0d2931e0a 100644 --- a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx +++ b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx @@ -9,8 +9,8 @@ import { useForm } from 'react-hook-form' import { useApiMutation, useApiQueryClient, type FloatingIp, type Instance } from '~/api' +import { ListboxField } from '~/components/form/fields/ListboxField' import { addToast } from '~/stores/toast' -import { Listbox } from '~/ui/lib/Listbox' import { Message } from '~/ui/lib/Message' import { Modal } from '~/ui/lib/Modal' @@ -45,7 +45,7 @@ export const AttachFloatingIpModal = ({ onSuccess() { queryClient.invalidateQueries('floatingIpList') queryClient.invalidateQueries('instanceExternalIpList') - addToast({ content: 'Your Floating IP has been attached' }) + addToast({ content: 'Your floating IP has been attached' }) onDismiss() }, onError: (err) => { @@ -53,6 +53,7 @@ export const AttachFloatingIpModal = ({ }, }) const form = useForm({ defaultValues: { floatingIp: '' } }) + const floatingIp = form.watch('floatingIp') return ( @@ -63,30 +64,27 @@ export const AttachFloatingIpModal = ({ content={`Instance ‘${instance.name}’ will be reachable at the selected IP address`} /> - ({ value: ip.id, label: , labelString: ip.name, }))} - label="Floating IP" - onChange={(e) => { - form.setValue('floatingIp', e) - }} required - placeholder="Select floating IP" - selected={form.watch('floatingIp')} />
floatingIpAttach.mutate({ - path: { floatingIp: form.getValues('floatingIp')! }, + path: { floatingIp }, query: { project }, body: { kind: 'instance', parent: instance.id }, }) From 082c209273c202724a350b9fce319536634a7ea8 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 11 Apr 2024 12:55:49 -0500 Subject: [PATCH 18/23] fix mock floating IP update with empty description --- mock-api/msw/handlers.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 9cc079591..b52c297c3 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -261,9 +261,7 @@ export const handlers = makeHandlers({ } floatingIp.name = body.name } - if (body.description) { - floatingIp.description = body.description - } + floatingIp.description = body.description || '' return floatingIp }, floatingIpDelete({ path, query }) { From 3122232d83bdfe73aeaed31a52dc69d5744b3534 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 11 Apr 2024 12:57:28 -0500 Subject: [PATCH 19/23] leave off dash when there's no description --- .../project/floating-ips/AttachFloatingIpModal.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx index 0d2931e0a..43ce1c090 100644 --- a/app/pages/project/floating-ips/AttachFloatingIpModal.tsx +++ b/app/pages/project/floating-ips/AttachFloatingIpModal.tsx @@ -20,10 +20,14 @@ function FloatingIpLabel({ fip }: { fip: FloatingIp }) {
{fip.name}
{fip.ip}
- / -
- {fip.description || '—'} -
+ {fip.description && ( + <> + / +
+ {fip.description} +
+ + )}
) From 5418dc3bc58ca803a82137b1fcb96586339f2866 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 11 Apr 2024 19:40:20 +0100 Subject: [PATCH 20/23] Table label tweaks --- app/pages/project/instances/instance/tabs/StorageTab.tsx | 8 +------- app/ui/lib/Table.tsx | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/pages/project/instances/instance/tabs/StorageTab.tsx b/app/pages/project/instances/instance/tabs/StorageTab.tsx index 9598d0ebc..5653b1181 100644 --- a/app/pages/project/instances/instance/tabs/StorageTab.tsx +++ b/app/pages/project/instances/instance/tabs/StorageTab.tsx @@ -31,7 +31,6 @@ import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' import { Button } from '~/ui/lib/Button' import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import { TableControls, TableTitle } from '~/ui/lib/Table' import { fancifyStates } from './common' @@ -169,12 +168,7 @@ export function StorageTab() { return ( <> - - Disks - - {/* TODO: need 40px high rows. another table or a flag on Table (ew) */} - -
+