diff --git a/app/components/AttachEphemeralIpModal.tsx b/app/components/AttachEphemeralIpModal.tsx
index 878021a11f..f50c83589d 100644
--- a/app/components/AttachEphemeralIpModal.tsx
+++ b/app/components/AttachEphemeralIpModal.tsx
@@ -13,10 +13,11 @@ import { useApiMutation, useApiQueryClient, usePrefetchedApiQuery } from '~/api'
import { ListboxField } from '~/components/form/fields/ListboxField'
import { useInstanceSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
-import { Badge } from '~/ui/lib/Badge'
import { Modal } from '~/ui/lib/Modal'
import { ALL_ISH } from '~/util/consts'
+import { toIpPoolItem } from './form/fields/ip-pool-item'
+
export const AttachEphemeralIpModal = ({ onDismiss }: { onDismiss: () => void }) => {
const queryClient = useApiQueryClient()
const { project, instance } = useInstanceSelector()
@@ -54,17 +55,7 @@ export const AttachEphemeralIpModal = ({ onDismiss }: { onDismiss: () => void })
? 'Select a pool'
: 'No pools available'
}
- items={
- siloPools?.items.map((pool) => ({
- label: (
-
- {pool.name}
- {pool.isDefault && default}
-
- ),
- value: pool.name,
- })) || []
- }
+ items={siloPools.items.map(toIpPoolItem)}
required
/>
diff --git a/app/components/form/fields/ImageSelectField.tsx b/app/components/form/fields/ImageSelectField.tsx
index 2254c0cd00..0f2307c063 100644
--- a/app/components/form/fields/ImageSelectField.tsx
+++ b/app/components/form/fields/ImageSelectField.tsx
@@ -80,10 +80,10 @@ export function toImageComboboxItem(
value: id,
selectedLabel: name,
label: (
- <>
+
{name}
{itemMetadata}
- >
+
),
}
}
diff --git a/app/components/form/fields/ip-pool-item.tsx b/app/components/form/fields/ip-pool-item.tsx
new file mode 100644
index 0000000000..eafed84c71
--- /dev/null
+++ b/app/components/form/fields/ip-pool-item.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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 type { SiloIpPool } from '~/api'
+import { Badge } from '~/ui/lib/Badge'
+
+export function toIpPoolItem(p: SiloIpPool) {
+ const value = p.name
+ const selectedLabel = p.name
+ const label = (
+
+
+ {p.name}
+ {p.isDefault && (
+
+ default
+
+ )}
+
+ {p.description.length && (
+
{p.description}
+ )}
+
+ )
+ return { value, selectedLabel, label }
+}
diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx
index 77424696c7..65a9d742bf 100644
--- a/app/forms/floating-ip-create.tsx
+++ b/app/forms/floating-ip-create.tsx
@@ -15,40 +15,20 @@ import {
useApiQuery,
useApiQueryClient,
type FloatingIpCreate,
- type SiloIpPool,
} from '@oxide/api'
import { AccordionItem } from '~/components/AccordionItem'
import { DescriptionField } from '~/components/form/fields/DescriptionField'
+import { toIpPoolItem } from '~/components/form/fields/ip-pool-item'
import { ListboxField } from '~/components/form/fields/ListboxField'
import { NameField } from '~/components/form/fields/NameField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { useProjectSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
-import { Badge } from '~/ui/lib/Badge'
import { Message } from '~/ui/lib/Message'
import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'
-const toListboxItem = (p: SiloIpPool) => {
- if (!p.isDefault) {
- return { value: p.name, label: p.name }
- }
- // For the default pool, add a label to the dropdown
- return {
- value: p.name,
- selectedLabel: p.name,
- label: (
- <>
- {p.name}{' '}
-
- default
-
- >
- ),
- }
-}
-
const defaultValues: Omit = {
name: '',
description: '',
@@ -108,7 +88,7 @@ export function CreateFloatingIpSideModalForm() {
toListboxItem(p))}
+ items={(allPools?.items || []).map(toIpPoolItem)}
label="IP pool"
control={form.control}
placeholder="Select a pool"
diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx
index 35f91958c5..be7e9eaa2d 100644
--- a/app/forms/instance-create.tsx
+++ b/app/forms/instance-create.tsx
@@ -25,6 +25,7 @@ import {
type InstanceCreate,
type InstanceDiskAttachment,
type NameOrId,
+ type SiloIpPool,
} from '@oxide/api'
import {
Images16Icon,
@@ -46,6 +47,7 @@ import {
} from '~/components/form/fields/DisksTableField'
import { FileField } from '~/components/form/fields/FileField'
import { BootDiskImageSelectField as ImageSelectField } from '~/components/form/fields/ImageSelectField'
+import { toIpPoolItem } from '~/components/form/fields/ip-pool-item'
import { NameField } from '~/components/form/fields/NameField'
import { NetworkInterfaceField } from '~/components/form/fields/NetworkInterfaceField'
import { NumberField } from '~/components/form/fields/NumberField'
@@ -57,7 +59,6 @@ import { FullPageForm } from '~/components/form/FullPageForm'
import { HL } from '~/components/HL'
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
-import { Badge } from '~/ui/lib/Badge'
import { Button } from '~/ui/lib/Button'
import { Checkbox } from '~/ui/lib/Checkbox'
import { toComboboxItems } from '~/ui/lib/Combobox'
@@ -609,7 +610,7 @@ const AdvancedAccordion = ({
}: {
control: Control
isSubmitting: boolean
- siloPools: Array<{ name: string; isDefault: boolean }>
+ siloPools: Array
}) => {
// 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
@@ -733,17 +734,7 @@ const AdvancedAccordion = ({
label="IP pool for ephemeral IP"
placeholder={defaultPool ? `${defaultPool} (default)` : 'Select a pool'}
selected={`${siloPools.find((pool) => pool.name === selectedPool)?.name}`}
- items={
- siloPools.map((pool) => ({
- label: (
-
- {pool.name}
- {pool.isDefault && default}
-
- ),
- value: pool.name,
- })) || []
- }
+ items={siloPools.map(toIpPoolItem)}
disabled={!assignEphemeralIp || isSubmitting}
required
onChange={(value) => {
diff --git a/app/forms/ip-pool-create.tsx b/app/forms/ip-pool-create.tsx
index c91e8d8d31..e1f87b15d9 100644
--- a/app/forms/ip-pool-create.tsx
+++ b/app/forms/ip-pool-create.tsx
@@ -14,6 +14,7 @@ import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { addToast } from '~/stores/toast'
+import { Message } from '~/ui/lib/Message'
import { pb } from '~/util/path-builder'
const defaultValues: IpPoolCreate = {
@@ -51,6 +52,14 @@ export function CreateIpPoolSideModalForm() {
>
+
)
}
+
+export const IpPoolVisibilityMessage = () => (
+
+)
diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx
index 73e2c942c5..2b1a15978e 100644
--- a/app/forms/ip-pool-edit.tsx
+++ b/app/forms/ip-pool-edit.tsx
@@ -22,6 +22,8 @@ import { getIpPoolSelector, useIpPoolSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'
+import { IpPoolVisibilityMessage } from './ip-pool-create'
+
EditIpPoolSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { pool } = getIpPoolSelector(params)
await apiQueryClient.prefetchQuery('ipPoolView', { path: { pool } })
@@ -68,6 +70,7 @@ export function EditIpPoolSideModalForm() {
>
+
)
}
diff --git a/app/ui/styles/components/menu-list.css b/app/ui/styles/components/menu-list.css
index 4241dec636..3aa0d0ff10 100644
--- a/app/ui/styles/components/menu-list.css
+++ b/app/ui/styles/components/menu-list.css
@@ -28,6 +28,9 @@
.ox-menu-item.is-selected {
@apply border-0 text-accent bg-accent-secondary hover:bg-accent-secondary-hover;
+ .ox-badge {
+ @apply ring-0 text-inverse bg-accent;
+ }
}
/* beautiful ring */
diff --git a/test/e2e/floating-ip-create.e2e.ts b/test/e2e/floating-ip-create.e2e.ts
index dfe5af2091..4bedc596ce 100644
--- a/test/e2e/floating-ip-create.e2e.ts
+++ b/test/e2e/floating-ip-create.e2e.ts
@@ -28,19 +28,19 @@ test('can create a floating IP', async ({ page }) => {
.getByRole('textbox', { name: 'Description' })
.fill('A description for this Floating IP')
- const poolListbox = page.getByRole('button', { name: 'IP pool' })
+ const label = page.getByLabel('IP pool')
// accordion content should be hidden
- await expect(poolListbox).toBeHidden()
+ await expect(label).toBeHidden()
// open accordion
await page.getByRole('button', { name: 'Advanced' }).click()
// accordion content should be visible
- await expect(poolListbox).toBeVisible()
+ await expect(label).toBeVisible()
// choose pool and submit
- await poolListbox.click()
+ await label.click()
await page.getByRole('option', { name: 'ip-pool-1' }).click()
await page.getByRole('button', { name: 'Create floating IP' }).click()
diff --git a/test/e2e/instance-create.e2e.ts b/test/e2e/instance-create.e2e.ts
index 628b92b9b5..e2e2125100 100644
--- a/test/e2e/instance-create.e2e.ts
+++ b/test/e2e/instance-create.e2e.ts
@@ -70,27 +70,23 @@ test('can create an instance', async ({ page }) => {
await page.getByRole('button', { name: 'Networking' }).click()
await page.getByRole('button', { name: 'Configuration' }).click()
- const assignEphemeralIpCheckbox = page.getByRole('checkbox', {
+ const checkbox = page.getByRole('checkbox', {
name: 'Allocate and attach an ephemeral IP address',
})
- const assignEphemeralIpButton = page.getByRole('button', {
- name: 'IP pool for ephemeral IP',
- })
+ const label = page.getByLabel('IP pool for ephemeral IP')
// verify that the ip pool selector is visible and default is selected
- await expect(assignEphemeralIpCheckbox).toBeChecked()
- await assignEphemeralIpButton.click()
+ await expect(checkbox).toBeChecked()
+ await label.click()
await expect(page.getByRole('option', { name: 'ip-pool-1' })).toBeEnabled()
- await assignEphemeralIpButton.click() // click closes the listbox so we can do more stuff
// unchecking the box should disable the selector
- await assignEphemeralIpCheckbox.uncheck()
- await expect(assignEphemeralIpButton).toBeHidden()
+ await checkbox.uncheck()
+ await expect(label).toBeHidden()
// re-checking the box should re-enable the selector, and other options should be selectable
- await assignEphemeralIpCheckbox.check()
- await assignEphemeralIpButton.click()
- await page.getByRole('option', { name: 'ip-pool-2' }).click()
+ await checkbox.check()
+ await selectOption(page, 'IP pool for ephemeral IP', 'ip-pool-2 VPN IPs')
// should be visible in accordion
await expect(page.getByRole('radiogroup', { name: 'Network interface' })).toBeVisible()