Merge ticket/017-regex-cheat-sheet-icon
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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