From 0da891004e3497550ba2e8d3d46df940f8cfe1d9 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 14 May 2026 14:19:52 -0700 Subject: [PATCH 1/2] add affordance to direct user to firewall rules tab --- app/pages/project/instances/NetworkingTab.tsx | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/pages/project/instances/NetworkingTab.tsx b/app/pages/project/instances/NetworkingTab.tsx index 10817eac6..d8d09b1a6 100644 --- a/app/pages/project/instances/NetworkingTab.tsx +++ b/app/pages/project/instances/NetworkingTab.tsx @@ -9,7 +9,7 @@ import { useQuery } from '@tanstack/react-query' import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table' import { useCallback, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' -import { type LoaderFunctionArgs } from 'react-router' +import { Link, type LoaderFunctionArgs } from 'react-router' import { match } from 'ts-pattern' import { @@ -59,6 +59,7 @@ import { Button } from '~/ui/lib/Button' import { CardBlock } from '~/ui/lib/CardBlock' import { CopyableIp } from '~/ui/lib/CopyableIp' import { EmptyMessage } from '~/ui/lib/EmptyMessage' +import { Message } from '~/ui/lib/Message' import { TableEmptyBox } from '~/ui/lib/Table' import { TipIcon } from '~/ui/lib/TipIcon' import { Tooltip } from '~/ui/lib/Tooltip' @@ -385,6 +386,20 @@ export default function NetworkingTab() { q(api.instanceView, { path: { instance: instanceName }, query: { project } }) ) + // If every NIC sits in the same VPC, link straight to its firewall rules; + // otherwise send users to the VPCs list to pick. Primary NIC's VPC is + // prefetched by InstancePage's loader, so this hits the cache. + const primaryNic = nics.find((n) => n.primary) + const { data: primaryVpc } = useQuery({ + ...q(api.vpcView, { path: { vpc: primaryNic?.vpcId ?? '' } }), + enabled: !!primaryNic, + }) + const singleVpc = new Set(nics.map((n) => n.vpcId)).size === 1 + const firewallLink = + singleVpc && primaryVpc + ? pb.vpcFirewallRules({ project, vpc: primaryVpc.name }) + : pb.vpcs({ project }) + const multipleNics = nics.length > 1 const makeActions = useCallback( @@ -633,6 +648,17 @@ export default function NetworkingTab() { return (
+ {nics.length > 0 && ( + + Edit firewall rules on{' '} + {singleVpc ? 'the VPC' : 'the relevant VPC'} + + } + /> + )}
From e4d58a1f855e0fa6d20346ad01e5f018d361ce19 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 1 Jun 2026 16:25:50 -0500 Subject: [PATCH 2/2] do it in a box --- app/pages/project/instances/NetworkingTab.tsx | 57 ++++++++++--------- test/e2e/instance-networking.e2e.ts | 19 +++++++ 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/app/pages/project/instances/NetworkingTab.tsx b/app/pages/project/instances/NetworkingTab.tsx index d8d09b1a6..8c89087e4 100644 --- a/app/pages/project/instances/NetworkingTab.tsx +++ b/app/pages/project/instances/NetworkingTab.tsx @@ -59,7 +59,6 @@ import { Button } from '~/ui/lib/Button' import { CardBlock } from '~/ui/lib/CardBlock' import { CopyableIp } from '~/ui/lib/CopyableIp' import { EmptyMessage } from '~/ui/lib/EmptyMessage' -import { Message } from '~/ui/lib/Message' import { TableEmptyBox } from '~/ui/lib/Table' import { TipIcon } from '~/ui/lib/TipIcon' import { Tooltip } from '~/ui/lib/Tooltip' @@ -348,6 +347,16 @@ export default function NetworkingTab() { }) ).data.items + // Firewall rules and other external networking state live on the VPC of the + // primary NIC. The parent InstancePage loader prefetches this VPC, but + // primaryNic may be undefined, so we can't use usePrefetchedQuery. + const primaryVpcId = nics.find((nic) => nic.primary)?.vpcId + const { data: primaryVpc } = useQuery({ + // primaryVpcId is defined when enabled, so the assertion is safe + ...q(api.vpcView, { path: { vpc: primaryVpcId! } }), + enabled: !!primaryVpcId, + }) + const { data: siloPools } = usePrefetchedQuery( q(api.ipPoolList, { query: { limit: ALL_ISH } }) ) @@ -386,20 +395,6 @@ export default function NetworkingTab() { q(api.instanceView, { path: { instance: instanceName }, query: { project } }) ) - // If every NIC sits in the same VPC, link straight to its firewall rules; - // otherwise send users to the VPCs list to pick. Primary NIC's VPC is - // prefetched by InstancePage's loader, so this hits the cache. - const primaryNic = nics.find((n) => n.primary) - const { data: primaryVpc } = useQuery({ - ...q(api.vpcView, { path: { vpc: primaryNic?.vpcId ?? '' } }), - enabled: !!primaryNic, - }) - const singleVpc = new Set(nics.map((n) => n.vpcId)).size === 1 - const firewallLink = - singleVpc && primaryVpc - ? pb.vpcFirewallRules({ project, vpc: primaryVpc.name }) - : pb.vpcs({ project }) - const multipleNics = nics.length > 1 const makeActions = useCallback( @@ -648,17 +643,6 @@ export default function NetworkingTab() { return (
- {nics.length > 0 && ( - - Edit firewall rules on{' '} - {singleVpc ? 'the VPC' : 'the relevant VPC'} - - } - /> - )}
@@ -765,7 +749,7 @@ export default function NetworkingTab() { - +
) } diff --git a/test/e2e/instance-networking.e2e.ts b/test/e2e/instance-networking.e2e.ts index 9c32f23ca..0b0117e58 100644 --- a/test/e2e/instance-networking.e2e.ts +++ b/test/e2e/instance-networking.e2e.ts @@ -22,6 +22,25 @@ const selectASiloImage = async (page: Page, name: string) => { await page.getByRole('option', { name }).click() } +test('Instance networking tab — firewall rules card', async ({ page }) => { + // db1 has a primary NIC in mock-vpc, so the card names that VPC + await page.goto('/projects/mock-project/instances/db1/networking') + await expect( + page.getByText('Manage firewall rules affecting this instance in VPC mock-vpc') + ).toBeVisible() + + // you-fail has no NICs, so there's no primary VPC and we show the fallback copy + await page.goto('/projects/mock-project/instances/you-fail/networking') + await expect( + page.getByText( + 'Firewall rules are managed on the VPC associated with the primary network interface.' + ) + ).toBeVisible() + await expect( + page.getByText('Manage firewall rules affecting this instance in VPC') + ).toBeHidden() +}) + test('Instance networking tab — NIC table', async ({ page }) => { await page.goto('/projects/mock-project/instances/db1')