Add regex cheat-sheet icon next to regex inputs
Adds RegexHelpIcon, a hover/click popover (modeled on HelpIcon) showing a compact cheat sheet for the Rust `regex` crate's actual syntax, and places it next to every user-facing regex field: Params/Metrics message match regex, Alerts field regex, and FilterBar's regex(...) helper. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e6e5bc8f50
commit
83cc7105d6
@@ -11,6 +11,7 @@ import {
|
||||
} from '../lib/api'
|
||||
import { sectionHeaderClassName, sectionHeaderLeftClassName } from './CollapsibleSection'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
import { RegexHelpIcon } from './RegexHelpIcon'
|
||||
import { queryInputClassName } from './FilterBar'
|
||||
|
||||
type ConditionKind = AlertCondition['kind']
|
||||
@@ -323,7 +324,10 @@ export function Alerts() {
|
||||
{form.kind === 'newvalue' && (
|
||||
<div className="space-y-1">
|
||||
<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
|
||||
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}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useState } from 'react'
|
||||
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.
|
||||
export const queryInputClassName =
|
||||
@@ -30,6 +31,7 @@ export function FilterBar({
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpIcon sectionId="filtering" label="Help: Filtering" />
|
||||
<RegexHelpIcon label="Help: regex(...) syntax" />
|
||||
{collapseToggle}
|
||||
<input
|
||||
className={queryInputClassName}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../lib/api'
|
||||
import { CollapseToggle, PopOutButton, sectionHeaderClassName, sectionHeaderLeftClassName, useSectionCollapsed } from './CollapsibleSection'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
import { RegexHelpIcon } from './RegexHelpIcon'
|
||||
|
||||
const EMPTY_MATCHER: MetricParamMatcher = { param_name: '', capture_group: '', kind: 'string' }
|
||||
|
||||
@@ -174,7 +175,10 @@ export function Metrics({ range }: { range: TimeRange | null }) {
|
||||
/>
|
||||
</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
|
||||
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}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
import { ApiError, declareParam, type ParamDeclaration } from '../lib/api'
|
||||
import { CollapseToggle, PopOutButton, sectionHeaderClassName, sectionHeaderLeftClassName, useSectionCollapsed } from './CollapsibleSection'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
import { RegexHelpIcon } from './RegexHelpIcon'
|
||||
|
||||
const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
|
||||
|
||||
@@ -57,7 +58,10 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
|
||||
/>
|
||||
</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
|
||||
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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user