Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b944050
More updates
charliepark Aug 6, 2026
4755f73
Update side modals, polling
charliepark Aug 7, 2026
6958a48
Make webhook create a dedicated page
benjaminleonard Aug 10, 2026
da0cb41
Merge branch 'main' into webhooks
charliepark Aug 10, 2026
6e8c8f4
Initial input pass
benjaminleonard Aug 10, 2026
59e0d9b
Remove resend checkbox; other tweaks to lists
charliepark Aug 12, 2026
a262089
Refinement and more accurate mock data
benjaminleonard Aug 13, 2026
d68c2e5
adjustments to filtering with pagination
charliepark Aug 13, 2026
8172938
Merge branch 'webhooks' into webhooks-input
benjaminleonard Aug 14, 2026
01bbbf0
Test fix
benjaminleonard Aug 14, 2026
7bf1bed
nav changes, tabs, column updates
charliepark Aug 18, 2026
dc9a334
merge main
charliepark Aug 18, 2026
a178ae2
Update app/forms/webhook-create.tsx
charliepark Aug 18, 2026
0857569
merge main and resolve conflicts
charliepark Aug 27, 2026
c95b08c
merge webhooks-input and resolve conflicts
charliepark Aug 27, 2026
fa18981
new receiver form should be on its own page
charliepark Aug 27, 2026
416a074
filter probe from list of subscription classes; update docs links
charliepark Aug 27, 2026
dd204ca
getting clever with spaces and chip creation
charliepark Aug 27, 2026
28fad68
Update app/pages/system/alerting/AlertReceiversTab.tsx
charliepark Aug 27, 2026
01ca961
Update app/pages/system/alerting/AlertReceiverPage.tsx
charliepark Aug 27, 2026
c97b759
Update app/pages/system/alerting/AlertReceiverPage.tsx
charliepark Aug 27, 2026
a4418d6
Update app/pages/system/alerting/AlertsTab.tsx
charliepark Aug 27, 2026
5df3af2
Update app/pages/system/alerting/AlertReceiversTab.tsx
charliepark Aug 27, 2026
2721527
Update app/pages/system/alerting/AlertReceiverPage.tsx
charliepark Aug 27, 2026
1d11577
Update app/pages/system/alerting/AlertReceiverPage.tsx
charliepark Aug 27, 2026
e6fcabf
Update app/pages/system/alerting/AlertReceiverPage.tsx
charliepark Aug 27, 2026
8ca2463
Update app/pages/system/alerting/AlertReceiverPage.tsx
charliepark Aug 27, 2026
3b7f2d2
refactoring
charliepark Aug 27, 2026
43e0c40
update wording in more places; integrate alert_view
charliepark Aug 28, 2026
330d17c
a few small copy changes
charliepark Aug 28, 2026
53f4cce
Update combobox querying; update icon
charliepark Aug 28, 2026
119156f
Add link to Secrets to webhook creation form and secret creation modal
charliepark Aug 28, 2026
8d070f9
use placeholder icon until Ben adds a 24px bell
charliepark Aug 28, 2026
3afe559
Add alerts list
fakemonster Aug 17, 2026
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
1 change: 1 addition & 0 deletions app/api/__tests__/safety.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ it('mock-api is only referenced in test files', () => {
"AGENTS.md",
"app/api/__tests__/client.browser.spec.ts",
"mock-api/msw/db.ts",
"test/e2e/alerts.e2e.ts",
"test/e2e/fleet-access.e2e.ts",
"test/e2e/instance-create.e2e.ts",
"test/e2e/inventory.e2e.ts",
Expand Down
1 change: 1 addition & 0 deletions app/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './client'
export * from './roles'
export * from './util'
export * from './__generated__/Api'
export { snakeify } from './__generated__/util'
// export * as ZVal from './__generated__/validate'

export type { ApiTypes }
Expand Down
1 change: 1 addition & 0 deletions app/api/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type SshKey = Readonly<{ sshKey: string }>
export type Sled = Readonly<{ sledId?: string }>
export type IpPool = Readonly<{ pool?: string }>
export type SubnetPool = Readonly<{ subnetPool?: string }>
export type AlertReceiver = Readonly<{ receiver?: string }>
export type ExternalSubnet = Readonly<Merge<Project, { externalSubnet?: string }>>
export type FloatingIp = Readonly<Merge<Project, { floatingIp?: string }>>

Expand Down
47 changes: 46 additions & 1 deletion app/api/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,52 @@
*/
import { describe, expect, it, test } from 'vitest'

import { diskCan, genName, instanceCan, parsePortRange, synthesizeData } from './util'
import {
diskCan,
genName,
instanceCan,
parsePortRange,
subscriptionRegex,
synthesizeData,
} from './util'

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)
})

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)
})

