Skip to content

Commit

Permalink
Add descriptions to IP Pool dropdowns (#2514)
Browse files Browse the repository at this point in the history
Co-authored-by: David Crespo <[email protected]>
  • Loading branch information
charliepark and david-crespo authored Oct 29, 2024
1 parent 3474c6c commit df0dea4
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 65 deletions.
15 changes: 3 additions & 12 deletions app/components/AttachEphemeralIpModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -54,17 +55,7 @@ export const AttachEphemeralIpModal = ({ onDismiss }: { onDismiss: () => void })
? 'Select a pool'
: 'No pools available'
}
items={
siloPools?.items.map((pool) => ({
label: (
<div className="flex items-center gap-2">
{pool.name}
{pool.isDefault && <Badge>default</Badge>}
</div>
),
value: pool.name,
})) || []
}
items={siloPools.items.map(toIpPoolItem)}
required
/>
</form>
Expand Down
4 changes: 2 additions & 2 deletions app/components/form/fields/ImageSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ export function toImageComboboxItem(
value: id,
selectedLabel: name,
label: (
<>
<div className="flex flex-col gap-1">
<div>{name}</div>
<div className="text-tertiary selected:text-accent-secondary">{itemMetadata}</div>
</>
</div>
),
}
}
30 changes: 30 additions & 0 deletions app/components/form/fields/ip-pool-item.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<div className="flex flex-col gap-1">
<div>
{p.name}
{p.isDefault && (
<Badge className="ml-1.5" color="neutral">
default
</Badge>
)}
</div>
{p.description.length && (
<div className="text-tertiary selected:text-accent-secondary">{p.description}</div>
)}
</div>
)
return { value, selectedLabel, label }
}
24 changes: 2 additions & 22 deletions app/forms/floating-ip-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}{' '}
<Badge className="ml-1" color="neutral">
default
</Badge>
</>
),
}
}

const defaultValues: Omit<FloatingIpCreate, 'ip'> = {
name: '',
description: '',
Expand Down Expand Up @@ -108,7 +88,7 @@ export function CreateFloatingIpSideModalForm() {

<ListboxField
name="pool"
items={(allPools?.items || []).map((p) => toListboxItem(p))}
items={(allPools?.items || []).map(toIpPoolItem)}
label="IP pool"
control={form.control}
placeholder="Select a pool"
Expand Down
17 changes: 4 additions & 13 deletions app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
type InstanceCreate,
type InstanceDiskAttachment,
type NameOrId,
type SiloIpPool,
} from '@oxide/api'
import {
Images16Icon,
Expand All @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -609,7 +610,7 @@ const AdvancedAccordion = ({
}: {
control: Control<InstanceCreateInput>
isSubmitting: boolean
siloPools: Array<{ name: string; isDefault: boolean }>
siloPools: Array<SiloIpPool>
}) => {
// 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
Expand Down Expand Up @@ -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: (
<div className="flex items-center gap-2">
{pool.name}
{pool.isDefault && <Badge>default</Badge>}
</div>
),
value: pool.name,
})) || []
}
items={siloPools.map(toIpPoolItem)}
disabled={!assignEphemeralIp || isSubmitting}
required
onChange={(value) => {
Expand Down
9 changes: 9 additions & 0 deletions app/forms/ip-pool-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -51,6 +52,14 @@ export function CreateIpPoolSideModalForm() {
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
<IpPoolVisibilityMessage />
</SideModalForm>
)
}

export const IpPoolVisibilityMessage = () => (
<Message
variant="info"
content="Users in linked silos will use IP pool names and descriptions to help them choose a pool when allocating IPs."
/>
)
3 changes: 3 additions & 0 deletions app/forms/ip-pool-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } })
Expand Down Expand Up @@ -68,6 +70,7 @@ export function EditIpPoolSideModalForm() {
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
<IpPoolVisibilityMessage />
</SideModalForm>
)
}
3 changes: 3 additions & 0 deletions app/ui/styles/components/menu-list.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
8 changes: 4 additions & 4 deletions test/e2e/floating-ip-create.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
20 changes: 8 additions & 12 deletions test/e2e/instance-create.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit df0dea4

Please sign in to comment.