diff --git a/app/components/form/fields/FileField.tsx b/app/components/form/fields/FileField.tsx
index c1cb0442c..a3e396a14 100644
--- a/app/components/form/fields/FileField.tsx
+++ b/app/components/form/fields/FileField.tsx
@@ -46,7 +46,7 @@ export function FileField<
fieldState: { error },
} = useController({ name, control, rules: { required } })
return (
-
+
{label}
diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx
index 2ebe3d923..5c0941b68 100644
--- a/app/forms/instance-create.tsx
+++ b/app/forms/instance-create.tsx
@@ -5,7 +5,6 @@
*
* Copyright Oxide Computer Company
*/
-import * as Accordion from '@radix-ui/react-accordion'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useController, useForm, useWatch, type Control } from 'react-hook-form'
import { Link, useNavigate, type LoaderFunctionArgs } from 'react-router'
@@ -44,7 +43,6 @@ import {
Storage16Icon,
} from '@oxide/design-system/icons/react'
-import { AccordionItem } from '~/components/AccordionItem'
import { DocsPopover } from '~/components/DocsPopover'
import { CheckboxField } from '~/components/form/fields/CheckboxField'
import { ComboboxField } from '~/components/form/fields/ComboboxField'
@@ -720,13 +718,23 @@ export default function CreateInstanceForm() {
Authentication
- Advanced
- Networking
+
+
+ Advanced
+ }
+ name="userData"
+ label="User Data"
+ control={control}
+ disabled={isSubmitting}
+ />
Create instance
navigate(pb.instances({ project }))} />
@@ -753,7 +761,7 @@ const FloatingIpLabel = ({ ip }: { ip: FloatingIp }) => (
)
-const AdvancedAccordion = ({
+const NetworkingSection = ({
control,
isSubmitting,
unicastPools,
@@ -765,10 +773,6 @@ const AdvancedAccordion = ({
hasVpcs: boolean
}) => {
const networkInterfaces = useWatch({ control, name: 'networkInterfaces' })
- // we track this state manually for the sole reason that we need to be able to
- // tell, inside AccordionItem, when an accordion is opened so we can scroll its
- // contents into view
- const [openItems, setOpenItems] = useState
([])
const [floatingIpModalOpen, setFloatingIpModalOpen] = useState(false)
const [selectedFloatingIp, setSelectedFloatingIp] = useState()
const assignEphemeralIpField = useController({ control, name: 'assignEphemeralIp' })
@@ -919,184 +923,153 @@ const AdvancedAccordion = ({
)
return (
-
-
- {!hasVpcs && (
-
- A VPC is required to add network interfaces.{' '}
- Create a VPC to enable networking.
- >
- }
- />
- )}
-
+ {!hasVpcs && (
+
+ A VPC is required to add network interfaces.{' '}
+ Create a VPC to enable networking.
+ >
+ }
+ />
+ )}
+
+
+
+
+ Ephemeral IP{' '}
+
+ Ephemeral IPs are allocated when the instance is created and deallocated when it
+ is deleted
+
+
+
+ }
+ >
+ {/* TODO: Wrapping the checkbox in a makes it so the tooltip
+ * shows up when you hover anywhere on the label or checkbox, not
+ * just the checkbox itself. The downside is the placement of the tooltip
+ * is a little weird (I'd like it better if it was anchored to the checkbox),
+ * but I think having it show up on label hover is worth it.
+ */}
+
+ {
+ assignEphemeralIpField.field.onChange(!assignEphemeralIp)
+ }}
+ >
+ Allocate and attach an ephemeral IP address
+
+
+
+
+
-
-
- Ephemeral IP{' '}
-
- Ephemeral IPs are allocated when the instance is created and deallocated when
- it is deleted
-
-
-
- }
- >
- {/* TODO: Wrapping the checkbox in a makes it so the tooltip
- * shows up when you hover anywhere on the label or checkbox, not
- * just the checkbox itself. The downside is the placement of the tooltip
- * is a little weird (I'd like it better if it was anchored to the checkbox),
- * but I think having it show up on label hover is worth it.
- */}
-
- {
- assignEphemeralIpField.field.onChange(!assignEphemeralIp)
- }}
- >
- Allocate and attach an ephemeral IP address
-
-
-
-
-
-
-
-
- Floating IPs{' '}
-
- Floating IPs exist independently of instances and can be attached to and
- detached from them as needed
-
-
- {floatingIpList.items.length === 0 ? (
-
- }
- title="No floating IPs found"
- body="Create a floating IP to attach it to this instance"
- />
-
- ) : (
-
- item.name },
- { header: 'IP', cell: (item) => item.ip },
- ]}
- rowKey={(item) => item.name}
- onRemoveItem={(item) => detachFloatingIp(item.name)}
- removeLabel={(item) => `remove floating IP ${item.name}`}
- />
-
- A network interface is required
-
- to attach a floating IP
- >
- ) : availableFloatingIps.length === 0 ? (
- 'No floating IPs available'
- ) : undefined
- }
- onClick={() => setFloatingIpModalOpen(true)}
- >
- Attach floating IP
-
-
- )}
-
+
+ Floating IPs{' '}
+
+ Floating IPs exist independently of instances and can be attached to and
+ detached from them as needed
+
+
+ {floatingIpList.items.length === 0 ? (
+
+ }
+ title="No floating IPs found"
+ body="Create a floating IP to attach it to this instance"
+ />
+
+ ) : (
+
+ item.name },
+ { header: 'IP', cell: (item) => item.ip },
+ ]}
+ rowKey={(item) => item.name}
+ onRemoveItem={(item) => detachFloatingIp(item.name)}
+ removeLabel={(item) => `remove floating IP ${item.name}`}
+ />
+
+ A network interface is required
+
+ to attach a floating IP
+ >
+ ) : availableFloatingIps.length === 0 ? (
+ 'No floating IPs available'
+ ) : undefined
+ }
+ onClick={() => setFloatingIpModalOpen(true)}
+ >
+ Attach floating IP
+
+
+ )}
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- }
- name="userData"
- label="User Data"
- control={control}
- disabled={isSubmitting}
- />
-
-
+ >
+
+
+ >
)
}
diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts
index a839759be..484b08798 100644
--- a/test/e2e/instance-create.e2e.ts
+++ b/test/e2e/instance-create.e2e.ts
@@ -61,16 +61,8 @@ test('can create an instance', async ({ page }) => {
// pick a project image just to show we can
await selectAProjectImage(page, 'image-3')
- // should be hidden in accordion
- await expectNotVisible(page, [
- 'role=radiogroup[name="Network interface"]',
- 'role=textbox[name="Hostname"]',
- 'text="User Data"',
- ])
-
- // open networking and config accordions
- await page.getByRole('button', { name: 'Networking' }).click()
- await page.getByRole('button', { name: 'Configuration' }).click()
+ // hostname field should not exist
+ await expectNotVisible(page, ['role=textbox[name="Hostname"]'])
const checkbox = page.getByRole('checkbox', {
name: 'Allocate and attach an ephemeral IP address',
@@ -102,7 +94,6 @@ test('can create an instance', async ({ page }) => {
// Force click since there might be overlays
await page.getByRole('option', { name: 'ip-pool-2' }).click({ force: true })
- // should be visible in accordion
await expect(page.getByRole('radiogroup', { name: 'Network interface' })).toBeVisible()
await expect(page.getByLabel('User data')).toBeVisible()
@@ -146,7 +137,6 @@ test('can create an instance', async ({ page }) => {
test('ephemeral pool selection tracks network interface IP version', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
- await page.getByRole('button', { name: 'Networking' }).click()
const poolDropdown = page.getByLabel('Pool')
await expect(poolDropdown).toBeVisible()
@@ -443,7 +433,7 @@ test('does not attach an ephemeral IP when the checkbox is unchecked', async ({
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('no-ephemeral-ip')
await selectAProjectImage(page, 'image-1')
- await page.getByRole('button', { name: 'Networking' }).click()
+
await page
.getByRole('checkbox', { name: 'Allocate and attach an ephemeral IP address' })
.uncheck()
@@ -463,7 +453,6 @@ test('attaches a floating IP; disables button when no IPs available', async ({ p
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectAProjectImage(page, 'image-1')
- await page.getByRole('button', { name: 'Networking' }).click()
await attachFloatingIpButton.click()
await expect(
@@ -531,7 +520,7 @@ test('attach a floating IP section has Empty version when no floating IPs exist
page,
}) => {
await page.goto('/projects/other-project/instances-new')
- await page.getByRole('button', { name: 'Networking' }).click()
+
await expect(page.getByRole('button', { name: 'Attach floating IP' })).toBeHidden()
await expect(
page.getByText('Create a floating IP to attach it to this instance')
@@ -704,8 +693,7 @@ test('create instance with IPv6-only networking', async ({ page }) => {
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Configure networking
// Ensure "Default" network interface is selected
const defaultRadio = page.getByRole('radio', { name: 'Default', exact: true })
@@ -748,8 +736,7 @@ test('create instance with IPv4-only networking', async ({ page }) => {
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Configure networking
// Ensure "Default" network interface is selected
const defaultRadio = page.getByRole('radio', { name: 'Default', exact: true })
@@ -791,8 +778,7 @@ test('create instance with dual-stack networking shows both IPs', async ({ page
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Configure networking
// Default is already "Default IPv4 & IPv6", so no need to select it
@@ -831,8 +817,7 @@ test('create instance with custom IPv4-only NIC constrains ephemeral IP to IPv4'
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Configure networking
// Select "Custom" network interface (use exact match and first to disambiguate from "custom pool")
await page.getByRole('radio', { name: 'Custom', exact: true }).first().click()
@@ -893,8 +878,7 @@ test('create instance with custom IPv6-only NIC constrains ephemeral IP to IPv6'
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Configure networking
// Select "Custom" network interface (use exact match and first to disambiguate from "custom pool")
await page.getByRole('radio', { name: 'Custom', exact: true }).first().click()
@@ -955,8 +939,7 @@ test('create instance with custom dual-stack NIC allows both IPv4 and IPv6 ephem
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Configure networking
// Select "Custom" network interface (use exact match and first to disambiguate from "custom pool")
await page.getByRole('radio', { name: 'Custom', exact: true }).first().click()
@@ -1013,8 +996,7 @@ test('ephemeral IP checkbox disabled when no NICs configured', async ({ page })
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Configure networking
const ephemeralCheckbox = page.getByRole('checkbox', {
name: 'Allocate and attach an ephemeral IP address',
@@ -1095,8 +1077,7 @@ test('network interface options disabled when no VPCs exist', async ({ page }) =
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Configure networking
// Get radio button elements
const defaultRadio = page.getByRole('radio', { name: 'Default', exact: true })
@@ -1119,8 +1100,7 @@ test('network interface options disabled when no VPCs exist', async ({ page }) =
test('floating IPs are filtered by NIC IP version', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Configure networking
// Select IPv4-only networking
const defaultRadio = page.getByRole('radio', { name: 'Default', exact: true })
diff --git a/test/e2e/instance-networking.e2e.ts b/test/e2e/instance-networking.e2e.ts
index 0c642494c..ade1a66db 100644
--- a/test/e2e/instance-networking.e2e.ts
+++ b/test/e2e/instance-networking.e2e.ts
@@ -331,8 +331,8 @@ test('IPv4-only instance cannot attach IPv6 ephemeral IP', async ({ page }) => {
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion and select IPv4-only
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Select IPv4-only
+
const defaultRadio = page.getByRole('radio', { name: 'Default', exact: true })
if (!(await defaultRadio.isChecked())) {
await defaultRadio.click()
@@ -386,8 +386,8 @@ test('IPv6-only instance cannot attach IPv4 ephemeral IP', async ({ page }) => {
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion and select IPv6-only
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Select IPv6-only
+
const defaultRadio = page.getByRole('radio', { name: 'Default', exact: true })
if (!(await defaultRadio.isChecked())) {
await defaultRadio.click()
@@ -441,8 +441,8 @@ test('IPv4-only instance can attach IPv4 ephemeral IP', async ({ page }) => {
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion and select IPv4-only
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Select IPv4-only
+
const defaultRadio = page.getByRole('radio', { name: 'Default', exact: true })
if (!(await defaultRadio.isChecked())) {
await defaultRadio.click()
@@ -490,8 +490,8 @@ test('IPv6-only instance can attach IPv6 ephemeral IP', async ({ page }) => {
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
- // Open networking accordion and select IPv6-only
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Select IPv6-only
+
const defaultRadio = page.getByRole('radio', { name: 'Default', exact: true })
if (!(await defaultRadio.isChecked())) {
await defaultRadio.click()
diff --git a/test/e2e/ip-pool-silo-config.e2e.ts b/test/e2e/ip-pool-silo-config.e2e.ts
index 9f5cea0be..6d68085f7 100644
--- a/test/e2e/ip-pool-silo-config.e2e.ts
+++ b/test/e2e/ip-pool-silo-config.e2e.ts
@@ -37,9 +37,6 @@ test.describe('IP pool configuration: myriad silo (v4-only default)', () => {
await page.getByPlaceholder('Select a silo image', { exact: true }).click()
await page.getByRole('option', { name: 'ubuntu-22-04' }).click()
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
-
// Verify ephemeral IP checkbox is checked by default
const ephemeralCheckbox = page.getByRole('checkbox', {
name: 'Allocate and attach an ephemeral IP address',
@@ -83,8 +80,7 @@ test.describe('IP pool configuration: myriad silo (v4-only default)', () => {
await page.getByPlaceholder('Select a silo image', { exact: true }).click()
await page.getByRole('option', { name: 'ubuntu-22-04' }).click()
- // Open networking accordion and verify ephemeral IP defaults
- await page.getByRole('button', { name: 'Networking' }).click()
+ // Verify ephemeral IP defaults
const ephemeralCheckbox = page.getByRole('checkbox', {
name: 'Allocate and attach an ephemeral IP address',
})
@@ -213,9 +209,6 @@ test.describe('IP pool configuration: thrax silo (v6-only default)', () => {
await page.getByPlaceholder('Select a silo image', { exact: true }).click()
await page.getByRole('option', { name: 'ubuntu-22-04' }).click()
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
-
// Verify ephemeral IP checkbox is checked by default
const ephemeralCheckbox = page.getByRole('checkbox', {
name: 'Allocate and attach an ephemeral IP address',
@@ -262,9 +255,6 @@ test.describe('IP pool configuration: pelerines silo (no defaults)', () => {
await page.getByPlaceholder('Select a silo image', { exact: true }).click()
await page.getByRole('option', { name: 'ubuntu-22-04' }).click()
- // Open networking accordion
- await page.getByRole('button', { name: 'Networking' }).click()
-
// Verify ephemeral IP checkbox is not checked by default
const ephemeralCheckbox = page.getByRole('checkbox', {
name: 'Allocate and attach an ephemeral IP address',
@@ -314,8 +304,6 @@ test.describe('IP pool configuration: no-pools silo (no IP pools)', () => {
await page.getByPlaceholder('Select a silo image', { exact: true }).click()
await page.getByRole('option', { name: 'ubuntu-22-04' }).click()
- await page.getByRole('button', { name: 'Networking' }).click()
-
const defaultRadio = page.getByRole('radio', { name: 'Default' })
await expect(defaultRadio).toBeChecked()