Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading