Skip to content

Commit df0dea4

Browse files
Add descriptions to IP Pool dropdowns (#2514)
Co-authored-by: David Crespo <[email protected]>
1 parent 3474c6c commit df0dea4

10 files changed

+68
-65
lines changed

app/components/AttachEphemeralIpModal.tsx

+3-12
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import { useApiMutation, useApiQueryClient, usePrefetchedApiQuery } from '~/api'
1313
import { ListboxField } from '~/components/form/fields/ListboxField'
1414
import { useInstanceSelector } from '~/hooks/use-params'
1515
import { addToast } from '~/stores/toast'
16-
import { Badge } from '~/ui/lib/Badge'
1716
import { Modal } from '~/ui/lib/Modal'
1817
import { ALL_ISH } from '~/util/consts'
1918

19+
import { toIpPoolItem } from './form/fields/ip-pool-item'
20+
2021
export const AttachEphemeralIpModal = ({ onDismiss }: { onDismiss: () => void }) => {
2122
const queryClient = useApiQueryClient()
2223
const { project, instance } = useInstanceSelector()
@@ -54,17 +55,7 @@ export const AttachEphemeralIpModal = ({ onDismiss }: { onDismiss: () => void })
5455
? 'Select a pool'
5556
: 'No pools available'
5657
}
57-
items={
58-
siloPools?.items.map((pool) => ({
59-
label: (
60-
<div className="flex items-center gap-2">
61-
{pool.name}
62-
{pool.isDefault && <Badge>default</Badge>}
63-
</div>
64-
),
65-
value: pool.name,
66-
})) || []
67-
}
58+
items={siloPools.items.map(toIpPoolItem)}
6859
required
6960
/>
7061
</form>

app/components/form/fields/ImageSelectField.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ export function toImageComboboxItem(
8080
value: id,
8181
selectedLabel: name,
8282
label: (
83-
<>
83+
<div className="flex flex-col gap-1">
8484
<div>{name}</div>
8585
<div className="text-tertiary selected:text-accent-secondary">{itemMetadata}</div>
86-
</>
86+
</div>
8787
),
8888
}
8989
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
import type { SiloIpPool } from '~/api'
9+
import { Badge } from '~/ui/lib/Badge'
10+
11+
export function toIpPoolItem(p: SiloIpPool) {
12+
const value = p.name
13+
const selectedLabel = p.name
14+
const label = (
15+
<div className="flex flex-col gap-1">
16+
<div>
17+
{p.name}
18+
{p.isDefault && (
19+
<Badge className="ml-1.5" color="neutral">
20+
default
21+
</Badge>
22+
)}
23+
</div>
24+
{p.description.length && (
25+
<div className="text-tertiary selected:text-accent-secondary">{p.description}</div>
26+
)}
27+
</div>
28+
)
29+
return { value, selectedLabel, label }
30+
}

app/forms/floating-ip-create.tsx

+2-22
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,20 @@ import {
1515
useApiQuery,
1616
useApiQueryClient,
1717
type FloatingIpCreate,
18-
type SiloIpPool,
1918
} from '@oxide/api'
2019

2120
import { AccordionItem } from '~/components/AccordionItem'
2221
import { DescriptionField } from '~/components/form/fields/DescriptionField'
22+
import { toIpPoolItem } from '~/components/form/fields/ip-pool-item'
2323
import { ListboxField } from '~/components/form/fields/ListboxField'
2424
import { NameField } from '~/components/form/fields/NameField'
2525
import { SideModalForm } from '~/components/form/SideModalForm'
2626
import { useProjectSelector } from '~/hooks/use-params'
2727
import { addToast } from '~/stores/toast'
28-
import { Badge } from '~/ui/lib/Badge'
2928
import { Message } from '~/ui/lib/Message'
3029
import { ALL_ISH } from '~/util/consts'
3130
import { pb } from '~/util/path-builder'
3231

33-
const toListboxItem = (p: SiloIpPool) => {
34-
if (!p.isDefault) {
35-
return { value: p.name, label: p.name }
36-
}
37-
// For the default pool, add a label to the dropdown
38-
return {
39-
value: p.name,
40-
selectedLabel: p.name,
41-
label: (
42-
<>
43-
{p.name}{' '}
44-
<Badge className="ml-1" color="neutral">
45-
default
46-
</Badge>
47-
</>
48-
),
49-
}
50-
}
51-
5232
const defaultValues: Omit<FloatingIpCreate, 'ip'> = {
5333
name: '',
5434
description: '',
@@ -108,7 +88,7 @@ export function CreateFloatingIpSideModalForm() {
10888

10989
<ListboxField
11090
name="pool"
111-
items={(allPools?.items || []).map((p) => toListboxItem(p))}
91+
items={(allPools?.items || []).map(toIpPoolItem)}
11292
label="IP pool"
11393
control={form.control}
11494
placeholder="Select a pool"

app/forms/instance-create.tsx

+4-13
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
type InstanceCreate,
2626
type InstanceDiskAttachment,
2727
type NameOrId,
28+
type SiloIpPool,
2829
} from '@oxide/api'
2930
import {
3031
Images16Icon,
@@ -46,6 +47,7 @@ import {
4647
} from '~/components/form/fields/DisksTableField'
4748
import { FileField } from '~/components/form/fields/FileField'
4849
import { BootDiskImageSelectField as ImageSelectField } from '~/components/form/fields/ImageSelectField'
50+
import { toIpPoolItem } from '~/components/form/fields/ip-pool-item'
4951
import { NameField } from '~/components/form/fields/NameField'
5052
import { NetworkInterfaceField } from '~/components/form/fields/NetworkInterfaceField'
5153
import { NumberField } from '~/components/form/fields/NumberField'
@@ -57,7 +59,6 @@ import { FullPageForm } from '~/components/form/FullPageForm'
5759
import { HL } from '~/components/HL'
5860
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
5961
import { addToast } from '~/stores/toast'
60-
import { Badge } from '~/ui/lib/Badge'
6162
import { Button } from '~/ui/lib/Button'
6263
import { Checkbox } from '~/ui/lib/Checkbox'
6364
import { toComboboxItems } from '~/ui/lib/Combobox'
@@ -609,7 +610,7 @@ const AdvancedAccordion = ({
609610
}: {
610611
control: Control<InstanceCreateInput>
611612
isSubmitting: boolean
612-
siloPools: Array<{ name: string; isDefault: boolean }>
613+
siloPools: Array<SiloIpPool>
613614
}) => {
614615
// we track this state manually for the sole reason that we need to be able to
615616
// tell, inside AccordionItem, when an accordion is opened so we can scroll its
@@ -733,17 +734,7 @@ const AdvancedAccordion = ({
733734
label="IP pool for ephemeral IP"
734735
placeholder={defaultPool ? `${defaultPool} (default)` : 'Select a pool'}
735736
selected={`${siloPools.find((pool) => pool.name === selectedPool)?.name}`}
736-
items={
737-
siloPools.map((pool) => ({
738-
label: (
739-
<div className="flex items-center gap-2">
740-
{pool.name}
741-
{pool.isDefault && <Badge>default</Badge>}
742-
</div>
743-
),
744-
value: pool.name,
745-
})) || []
746-
}
737+
items={siloPools.map(toIpPoolItem)}
747738
disabled={!assignEphemeralIp || isSubmitting}
748739
required
749740
onChange={(value) => {

app/forms/ip-pool-create.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { DescriptionField } from '~/components/form/fields/DescriptionField'
1414
import { NameField } from '~/components/form/fields/NameField'
1515
import { SideModalForm } from '~/components/form/SideModalForm'
1616
import { addToast } from '~/stores/toast'
17+
import { Message } from '~/ui/lib/Message'
1718
import { pb } from '~/util/path-builder'
1819

1920
const defaultValues: IpPoolCreate = {
@@ -51,6 +52,14 @@ export function CreateIpPoolSideModalForm() {
5152
>
5253
<NameField name="name" control={form.control} />
5354
<DescriptionField name="description" control={form.control} />
55+
<IpPoolVisibilityMessage />
5456
</SideModalForm>
5557
)
5658
}
59+
60+
export const IpPoolVisibilityMessage = () => (
61+
<Message
62+
variant="info"
63+
content="Users in linked silos will use IP pool names and descriptions to help them choose a pool when allocating IPs."
64+
/>
65+
)

app/forms/ip-pool-edit.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { getIpPoolSelector, useIpPoolSelector } from '~/hooks/use-params'
2222
import { addToast } from '~/stores/toast'
2323
import { pb } from '~/util/path-builder'
2424

25+
import { IpPoolVisibilityMessage } from './ip-pool-create'
26+
2527
EditIpPoolSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
2628
const { pool } = getIpPoolSelector(params)
2729
await apiQueryClient.prefetchQuery('ipPoolView', { path: { pool } })
@@ -68,6 +70,7 @@ export function EditIpPoolSideModalForm() {
6870
>
6971
<NameField name="name" control={form.control} />
7072
<DescriptionField name="description" control={form.control} />
73+
<IpPoolVisibilityMessage />
7174
</SideModalForm>
7275
)
7376
}

app/ui/styles/components/menu-list.css

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828

2929
.ox-menu-item.is-selected {
3030
@apply border-0 text-accent bg-accent-secondary hover:bg-accent-secondary-hover;
31+
.ox-badge {
32+
@apply ring-0 text-inverse bg-accent;
33+
}
3134
}
3235

3336
/* beautiful ring */

test/e2e/floating-ip-create.e2e.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,19 @@ test('can create a floating IP', async ({ page }) => {
2828
.getByRole('textbox', { name: 'Description' })
2929
.fill('A description for this Floating IP')
3030

31-
const poolListbox = page.getByRole('button', { name: 'IP pool' })
31+
const label = page.getByLabel('IP pool')
3232

3333
// accordion content should be hidden
34-
await expect(poolListbox).toBeHidden()
34+
await expect(label).toBeHidden()
3535

3636
// open accordion
3737
await page.getByRole('button', { name: 'Advanced' }).click()
3838

3939
// accordion content should be visible
40-
await expect(poolListbox).toBeVisible()
40+
await expect(label).toBeVisible()
4141

4242
// choose pool and submit
43-
await poolListbox.click()
43+
await label.click()
4444
await page.getByRole('option', { name: 'ip-pool-1' }).click()
4545
await page.getByRole('button', { name: 'Create floating IP' }).click()
4646

test/e2e/instance-create.e2e.ts

+8-12
Original file line numberDiff line numberDiff line change
@@ -70,27 +70,23 @@ test('can create an instance', async ({ page }) => {
7070
await page.getByRole('button', { name: 'Networking' }).click()
7171
await page.getByRole('button', { name: 'Configuration' }).click()
7272

73-
const assignEphemeralIpCheckbox = page.getByRole('checkbox', {
73+
const checkbox = page.getByRole('checkbox', {
7474
name: 'Allocate and attach an ephemeral IP address',
7575
})
76-
const assignEphemeralIpButton = page.getByRole('button', {
77-
name: 'IP pool for ephemeral IP',
78-
})
76+
const label = page.getByLabel('IP pool for ephemeral IP')
7977

8078
// verify that the ip pool selector is visible and default is selected
81-
await expect(assignEphemeralIpCheckbox).toBeChecked()
82-
await assignEphemeralIpButton.click()
79+
await expect(checkbox).toBeChecked()
80+
await label.click()
8381
await expect(page.getByRole('option', { name: 'ip-pool-1' })).toBeEnabled()
84-
await assignEphemeralIpButton.click() // click closes the listbox so we can do more stuff
8582

8683
// unchecking the box should disable the selector
87-
await assignEphemeralIpCheckbox.uncheck()
88-
await expect(assignEphemeralIpButton).toBeHidden()
84+
await checkbox.uncheck()
85+
await expect(label).toBeHidden()
8986

9087
// re-checking the box should re-enable the selector, and other options should be selectable
91-
await assignEphemeralIpCheckbox.check()
92-
await assignEphemeralIpButton.click()
93-
await page.getByRole('option', { name: 'ip-pool-2' }).click()
88+
await checkbox.check()
89+
await selectOption(page, 'IP pool for ephemeral IP', 'ip-pool-2 VPN IPs')
9490

9591
// should be visible in accordion
9692
await expect(page.getByRole('radiogroup', { name: 'Network interface' })).toBeVisible()

0 commit comments

Comments
 (0)