Skip to content
Open
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
6 changes: 6 additions & 0 deletions app/components/form/fields/ComboboxField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function ComboboxField<
label = capitalize(name),
required,
onChange,
onInputChange,
allowArbitraryValues,
placeholder,
// Intent is to not show both a placeholder and a description, while still having good defaults; prefer a description to a placeholder
Expand Down Expand Up @@ -88,6 +89,11 @@ export function ComboboxField<
onChange?.(value)
setSelectedItemLabel(getSelectedLabelFromValue(items, value))
}}
onInputChange={(value) => {
// if arbitrary values are allowed, set the field's value; otherwise, clear the selected value
field.onChange(allowArbitraryValues ? value : '')
onInputChange?.(value)
}}
allowArbitraryValues={allowArbitraryValues}
inputRef={field.ref}
transform={transform}
Expand Down
5 changes: 0 additions & 5 deletions app/forms/firewall-rules-common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,6 @@ const TargetAndHostFilterSubform = ({
const onTypeChange = () => {
subform.reset({ type: subform.getValues('type'), value: '' })
}
const onInputChange = (value: string) => {
subform.setValue('value', value)
}

const noun = sectionType === 'target' ? 'target' : 'host filter'
const nounTitle = capitalize(noun) + 's'
Expand All @@ -187,7 +184,6 @@ const TargetAndHostFilterSubform = ({
description="Select an option or enter a custom value"
control={subformControl}
onEnter={submitSubform}
onInputChange={onInputChange}
items={items}
allowArbitraryValues
hideOptionalTag
Expand Down Expand Up @@ -472,7 +468,6 @@ const ProtocolFilters = ({ control }: { control: Control<FirewallRuleValues> })
description="Leave blank to match any type"
placeholder=""
allowArbitraryValues
onInputChange={(value) => protocolForm.setValue('icmpType', value)}
items={icmpTypeItems}
validate={(value) => {
const result = parseIcmpType(value)
Expand Down
2 changes: 1 addition & 1 deletion app/ui/lib/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export const Combobox = ({
// of those rules one by one. Better to rely on the shared classes.
<div
className={cn('ox-menu-item', {
'is-selected': selected && query !== item.value,
'is-selected': selected,
'is-highlighted': focus,
})}
>
Expand Down
63 changes: 61 additions & 2 deletions test/e2e/instance-create.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,65 @@ test('Validate CPU and RAM', async ({ page }) => {
await expect(memMsg).toBeVisible()
})

test('clears silo image selection when typing arbitrary text and blurring', async ({
page,
}) => {
const instanceName = 'test-instance'

await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)

const imageSelectCombobox = page.getByRole('combobox', { name: 'Image' })
await imageSelectCombobox.scrollIntoViewIfNeeded()

// Ensure the combobox is visible and has the expected options
await expect(imageSelectCombobox).toHaveValue('')
await imageSelectCombobox.click()
await expect(page.getByRole('option', { name: 'ubuntu-22-04' })).toBeVisible()
await expect(page.getByRole('option', { name: 'ubuntu-20-04' })).toBeVisible()
await expect(page.getByRole('option', { name: 'arch-2022-06-01' })).toBeVisible()

// Filter the combobox for a particular silo image pattern
await imageSelectCombobox.fill('ubuntu')

// Ensure that only show the options that match the filter are visible
await expect(page.getByRole('option', { name: 'ubuntu-22-04' })).toBeVisible()
await expect(page.getByRole('option', { name: 'ubuntu-20-04' })).toBeVisible()
await expect(page.getByRole('option', { name: 'arch-2022-06-01' })).toBeHidden()

// Select an image
await page.getByRole('option', { name: 'ubuntu-22-04' }).click()
await expect(imageSelectCombobox).toHaveValue('ubuntu-22-04')

// Delete four characters from the end to reveal more options
await page.keyboard.press('Backspace')
await page.keyboard.press('Backspace')
await page.keyboard.press('Backspace')
await page.keyboard.press('Backspace')

// There should now be an invalid value in the combobox, but we should be able to see both the ubuntu options: `ubuntu-22-04` and `ubuntu-20-04`
// and we should NOT be able to see the `arch-2022-06-01` option
await expect(imageSelectCombobox).toHaveValue('ubuntu-2')
await expect(page.getByRole('option', { name: 'ubuntu-22-04' })).toBeVisible()
await expect(page.getByRole('option', { name: 'ubuntu-20-04' })).toBeVisible()
await expect(page.getByRole('option', { name: 'arch-2022-06-01' })).toBeHidden()

// Blur the field by clicking elsewhere; because the value is not a valid silo image, the selection should be cleared
await page.getByRole('textbox', { name: 'Name', exact: true }).click()

// The selection should be cleared since allowArbitraryValues=false
await expect(imageSelectCombobox).toHaveValue('')

// Re-focus and select the original option again
await imageSelectCombobox.click()
await page.getByRole('option', { name: 'ubuntu-22-04' }).click()
await expect(imageSelectCombobox).toHaveValue('ubuntu-22-04')

// Should be able to continue with instance creation
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
})

test('create instance with IPv6-only networking', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')

Expand Down Expand Up @@ -1237,7 +1296,7 @@ test('floating IPs are filtered by NIC IP version', async ({ page }) => {
// Verify only IPv4 floating IP is available (rootbeer-float with IP 123.4.56.4)
await expect(page.getByRole('option', { name: 'rootbeer-float' })).toBeVisible()
// IPv6 floating IP should not be in the list
await expect(page.getByRole('option', { name: 'ipv6-float' })).not.toBeVisible()
await expect(page.getByRole('option', { name: 'ipv6-float' })).toBeHidden()

// Close the listbox dropdown first by pressing Escape
await page.keyboard.press('Escape')
Expand All @@ -1262,7 +1321,7 @@ test('floating IPs are filtered by NIC IP version', async ({ page }) => {
// Verify only IPv6 floating IP is available (ipv6-float)
await expect(page.getByRole('option', { name: 'ipv6-float' })).toBeVisible()
// IPv4 floating IP should not be in the list
await expect(page.getByRole('option', { name: 'rootbeer-float' })).not.toBeVisible()
await expect(page.getByRole('option', { name: 'rootbeer-float' })).toBeHidden()

// Close the listbox dropdown first by pressing Escape
await page.keyboard.press('Escape')
Expand Down
Loading