it('* can appear in any position', () => {
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)
})

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)

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)
})

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)
})
})

describe('parsePortRange', () => {
describe('parses', () => {
Expand Down
34 changes: 34 additions & 0 deletions app/api/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,40 @@ export const INSTANCE_MAX_CPU = 254
export const INSTANCE_MIN_RAM_GiB = 1
export const INSTANCE_MAX_RAM_GiB = 1536

// Valid alert subscription: an alert class or a glob pattern matching multiple
// classes. https://github.com/oxidecomputer/omicron/blob/32615a35/nexus/types/versions/src/initial/alert.rs#L22-L23
export const ALERT_SUBSCRIPTION_REGEX =
/^([a-zA-Z0-9_]+|\*|\*\*)(\.([a-zA-Z0-9_]+|\*|\*\*))*$/

/** A subscription with a `*` or `**` segment, as opposed to an exact class */
export const isGlobPattern = (subscription: string) => subscription.includes('*')

/**
* The `probe` class is synthetic: it exists for webhook receiver liveness
* probes only.
* The API lists it in `alertClassList` but rejects exact subscriptions to it
* with a 400, so keep it out of anything the user can pick. Globs are exempt
* because the API returns from its glob branch before reaching this check.
* https://github.com/oxidecomputer/omicron/blob/6db4c7e/nexus/db-model/src/alert_subscription.rs#L91-L98
*/
export const PROBE_ALERT_CLASS = 'probe'

/** Alert classes a receiver can actually subscribe to */
export const isSubscribableClass = (c: { name: string }) => c.name !== PROBE_ALERT_CLASS

/**
* Convert an alert subscription to a regex matching the class names it covers:
* a `*` segment matches exactly one segment, `**` matches one or more.
* https://github.com/oxidecomputer/omicron/blob/32615a35/nexus/db-model/src/alert_subscription.rs
*/
export function subscriptionRegex(subscription: string) {
const pattern = subscription
.split('.')
.map((seg) => (seg === '**' ? '.+' : seg === '*' ? '[^.]+' : seg))
.join('\\.')
return new RegExp(`^${pattern}$`)
}

export const MIN_DISK_SIZE_GiB = 1
/**
* Disk size limited to 1023 as that's the maximum we can safely allocate right now
Expand Down
62 changes: 62 additions & 0 deletions app/components/SubscriptionMatchPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 { useQuery } from '@tanstack/react-query'

import { api, q } from '@oxide/api'
import { Badge } from '@oxide/design-system/ui'

import {
ALERT_SUBSCRIPTION_REGEX,
isGlobPattern,
isSubscribableClass,
subscriptionRegex,
} from '~/api/util'
import { ALL_ISH } from '~/util/consts'

/**
* For a glob subscription pattern, show which alert classes it currently
* matches. Renders nothing for exact (non-glob) patterns.
* Note the match set is point-in-time: globs are re-evaluated by the control
* plane as alert classes are added.
*/
export function SubscriptionMatchPreview({ pattern }: { pattern: string }) {
// Same query as the class picker this sits under, so it's a cache hit rather
// than a fetch. Matching locally with `subscriptionRegex`, mirroring the
// control plane's glob compiler.
const { data } = useQuery(q(api.alertClassList, { query: { limit: ALL_ISH } }))

// validate before subscriptionRegex, which assumes a well-formed subscription
const isValidGlob = isGlobPattern(pattern) && ALERT_SUBSCRIPTION_REGEX.test(pattern)
if (!isValidGlob || !data) return null

const re = subscriptionRegex(pattern)
// the probe class can't be subscribed to, so don't count it as a match
const classes = data.items.filter(isSubscribableClass).filter((c) => re.test(c.name))

if (classes.length === 0) {
return (
<p className="text-sans-sm text-secondary">
No current alert classes match this pattern. It may match classes added in the
future.
</p>
)
}

return (
<p className="text-sans-sm text-secondary">
Matches {classes.length} alert {classes.length === 1 ? 'class' : 'classes'}:{' '}
<span className="inline-flex flex-wrap gap-1 align-bottom">
{classes.map((c) => (
<Badge key={c.name} color="neutral">
{c.name}
</Badge>
))}
</span>
</p>
)
}
Loading
Loading