From 1328624a353943bcad8dee0683ce6b85a071159c Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 17 Nov 2025 16:46:31 -0800 Subject: [PATCH 1/6] Fetch and prefill VPC list in NIC forms --- app/forms/instance-create.tsx | 2 ++ app/forms/network-interface-create.tsx | 26 ++++++++++++++----- app/pages/project/instances/NetworkingTab.tsx | 2 ++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 6246a655ee..44fa2328f5 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -170,6 +170,8 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { queryClient.prefetchQuery( q(api.floatingIpList, { query: { project, limit: ALL_ISH } }) ), + // Fetch VPCs for custom NIC form + queryClient.prefetchQuery(q(api.vpcList, { query: { project, limit: ALL_ISH } })), ]) return null } diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index b4fc54b74e..0ab0e257d4 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -5,12 +5,17 @@ * * Copyright Oxide Computer Company */ -import { useQuery } from '@tanstack/react-query' -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { useForm } from 'react-hook-form' import type { SetNonNullable, SetRequired } from 'type-fest' -import { api, q, type ApiError, type InstanceNetworkInterfaceCreate } from '@oxide/api' +import { + api, + q, + usePrefetchedQuery, + type ApiError, + type InstanceNetworkInterfaceCreate, +} from '@oxide/api' import { DescriptionField } from '~/components/form/fields/DescriptionField' import { ListboxField } from '~/components/form/fields/ListboxField' @@ -20,6 +25,7 @@ import { TextField } from '~/components/form/fields/TextField' import { SideModalForm } from '~/components/form/SideModalForm' import { useProjectSelector } from '~/hooks/use-params' import { FormDivider } from '~/ui/lib/Divider' +import { ALL_ISH } from '~/util/consts' const defaultValues: SetRequired, 'ip'> = { name: '', @@ -47,12 +53,20 @@ export function CreateNetworkInterfaceForm({ submitError = null, }: CreateNetworkInterfaceFormProps) { const projectSelector = useProjectSelector() - - const { data: vpcsData } = useQuery(q(api.vpcList, { query: projectSelector })) - const vpcs = useMemo(() => vpcsData?.items || [], [vpcsData]) + const { data: vpcsData } = usePrefetchedQuery( + q(api.vpcList, { query: { ...projectSelector, limit: ALL_ISH } }) + ) + const vpcs = useMemo(() => vpcsData.items, [vpcsData]) const form = useForm({ defaultValues }) + // prefill form with first VPC + useEffect(() => { + if (vpcs.length > 0 && !form.getValues('vpcName')) { + form.setValue('vpcName', vpcs[0].name) + } + }, [vpcs, form]) + return ( Date: Mon, 17 Nov 2025 17:18:14 -0800 Subject: [PATCH 2/6] use prefetched data in defaultValues --- app/forms/network-interface-create.tsx | 34 +++++++++----------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index 0ab0e257d4..93038857a7 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -5,9 +5,7 @@ * * Copyright Oxide Computer Company */ -import { useEffect, useMemo } from 'react' import { useForm } from 'react-hook-form' -import type { SetNonNullable, SetRequired } from 'type-fest' import { api, @@ -27,14 +25,6 @@ import { useProjectSelector } from '~/hooks/use-params' import { FormDivider } from '~/ui/lib/Divider' import { ALL_ISH } from '~/util/consts' -const defaultValues: SetRequired, 'ip'> = { - name: '', - description: '', - ip: '', - subnetName: '', - vpcName: '', -} - type CreateNetworkInterfaceFormProps = { onDismiss: () => void onSubmit: (values: InstanceNetworkInterfaceCreate) => void @@ -53,20 +43,18 @@ export function CreateNetworkInterfaceForm({ submitError = null, }: CreateNetworkInterfaceFormProps) { const projectSelector = useProjectSelector() - const { data: vpcsData } = usePrefetchedQuery( - q(api.vpcList, { query: { ...projectSelector, limit: ALL_ISH } }) - ) - const vpcs = useMemo(() => vpcsData.items, [vpcsData]) - + const { + data: { items: vpcs }, + } = usePrefetchedQuery(q(api.vpcList, { query: { ...projectSelector, limit: ALL_ISH } })) + const defaultValues = { + name: '', + description: '', + ip: '', + subnetName: '', + // Preselect first VPC + vpcName: vpcs.length > 0 ? vpcs[0].name : '', + } const form = useForm({ defaultValues }) - - // prefill form with first VPC - useEffect(() => { - if (vpcs.length > 0 && !form.getValues('vpcName')) { - form.setValue('vpcName', vpcs[0].name) - } - }, [vpcs, form]) - return ( Date: Mon, 17 Nov 2025 17:20:10 -0800 Subject: [PATCH 3/6] remove comment --- app/forms/network-interface-create.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index 93038857a7..e4292d68d6 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -51,7 +51,6 @@ export function CreateNetworkInterfaceForm({ description: '', ip: '', subnetName: '', - // Preselect first VPC vpcName: vpcs.length > 0 ? vpcs[0].name : '', } const form = useForm({ defaultValues }) From d3be9327271ac35d13d27bd3d86711005f08ed66 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 17 Nov 2025 17:34:32 -0800 Subject: [PATCH 4/6] add required type Co-authored-by: David Crespo --- app/forms/network-interface-create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index e4292d68d6..ac1876e6bd 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -46,7 +46,7 @@ export function CreateNetworkInterfaceForm({ const { data: { items: vpcs }, } = usePrefetchedQuery(q(api.vpcList, { query: { ...projectSelector, limit: ALL_ISH } })) - const defaultValues = { + const defaultValues: SetRequired, 'ip'> = { name: '', description: '', ip: '', From d57d243bfddc535c1ed86a3cdfde8b52c6eb0ea5 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 17 Nov 2025 17:35:26 -0800 Subject: [PATCH 5/6] fix imports --- app/forms/network-interface-create.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index ac1876e6bd..14b62ee2e3 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -6,6 +6,7 @@ * Copyright Oxide Computer Company */ import { useForm } from 'react-hook-form' +import type { SetNonNullable, SetRequired } from 'type-fest' import { api, From 2da5a80c7ad2927c38d20f783b76ef014fc1a2f8 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 7 Jul 2026 17:57:34 -0500 Subject: [PATCH 6/6] only preselect VPC when there's exactly one --- app/forms/network-interface-create.tsx | 2 +- test/e2e/network-interface-create.e2e.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index 2c984f0bba..f4bde1cbbb 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -79,7 +79,7 @@ export function CreateNetworkInterfaceForm({ name: '', description: '', subnetName: '', - vpcName: vpcs.length > 0 ? vpcs[0].name : '', + vpcName: vpcs.length === 1 ? vpcs[0].name : '', ipStackType: 'dual_stack', ipv4: '', ipv6: '', diff --git a/test/e2e/network-interface-create.e2e.ts b/test/e2e/network-interface-create.e2e.ts index 341c43dc5d..b3698856bd 100644 --- a/test/e2e/network-interface-create.e2e.ts +++ b/test/e2e/network-interface-create.e2e.ts @@ -18,8 +18,8 @@ test('can create a NIC with a specified IP address', async ({ page }) => { // fill out the form await page.getByLabel('Name').fill('nic-1') - await page.getByLabel('VPC', { exact: true }).click() - await page.getByRole('option', { name: 'mock-vpc' }).click() + // VPC is preselected because the project has exactly one + await expect(page.getByLabel('VPC', { exact: true })).toContainText('mock-vpc') await page.getByRole('dialog').getByRole('button', { name: 'VPC subnet' }).click() await page.getByRole('option', { name: 'mock-subnet', exact: true }).click()