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
60 changes: 36 additions & 24 deletions app/api/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,56 +19,68 @@ import {

describe('subscriptionRegex', () => {
it('matches exact class names', () => {
expect(subscriptionRegex('instance.create').test('instance.create')).toBe(true)
expect(subscriptionRegex('instance.create').test('instance.created')).toBe(false)
expect(subscriptionRegex('instance.create')?.test('instance.create')).toBe(true)
expect(subscriptionRegex('instance.create')?.test('instance.created')).toBe(false)
})

it('* matches exactly one segment', () => {
const re = subscriptionRegex('disk.*')
expect(re.test('disk.create')).toBe(true)
expect(re.test('disk.snapshot.create')).toBe(false)
expect(re.test('disk')).toBe(false)
expect(re?.test('disk.create')).toBe(true)
expect(re?.test('disk.snapshot.create')).toBe(false)
expect(re?.test('disk')).toBe(false)
})

it('* can appear at the head', () => {
const re = subscriptionRegex('*.create')
expect(re.test('disk.create')).toBe(true)
expect(re.test('instance.create')).toBe(true)
expect(re.test('instance.ephemeral_ip.create')).toBe(false)
expect(re?.test('disk.create')).toBe(true)
expect(re?.test('instance.create')).toBe(true)
expect(re?.test('instance.ephemeral_ip.create')).toBe(false)
})

it('* can appear mid-string', () => {
const re = subscriptionRegex('hardware.*.insert')
expect(re.test('hardware.sled.insert')).toBe(true)
expect(re.test('hardware.power_shelf.psu.insert')).toBe(false)
expect(re.test('hardware.insert')).toBe(false)
expect(re?.test('hardware.sled.insert')).toBe(true)
expect(re?.test('hardware.power_shelf.psu.insert')).toBe(false)
expect(re?.test('hardware.insert')).toBe(false)
})

it('** matches one or more segments', () => {
const re = subscriptionRegex('hardware.**')
expect(re.test('hardware.power_shelf.psu.insert')).toBe(true)
expect(re.test('hardware.psu')).toBe(true)
expect(re.test('hardware')).toBe(false)
expect(re?.test('hardware.power_shelf.psu.insert')).toBe(true)
expect(re?.test('hardware.psu')).toBe(true)
expect(re?.test('hardware')).toBe(false)

const suffix = subscriptionRegex('**.delete')
expect(suffix.test('project.delete')).toBe(true)
expect(suffix.test('instance.ephemeral_ip.delete')).toBe(true)
expect(suffix.test('delete')).toBe(false)
expect(suffix?.test('project.delete')).toBe(true)
expect(suffix?.test('instance.ephemeral_ip.delete')).toBe(true)
expect(suffix?.test('delete')).toBe(false)

const infix = subscriptionRegex('hardware.**.insert')
expect(infix.test('hardware.sled.insert')).toBe(true)
expect(infix.test('hardware.power_shelf.psu.insert')).toBe(true)
expect(infix.test('hardware.insert')).toBe(false)
expect(infix?.test('hardware.sled.insert')).toBe(true)
expect(infix?.test('hardware.power_shelf.psu.insert')).toBe(true)
expect(infix?.test('hardware.insert')).toBe(false)
})

it('does not match substrings within a segment', () => {
expect(subscriptionRegex('instance.**').test('silo.instance_quota.hit')).toBe(false)
expect(subscriptionRegex('disk.*').test('bigdisk.create')).toBe(false)
expect(subscriptionRegex('instance.**')?.test('silo.instance_quota.hit')).toBe(false)
expect(subscriptionRegex('disk.*')?.test('bigdisk.create')).toBe(false)
})

it('does not match leading/trailing segments without a star pattern', () => {
expect(subscriptionRegex('instance.**').test('silo.instance.hit')).toBe(false)
expect(subscriptionRegex('**.power_shelf').test('hardware.power_shelf.psu')).toBe(false)
expect(subscriptionRegex('instance.**')?.test('silo.instance.hit')).toBe(false)
expect(subscriptionRegex('**.power_shelf')?.test('hardware.power_shelf.psu')).toBe(
false
)
})

it('never throws on invalid input, including regex metacharacters', () => {
expect(subscriptionRegex('')).toBeNull()
expect(subscriptionRegex('a|b*')).toBeNull()
expect(subscriptionRegex('disk+')).toBeNull()
expect(subscriptionRegex('(*')).toBeNull()
expect(subscriptionRegex('*.{2,}')).toBeNull()
expect(subscriptionRegex('hardware.***')).toBeNull()
expect(subscriptionRegex('hardware..disk')).toBeNull()
})
})

Expand Down
6 changes: 4 additions & 2 deletions app/api/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ export const isSubscribableClass = (c: { name: string }) => c.name !== PROBE_ALE

/**
* Convert an alert subscription to a regex matching the class names it covers:
* a `*` segment matches exactly one segment, `**` matches one or more.
* a `*` segment matches exactly one segment, `**` matches one or more. An
* invalid input returns `null`.
* https://github.com/oxidecomputer/omicron/blob/32615a35/nexus/db-model/src/alert_subscription.rs
*/
export function subscriptionRegex(subscription: string) {
export function subscriptionRegex(subscription: string): RegExp | null {
if (!ALERT_SUBSCRIPTION_REGEX.test(subscription)) return null
const pattern = subscription
.split('.')
.map((seg) => (seg === '**' ? '.+' : seg === '*' ? '[^.]+' : seg))
Expand Down
142 changes: 142 additions & 0 deletions app/components/FilterPopover.browser.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* 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 { useState } from 'react'
import { expect, test } from 'vitest'
import { render } from 'vitest-browser-react'
import { page, userEvent } from 'vitest/browser'

import { TextField } from '~/components/form/fields/TextField'

import { FilterPopover } from './FilterPopover'

type Filters = { name: string; owner: string; types: string[] }

function Harness() {
const defaultValues = { name: '', owner: '', types: [] }
const [applied, setApplied] = useState<Filters>(defaultValues)
return (
<>
<FilterPopover
isFetching={false}
resetFieldValues={defaultValues}
lastApplied={applied}
handleSubmit={setApplied}
pluralFilteredItemName="widgets"
>
{(form) => (
<>
<TextField
name="name"
label="Name"
control={form.control}
validate={(value) => (value === 'bad' ? 'No bad names' : undefined)}
/>
<TextField name="owner" label="Owner" control={form.control} />
<button
type="button"
onClick={() => setApplied((prev) => ({ ...prev, types: ['disk'] }))}
>
Apply type filter
</button>
</>
)}
</FilterPopover>
<output aria-label="Applied name">{applied.name}</output>
</>
)
}

const filterButton = () => page.getByRole('button', { name: /^Filter widgets/ })
const form = () => page.getByRole('form', { name: 'Filter widgets' })
const resetButton = () => page.getByRole('button', { name: 'Reset' })
const nameField = () => page.getByRole('textbox', { name: 'Name' })
const applyButton = () => page.getByRole('button', { name: 'Apply' })
const appliedName = () => page.getByRole('status', { name: 'Applied name' })

test('badge counts applied filters', async () => {
await render(<Harness />)
const noneApplied = page.getByRole('button', { name: 'Filter widgets', exact: true })
await expect.element(noneApplied).toBeVisible()

await noneApplied.click()
await page.getByRole('button', { name: 'Apply type filter' }).click()
await userEvent.keyboard('{Escape}')
const oneApplied = page.getByRole('button', { name: 'Filter widgets (1 applied)' })
await expect.element(oneApplied).toBeVisible()
// this is the non-accessible text
await expect.element(oneApplied.getByText('1')).toBeVisible()

await oneApplied.click()
await nameField().fill('abc')
await applyButton().click()
await expect
.element(page.getByRole('button', { name: 'Filter widgets (2 applied)' }))
.toBeVisible()

await filterButton().click()
await page.getByRole('textbox', { name: 'Owner' }).fill('abc')
await applyButton().click()
await expect
.element(page.getByRole('button', { name: 'Filter widgets (3 applied)' }))
.toBeVisible()
})

test('apply submits the values and closes only if there are no validation errors', async () => {
await render(<Harness />)
await filterButton().click()
await nameField().fill('bad')
await applyButton().click()
await expect.element(form().getByText('No bad names')).toBeVisible()
await expect.element(appliedName()).toHaveTextContent('')

await nameField().fill('good')
await applyButton().click()
await expect.element(form()).not.toBeInTheDocument()
await expect.element(appliedName()).toHaveTextContent('good')
})

test('reset applies the reset values', async () => {
await render(<Harness />)
await filterButton().click()
await nameField().fill('abc')
await applyButton().click()
await expect.element(appliedName()).toHaveTextContent('abc')
await filterButton().click()
await resetButton().click()
await expect.element(form()).not.toBeInTheDocument()
await expect.element(appliedName()).toHaveTextContent('')
await expect
.element(page.getByRole('button', { name: 'Filter widgets', exact: true }))
.toBeVisible()
})

test('reset is disabled when nothing is applied', async () => {
await render(<Harness />)
await filterButton().click()
await expect.element(resetButton()).toBeDisabled()

// it's also disabled if you simply restore the default form values manually
await nameField().fill('abc')
await applyButton().click()
await filterButton().click()
await nameField().fill('')
await applyButton().click()
await filterButton().click()
await expect.element(resetButton()).toBeDisabled()
})

test('unapplied edits are discarded on close', async () => {
await render(<Harness />)
await filterButton().click()
await nameField().fill('draft')
await userEvent.keyboard('{Escape}')
await expect.element(form()).not.toBeInTheDocument()

await filterButton().click()
await expect.element(nameField()).toHaveValue('')
})
Loading
Loading