From df0dea4afc4eaa02296d4225a40bd3f6d1a7706b Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 29 Oct 2024 11:03:04 -0700 Subject: [PATCH] Add descriptions to IP Pool dropdowns (#2514) Co-authored-by: David Crespo --- app/components/AttachEphemeralIpModal.tsx | 15 ++-------- .../form/fields/ImageSelectField.tsx | 4 +-- app/components/form/fields/ip-pool-item.tsx | 30 +++++++++++++++++++ app/forms/floating-ip-create.tsx | 24 ++------------- app/forms/instance-create.tsx | 17 +++-------- app/forms/ip-pool-create.tsx | 9 ++++++ app/forms/ip-pool-edit.tsx | 3 ++ app/ui/styles/components/menu-list.css | 3 ++ test/e2e/floating-ip-create.e2e.ts | 8 ++--- test/e2e/instance-create.e2e.ts | 20 +++++-------- 10 files changed, 68 insertions(+), 65 deletions(-) create mode 100644 app/components/form/fields/ip-pool-item.tsx 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()