diff --git a/app/forms/external-subnet-edit.tsx b/app/forms/external-subnet-edit.tsx
index 0efa2622c..5e43b0556 100644
--- a/app/forms/external-subnet-edit.tsx
+++ b/app/forms/external-subnet-edit.tsx
@@ -101,9 +101,7 @@ export default function EditExternalSubnetSideModalForm() {
submitError={editExternalSubnet.error}
>
-
-
-
+
{subnet.subnet}
diff --git a/app/forms/firewall-rules-edit.tsx b/app/forms/firewall-rules-edit.tsx
index 0e7c0c590..1998b279d 100644
--- a/app/forms/firewall-rules-edit.tsx
+++ b/app/forms/firewall-rules-edit.tsx
@@ -27,6 +27,8 @@ import {
useVpcSelector,
} from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
+import { FormDivider } from '~/ui/lib/Divider'
+import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { ALL_ISH } from '~/util/consts'
import { invariant } from '~/util/invariant'
import { pb } from '~/util/path-builder'
@@ -134,6 +136,10 @@ export default function EditFirewallRuleForm() {
loading={updateRules.isPending}
submitError={updateRules.error}
>
+
+
+
+
-
-
-
+
@@ -113,6 +112,7 @@ export default function EditFloatingIpSideModalForm() {
+
diff --git a/app/forms/idp/edit.tsx b/app/forms/idp/edit.tsx
index 2f02e2723..ebec9d663 100644
--- a/app/forms/idp/edit.tsx
+++ b/app/forms/idp/edit.tsx
@@ -56,10 +56,9 @@ export default function EditIdpSideModalForm() {
}
>
-
-
-
+
+
diff --git a/app/forms/image-from-snapshot.tsx b/app/forms/image-from-snapshot.tsx
index b6f7767e8..012548590 100644
--- a/app/forms/image-from-snapshot.tsx
+++ b/app/forms/image-from-snapshot.tsx
@@ -25,6 +25,7 @@ import { HL } from '~/components/HL'
import { titleCrumb } from '~/hooks/use-crumbs'
import { getProjectSnapshotSelector, useProjectSnapshotSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
+import { FormDivider } from '~/ui/lib/Divider'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { docLinks } from '~/util/links'
@@ -97,6 +98,7 @@ export default function CreateImageFromSnapshotSideModalForm() {
{formatBytes(data.size).label}
+
diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx
index 15c6b7505..8f1a43750 100644
--- a/app/forms/ip-pool-edit.tsx
+++ b/app/forms/ip-pool-edit.tsx
@@ -18,7 +18,9 @@ import { HL } from '~/components/HL'
import { makeCrumb } from '~/hooks/use-crumbs'
import { getIpPoolSelector, useIpPoolSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
+import { FormDivider } from '~/ui/lib/Divider'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
+import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
@@ -73,6 +75,10 @@ export default function EditIpPoolSideModalForm() {
loading={editPool.isPending}
submitError={editPool.error}
>
+
+
+
+
diff --git a/app/forms/network-interface-edit.tsx b/app/forms/network-interface-edit.tsx
index cbc7ef6d0..f0caaa589 100644
--- a/app/forms/network-interface-edit.tsx
+++ b/app/forms/network-interface-edit.tsx
@@ -24,11 +24,14 @@ import { SideModalForm } from '~/components/form/SideModalForm'
import { HL } from '~/components/HL'
import { useInstanceSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
+import { SubnetNameFromId } from '~/table/cells/SubnetNameCell'
+import { CopyableIp } from '~/ui/lib/CopyableIp'
import { FormDivider } from '~/ui/lib/Divider'
import { FieldLabel } from '~/ui/lib/FieldLabel'
import { Message } from '~/ui/lib/Message'
import { ClearAndAddButtons, MiniTable } from '~/ui/lib/MiniTable'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
+import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { HintLink, TextInputHint } from '~/ui/lib/TextInput'
import { KEYS } from '~/ui/util/keys'
import { parseIpNet, validateIpNet } from '~/util/ip'
@@ -36,6 +39,16 @@ import { docLinks, links } from '~/util/links'
const transitIpTableColumns = [{ header: 'Transit IPs', cell: (ip: string) => ip }]
+// IP addresses aren't editable — a new interface must be created to change them
+// — so surface them as read-only metadata. A NIC has a v4 address, a v6
+// address, or both, depending on its stack type.
+const privateIps = (ipStack: InstanceNetworkInterface['ipStack']) =>
+ match(ipStack)
+ .with({ type: 'v4' }, ({ value }) => ({ v4: value.ip, v6: undefined }))
+ .with({ type: 'v6' }, ({ value }) => ({ v4: undefined, v6: value.ip }))
+ .with({ type: 'dual_stack' }, ({ value }) => ({ v4: value.v4.ip, v6: value.v6.ip }))
+ .exhaustive()
+
type EditNetworkInterfaceFormProps = {
editing: InstanceNetworkInterface
onDismiss: () => void
@@ -46,6 +59,7 @@ export function EditNetworkInterfaceForm({
editing,
}: EditNetworkInterfaceFormProps) {
const instanceSelector = useInstanceSelector()
+ const ips = privateIps(editing.ipStack)
const editNetworkInterface = useApiMutation(api.instanceNetworkInterfaceUpdate, {
onSuccess(nic) {
@@ -113,6 +127,24 @@ export function EditNetworkInterfaceForm({
loading={editNetworkInterface.isPending}
submitError={editNetworkInterface.error}
>
+
+
+ {ips.v4 && (
+
+
+
+ )}
+ {ips.v6 && (
+
+
+
+ )}
+ {editing.mac}
+
+
+
+
+
diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx
index 43144736a..00dea1902 100644
--- a/app/forms/project-edit.tsx
+++ b/app/forms/project-edit.tsx
@@ -17,7 +17,9 @@ import { HL } from '~/components/HL'
import { titleCrumb } from '~/hooks/use-crumbs'
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
+import { FormDivider } from '~/ui/lib/Divider'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
+import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
@@ -68,6 +70,10 @@ export default function EditProjectSideModalForm() {
loading={editProject.isPending}
submitError={editProject.error}
>
+
+
+
+
diff --git a/app/forms/ssh-key-edit.tsx b/app/forms/ssh-key-edit.tsx
index 77408c1ee..210c986cb 100644
--- a/app/forms/ssh-key-edit.tsx
+++ b/app/forms/ssh-key-edit.tsx
@@ -56,9 +56,7 @@ export default function EditSSHKeySideModalForm() {
}
>
-
-
-
+
diff --git a/app/forms/subnet-edit.tsx b/app/forms/subnet-edit.tsx
index 8b5bcb61d..8bc8415d7 100644
--- a/app/forms/subnet-edit.tsx
+++ b/app/forms/subnet-edit.tsx
@@ -33,6 +33,7 @@ import { getVpcSubnetSelector, useVpcSubnetSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { FormDivider } from '~/ui/lib/Divider'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
+import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
@@ -95,6 +96,12 @@ export default function EditSubnetForm() {
loading={updateSubnet.isPending}
submitError={updateSubnet.error}
>
+
+
+ {subnet.ipv4Block}
+ {subnet.ipv6Block}
+
+
diff --git a/app/forms/subnet-pool-edit.tsx b/app/forms/subnet-pool-edit.tsx
index 21049c539..1d5c860ae 100644
--- a/app/forms/subnet-pool-edit.tsx
+++ b/app/forms/subnet-pool-edit.tsx
@@ -18,7 +18,9 @@ import { HL } from '~/components/HL'
import { makeCrumb } from '~/hooks/use-crumbs'
import { getSubnetPoolSelector, useSubnetPoolSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
+import { FormDivider } from '~/ui/lib/Divider'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
+import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
@@ -72,6 +74,10 @@ export default function EditSubnetPoolSideModalForm() {
loading={editPool.isPending}
submitError={editPool.error}
>
+
+
+
+
diff --git a/app/forms/vpc-edit.tsx b/app/forms/vpc-edit.tsx
index a2e5c60e1..a286c9b73 100644
--- a/app/forms/vpc-edit.tsx
+++ b/app/forms/vpc-edit.tsx
@@ -17,7 +17,9 @@ import { HL } from '~/components/HL'
import { titleCrumb } from '~/hooks/use-crumbs'
import { getVpcSelector, useVpcSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
+import { FormDivider } from '~/ui/lib/Divider'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
+import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
@@ -75,6 +77,10 @@ export default function EditVpcSideModalForm() {
loading={editVpc.isPending}
submitError={editVpc.error}
>
+
+
+
+
diff --git a/app/forms/vpc-router-edit.tsx b/app/forms/vpc-router-edit.tsx
index 4feccb194..42720d8ad 100644
--- a/app/forms/vpc-router-edit.tsx
+++ b/app/forms/vpc-router-edit.tsx
@@ -16,6 +16,7 @@ import {
usePrefetchedQuery,
type VpcRouterUpdate,
} from '@oxide/api'
+import { Badge } from '@oxide/design-system/ui'
import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
@@ -24,7 +25,9 @@ import { HL } from '~/components/HL'
import { titleCrumb } from '~/hooks/use-crumbs'
import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
+import { FormDivider } from '~/ui/lib/Divider'
import { SideModalFormDocs } from '~/ui/lib/ModalLinks'
+import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
@@ -78,6 +81,13 @@ export default function EditRouterSideModalForm() {
loading={editRouter.isPending}
submitError={editRouter.error}
>
+
+
+
+ {routerData.kind}
+
+
+
diff --git a/app/forms/vpc-router-route-edit.tsx b/app/forms/vpc-router-route-edit.tsx
index f0d2dbd24..546cddc65 100644
--- a/app/forms/vpc-router-route-edit.tsx
+++ b/app/forms/vpc-router-route-edit.tsx
@@ -10,6 +10,7 @@ import { useNavigate, type LoaderFunctionArgs } from 'react-router'
import * as R from 'remeda'
import { api, q, queryClient, useApiMutation, usePrefetchedQuery } from '@oxide/api'
+import { Badge } from '@oxide/design-system/ui'
import { SideModalForm } from '~/components/form/SideModalForm'
import { HL } from '~/components/HL'
@@ -22,6 +23,8 @@ import {
import { titleCrumb } from '~/hooks/use-crumbs'
import { getVpcRouterRouteSelector, useVpcRouterRouteSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
+import { FormDivider } from '~/ui/lib/Divider'
+import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'
@@ -99,6 +102,13 @@ export default function EditRouterRouteSideModalForm() {
submitError={updateRouterRoute.error}
submitDisabled={disabled ? routeFormMessage.vpcSubnetNotModifiable : undefined}
>
+
+
+
+ {route.kind.replace('_', ' ')}
+
+
+
diff --git a/app/pages/project/external-subnets/ExternalSubnetsPage.tsx b/app/pages/project/external-subnets/ExternalSubnetsPage.tsx
index 8c3bec28f..c52b9b69d 100644
--- a/app/pages/project/external-subnets/ExternalSubnetsPage.tsx
+++ b/app/pages/project/external-subnets/ExternalSubnetsPage.tsx
@@ -7,7 +7,7 @@
*/
import { useQuery } from '@tanstack/react-query'
import { createColumnHelper } from '@tanstack/react-table'
-import { useCallback, useState } from 'react'
+import { useCallback, useMemo, useState } from 'react'
import { useForm } from 'react-hook-form'
import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
@@ -34,6 +34,7 @@ import { confirmAction } from '~/stores/confirm-action'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import { InstanceLink } from '~/table/cells/InstanceLinkCell'
+import { makeLinkCell } from '~/table/cells/LinkCell'
import { SubnetPoolCell } from '~/table/cells/SubnetPoolCell'
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
import { Columns } from '~/table/columns/common'
@@ -77,7 +78,6 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
const colHelper = createColumnHelper()
const staticCols = [
- colHelper.accessor('name', {}),
colHelper.accessor('description', Columns.description),
colHelper.accessor('subnet', {
header: 'Subnet',
@@ -189,7 +189,20 @@ export default function ExternalSubnetsPage() {
[deleteExternalSubnet, externalSubnetDetach, navigate, project, instances]
)
- const columns = useColsWithActions(staticCols, makeActions)
+ // name column links to the edit side modal; defined here (not in staticCols)
+ // because the link needs the project from the route
+ const cols = useMemo(
+ () => [
+ colHelper.accessor('name', {
+ cell: makeLinkCell((externalSubnet) =>
+ pb.externalSubnetEdit({ project, externalSubnet })
+ ),
+ }),
+ ...staticCols,
+ ],
+ [project]
+ )
+ const columns = useColsWithActions(cols, makeActions)
const { table } = useQueryTable({
query: subnetList(project),
columns,
diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx
index 8339f6777..fa1ae8972 100644
--- a/app/pages/project/floating-ips/FloatingIpsPage.tsx
+++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx
@@ -7,7 +7,7 @@
*/
import { useQuery } from '@tanstack/react-query'
import { createColumnHelper } from '@tanstack/react-table'
-import { useCallback, useState } from 'react'
+import { useCallback, useMemo, useState } from 'react'
import { useForm } from 'react-hook-form'
import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
@@ -35,6 +35,7 @@ import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import { InstanceLink } from '~/table/cells/InstanceLinkCell'
import { IpPoolCell, ipPoolErrorsAllowedQuery } from '~/table/cells/IpPoolCell'
+import { makeLinkCell } from '~/table/cells/LinkCell'
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
import { Columns } from '~/table/columns/common'
import { useQueryTable } from '~/table/QueryTable'
@@ -90,7 +91,6 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
const colHelper = createColumnHelper()
const staticCols = [
- colHelper.accessor('name', {}),
colHelper.accessor('description', Columns.description),
colHelper.accessor('ip', {
header: 'IP address',
@@ -204,7 +204,18 @@ export default function FloatingIpsPage() {
[deleteFloatingIp, floatingIpDetach, navigate, project, instances]
)
- const columns = useColsWithActions(staticCols, makeActions)
+ // name column links to the edit side modal; defined here (not in staticCols)
+ // because the link needs the project from the route
+ const cols = useMemo(
+ () => [
+ colHelper.accessor('name', {
+ cell: makeLinkCell((floatingIp) => pb.floatingIpEdit({ project, floatingIp })),
+ }),
+ ...staticCols,
+ ],
+ [project]
+ )
+ const columns = useColsWithActions(cols, makeActions)
const { table } = useQueryTable({
query: fipList(project),
columns,
diff --git a/app/pages/project/instances/NetworkingTab.tsx b/app/pages/project/instances/NetworkingTab.tsx
index 9f651adc4..bc622861e 100644
--- a/app/pages/project/instances/NetworkingTab.tsx
+++ b/app/pages/project/instances/NetworkingTab.tsx
@@ -52,6 +52,7 @@ import { DescriptionCell } from '~/table/cells/DescriptionCell'
import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell'
import { IpPoolCell } from '~/table/cells/IpPoolCell'
import { LinkCell } from '~/table/cells/LinkCell'
+import { SubnetNameFromId } from '~/table/cells/SubnetNameCell'
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
import { Columns } from '~/table/columns/common'
import { Table } from '~/table/Table'
@@ -88,18 +89,6 @@ const VpcNameFromId = ({ value }: { value: string }) => {
return {vpc.name}
}
-const SubnetNameFromId = ({ value }: { value: string }) => {
- const { data: subnet, isError } = useQuery(
- q(api.vpcSubnetView, { path: { subnet: value } }, { throwOnError: false })
- )
-
- // same deal as VPC: probably not possible but let's be safe
- if (isError) return Deleted
- if (!subnet) return // loading
-
- return {subnet.name}
-}
-
const NonFloatingEmptyCell = ({ kind }: { kind: 'snat' | 'ephemeral' }) => (
,
+ cell: (info) => ,
}),
colHelper.display({
id: 'transitIps',
diff --git a/app/pages/project/vpcs/RouterPage.tsx b/app/pages/project/vpcs/RouterPage.tsx
index 5dff2e109..43210dbff 100644
--- a/app/pages/project/vpcs/RouterPage.tsx
+++ b/app/pages/project/vpcs/RouterPage.tsx
@@ -7,7 +7,7 @@
*/
import { createColumnHelper } from '@tanstack/react-table'
-import { useCallback } from 'react'
+import { useCallback, useMemo } from 'react'
import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'
import { Networking16Icon, Networking24Icon } from '@oxide/design-system/icons/react'
@@ -34,6 +34,7 @@ import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params'
import { useQuickActions } from '~/hooks/use-quick-actions'
import { confirmAction } from '~/stores/confirm-action'
import { addToast } from '~/stores/toast'
+import { LinkCell } from '~/table/cells/LinkCell'
import { TypeValueCell } from '~/table/cells/TypeValueCell'
import { useColsWithActions, type MenuAction } from '~/table/columns/action-col'
import { useQueryTable } from '~/table/QueryTable'
@@ -73,6 +74,8 @@ const routeTypes = {
vpc: 'VPC',
}
+const routerRoutesColHelper = createColumnHelper()
+
// All will have a type and a value except `Drop`, which only has a type
const RouterRouteTypeValueBadge = ({
type,
@@ -111,23 +114,33 @@ export default function RouterPage() {
)
const navigate = useNavigate()
- const routerRoutesColHelper = createColumnHelper()
-
- const routerRoutesStaticCols = [
- routerRoutesColHelper.accessor('name', { header: 'Name' }),
- routerRoutesColHelper.accessor('kind', {
- header: 'Kind',
- cell: (info) => {info.getValue().replace('_', ' ')},
- }),
- routerRoutesColHelper.accessor('destination', {
- header: 'Destination',
- cell: (info) => ,
- }),
- routerRoutesColHelper.accessor('target', {
- header: 'Target',
- cell: (info) => ,
- }),
- ]
+ const routerRoutesStaticCols = useMemo(
+ () => [
+ routerRoutesColHelper.accessor('name', {
+ header: 'Name',
+ cell: (info) => (
+
+ {info.getValue()}
+
+ ),
+ }),
+ routerRoutesColHelper.accessor('kind', {
+ header: 'Kind',
+ cell: (info) => {info.getValue().replace('_', ' ')},
+ }),
+ routerRoutesColHelper.accessor('destination', {
+ header: 'Destination',
+ cell: (info) => ,
+ }),
+ routerRoutesColHelper.accessor('target', {
+ header: 'Target',
+ cell: (info) => ,
+ }),
+ ],
+ [project, vpc, router]
+ )
const makeRangeActions = useCallback(
(routerRoute: RouterRoute): MenuAction[] => [
diff --git a/app/pages/project/vpcs/internet-gateway-edit.tsx b/app/pages/project/vpcs/internet-gateway-edit.tsx
index 5d5ac415d..4c54c8bb8 100644
--- a/app/pages/project/vpcs/internet-gateway-edit.tsx
+++ b/app/pages/project/vpcs/internet-gateway-edit.tsx
@@ -128,8 +128,8 @@ export default function EditInternetGatewayForm() {
/>
-
+
Internet gateway IP address
diff --git a/app/table/cells/SubnetNameCell.tsx b/app/table/cells/SubnetNameCell.tsx
new file mode 100644
index 000000000..85710e328
--- /dev/null
+++ b/app/table/cells/SubnetNameCell.tsx
@@ -0,0 +1,25 @@
+/*
+ * 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 { useQuery } from '@tanstack/react-query'
+
+import { api, q } from '@oxide/api'
+import { Badge } from '@oxide/design-system/ui'
+
+import { SkeletonCell } from './EmptyCell'
+
+/** Resolve a subnet ID to its name. Callers must ensure the query is enabled. */
+export const SubnetNameFromId = ({ subnetId }: { subnetId: string }) => {
+ const { data: subnet, isError } = useQuery(
+ q(api.vpcSubnetView, { path: { subnet: subnetId } }, { throwOnError: false })
+ )
+ // probably not possible but let's be safe
+ if (isError) return Deleted
+ if (!subnet) return // loading
+ return {subnet.name}
+}
diff --git a/app/ui/lib/PropertiesTable.tsx b/app/ui/lib/PropertiesTable.tsx
index 9f7f96303..e7f7a5b69 100644
--- a/app/ui/lib/PropertiesTable.tsx
+++ b/app/ui/lib/PropertiesTable.tsx
@@ -37,6 +37,7 @@ export function PropertiesTable({
PropertiesTable.DateRow,
PropertiesTable.SizeRow,
PropertiesTable.CopyableRow,
+ PropertiesTable.ResourceRows,
]),
'PropertiesTable only accepts specific Row components as children'
)
@@ -122,3 +123,20 @@ PropertiesTable.CopyableRow = ({ label, text }: { label: string; text: string })
)
+
+/**
+ * The ID + created/updated timestamps every API resource carries. Renders the
+ * three rows nearly every edit side modal opens with; pass additional
+ * resource-specific `PropertiesTable.*` rows as siblings after it.
+ */
+PropertiesTable.ResourceRows = ({
+ resource,
+}: {
+ resource: { id: string; timeCreated: Date; timeModified: Date }
+}) => (
+ <>
+
+
+
+ >
+)
diff --git a/test/e2e/external-subnets.e2e.ts b/test/e2e/external-subnets.e2e.ts
index 3aa1133d6..dc343c0ff 100644
--- a/test/e2e/external-subnets.e2e.ts
+++ b/test/e2e/external-subnets.e2e.ts
@@ -128,6 +128,13 @@ test('can create an external subnet with explicit CIDR', async ({ page }) => {
})
})
+test('clicking the name opens the edit side modal', async ({ page }) => {
+ await page.goto(externalSubnetsPage)
+ await page.getByRole('link', { name: 'web-subnet' }).click()
+ await expect(page).toHaveURL(`${externalSubnetsPage}/web-subnet/edit`)
+ await expect(page.getByRole('heading', { name: /Edit external subnet/ })).toBeVisible()
+})
+
test('can update an external subnet', async ({ page }) => {
await page.goto(externalSubnetsPage)
await clickRowAction(page, 'web-subnet', 'Edit')
diff --git a/test/e2e/floating-ip-update.e2e.ts b/test/e2e/floating-ip-update.e2e.ts
index 7aeb8498d..d60eeb486 100644
--- a/test/e2e/floating-ip-update.e2e.ts
+++ b/test/e2e/floating-ip-update.e2e.ts
@@ -50,6 +50,13 @@ test('can update a floating IP', async ({ page }) => {
await expectToast(page, `Floating IP ${updatedName} updated`)
})
+test('clicking the name opens the edit side modal', async ({ page }) => {
+ await page.goto(floatingIpsPage)
+ await page.getByRole('link', { name: originalName }).click()
+ await expect(page).toHaveURL(`${floatingIpsPage}/${originalName}/edit`)
+ await expectVisible(page, expectedFormElements)
+})
+
// Make sure that it still works even if the name doesn't change
test('can update *just* the floating IP description', async ({ page }) => {
// Go to the edit page for the original floating IP
diff --git a/test/e2e/vpcs.e2e.ts b/test/e2e/vpcs.e2e.ts
index 6df00b31b..1694c6a67 100644
--- a/test/e2e/vpcs.e2e.ts
+++ b/test/e2e/vpcs.e2e.ts
@@ -280,6 +280,14 @@ test('create router route', async ({ page }) => {
})
})
+test('clicking a route name opens the edit modal', async ({ page }) => {
+ const router = '/projects/mock-project/vpcs/mock-vpc/routers/mock-custom-router'
+ await page.goto(router)
+ await page.getByRole('link', { name: 'drop-local' }).click()
+ await expect(page).toHaveURL(`${router}/routes/drop-local/edit`)
+ await expect(page.getByRole('dialog', { name: 'Edit route' })).toBeVisible()
+})
+
test('edit and delete router route', async ({ page }) => {
await page.goto('/projects/mock-project/vpcs/mock-vpc/routers/mock-custom-router')