Merge ticket/017-regex-cheat-sheet-icon

This commit is contained in:
Hugh Kaznowski
2026-09-05 19:41:48 +01:00
5 changed files with 84 additions and 3 deletions
+5 -1
View File
@@ -17,6 +17,7 @@ import {
useSectionCollapsed, useSectionCollapsed,
} from './CollapsibleSection' } from './CollapsibleSection'
import { HelpIcon } from './HelpIcon' import { HelpIcon } from './HelpIcon'
import { RegexHelpIcon } from './RegexHelpIcon'
import { queryInputClassName } from './FilterBar' import { queryInputClassName } from './FilterBar'
type ConditionKind = AlertCondition['kind'] type ConditionKind = AlertCondition['kind']
@@ -341,7 +342,10 @@ export function Alerts() {
{form.kind === 'newvalue' && ( {form.kind === 'newvalue' && (
<div className="space-y-1"> <div className="space-y-1">
<div> <div>
<label className="mb-1 block text-xs text-text-subtle">Field regex</label> <label className="mb-1 flex items-center gap-1 text-xs text-text-subtle">
Field regex
<RegexHelpIcon label="Help: Field regex syntax" />
</label>
<input <input
className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-sm text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none" className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-sm text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
value={form.fieldRegex} value={form.fieldRegex}
+2
View File
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { useState } from 'react' import { useState } from 'react'
import { HelpIcon } from './HelpIcon' import { HelpIcon } from './HelpIcon'
import { RegexHelpIcon } from './RegexHelpIcon'
// Reused by `Alerts.tsx`'s condition editor so alert conditions look like the filter bar without duplicating its draft/submit state. // Reused by `Alerts.tsx`'s condition editor so alert conditions look like the filter bar without duplicating its draft/submit state.
export const queryInputClassName = export const queryInputClassName =
@@ -30,6 +31,7 @@ export function FilterBar({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpIcon sectionId="filtering" label="Help: Filtering" /> <HelpIcon sectionId="filtering" label="Help: Filtering" />
<RegexHelpIcon label="Help: regex(...) syntax" />
{collapseToggle} {collapseToggle}
<input <input
className={queryInputClassName} className={queryInputClassName}
+5 -1
View File
@@ -19,6 +19,7 @@ import {
useSectionCollapsed, useSectionCollapsed,
} from './CollapsibleSection' } from './CollapsibleSection'
import { HelpIcon } from './HelpIcon' import { HelpIcon } from './HelpIcon'
import { RegexHelpIcon } from './RegexHelpIcon'
const EMPTY_MATCHER: MetricParamMatcher = { param_name: '', capture_group: '', kind: 'string' } const EMPTY_MATCHER: MetricParamMatcher = { param_name: '', capture_group: '', kind: 'string' }
@@ -188,7 +189,10 @@ export function Metrics({ range }: { range: TimeRange | null }) {
/> />
</div> </div>
<div> <div>
<label className="mb-1 block text-xs text-text-subtle">Message match regex</label> <label className="mb-1 flex items-center gap-1 text-xs text-text-subtle">
Message match regex
<RegexHelpIcon label="Help: Message match regex syntax" />
</label>
<input <input
className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-sm text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none" className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-sm text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
value={regex} value={regex}
+5 -1
View File
@@ -9,6 +9,7 @@ import {
useSectionCollapsed, useSectionCollapsed,
} from './CollapsibleSection' } from './CollapsibleSection'
import { HelpIcon } from './HelpIcon' import { HelpIcon } from './HelpIcon'
import { RegexHelpIcon } from './RegexHelpIcon'
const EMPTY_KIND: ParamDeclaration['kind'] = 'string' const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
@@ -104,7 +105,10 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
/> />
</div> </div>
<div> <div>
<label className="mb-1 block text-xs text-text-subtle">Message match regex</label> <label className="mb-1 flex items-center gap-1 text-xs text-text-subtle">
Message match regex
<RegexHelpIcon label="Help: Message match regex syntax" />
</label>
<input <input
className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-sm text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none" className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-sm text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
value={regex} value={regex}
+67
View File
@@ -0,0 +1,67 @@
import { useEffect, useRef, useState } from 'react'
// Regex syntax cheat sheet, scoped to what the Rust `regex` crate actually supports (used server-side
// for message-match/field-match regexes and the filter bar's `regex(...)`) — no PCRE-only lookaround
// or backreferences, since those would silently fail to compile there.
const CHEAT_SHEET: { tokens: string; meaning: string }[] = [
{ tokens: '.', meaning: 'Any character except newline' },
{ tokens: '\\d \\w \\s', meaning: 'Digit, word char, whitespace' },
{ tokens: '\\D \\W \\S', meaning: 'Negated versions of the above' },
{ tokens: '* + ?', meaning: '0+, 1+, 0-1 of the previous item' },
{ tokens: '{n} {n,} {n,m}', meaning: 'Exactly n, n-or-more, n-to-m repeats' },
{ tokens: '^ $', meaning: 'Start / end of text (or line, with (?m))' },
{ tokens: '\\b \\B', meaning: 'Word boundary / non-boundary' },
{ tokens: '[abc] [^abc]', meaning: 'Character class / negated class' },
{ tokens: '[a-z0-9]', meaning: 'Character range' },
{ tokens: 'a|b', meaning: 'Alternation' },
{ tokens: '(...)', meaning: 'Capturing group' },
{ tokens: '(?P<name>...)', meaning: 'Named capturing group' },
{ tokens: '(?:...)', meaning: 'Non-capturing group' },
{ tokens: '(?i) (?m) (?s)', meaning: 'Inline flags: case-insensitive, multi-line, dot-matches-newline' },
]
// "@" icon next to regex inputs; shows a compact cheat sheet for the regex flavor the server actually
// uses (Rust `regex` crate — no lookaround/backreferences), modeled on HelpIcon's button styling.
export function RegexHelpIcon({ label }: { label?: string }) {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!open) return
function onClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
}
document.addEventListener('mousedown', onClickOutside)
return () => document.removeEventListener('mousedown', onClickOutside)
}, [open])
return (
<div ref={ref} className="relative inline-flex" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
<button
type="button"
onClick={() => setOpen((v) => !v)}
title="Regex syntax cheat sheet"
aria-label={label ?? 'Regex syntax cheat sheet'}
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
>
@
</button>
{open && (
<div className="absolute left-0 top-full z-10 mt-1 w-72 rounded border border-border-soft bg-surface-0 p-2 shadow-lg">
<p className="mb-1.5 text-[11px] font-medium text-text-secondary">Regex syntax (Rust regex crate)</p>
<table className="w-full border-collapse text-[11px]">
<tbody>
{CHEAT_SHEET.map(({ tokens, meaning }) => (
<tr key={tokens}>
<td className="whitespace-nowrap py-0.5 pr-2 align-top font-mono text-text">{tokens}</td>
<td className="py-0.5 align-top text-text-muted">{meaning}</td>
</tr>
))}
</tbody>
</table>
<p className="mt-1.5 text-[10px] text-text-subtle">No lookaround or backreferences (not supported by the Rust regex crate).</p>
</div>
)}
</div>
)
}