- Extract tab components: JailsTab, ActionsTab, FiltersTab, JailFilesTab, GlobalTab, ServerTab, ConfFilesTab, RegexTesterTab, MapTab, ExportTab - Add form components: JailFileForm, ActionForm, FilterForm - Add AutoSaveIndicator, RegexList, configStyles, and barrel index - ConfigPage now composes these components; greatly reduces file size - Add tests: ConfigPage.test.tsx, useAutoSave.test.ts
223 lines
6.8 KiB
TypeScript
223 lines
6.8 KiB
TypeScript
/**
|
|
* RegexTesterTab — live regex pattern tester and log file preview.
|
|
*
|
|
* Provides two panels:
|
|
* 1. Single-line tester: paste a log line and a regex, get a match result.
|
|
* 2. Log file preview: read N lines from a server file and highlight matches.
|
|
*/
|
|
|
|
import { useCallback, useState } from "react";
|
|
import {
|
|
Badge,
|
|
Button,
|
|
Field,
|
|
Input,
|
|
MessageBar,
|
|
MessageBarBody,
|
|
Text,
|
|
Textarea,
|
|
tokens,
|
|
} from "@fluentui/react-components";
|
|
import {
|
|
Checkmark24Regular,
|
|
Dismiss24Regular,
|
|
} from "@fluentui/react-icons";
|
|
import { useLogPreview, useRegexTester } from "../../hooks/useConfig";
|
|
import { useConfigStyles } from "./configStyles";
|
|
|
|
/**
|
|
* Tab component for testing regex patterns against log lines or full files.
|
|
*
|
|
* @returns JSX element.
|
|
*/
|
|
export function RegexTesterTab(): React.JSX.Element {
|
|
const styles = useConfigStyles();
|
|
const { result, testing, test } = useRegexTester();
|
|
const { preview, loading: previewing, run: runPreview } = useLogPreview();
|
|
const [logLine, setLogLine] = useState("");
|
|
const [pattern, setPattern] = useState("");
|
|
const [previewPath, setPreviewPath] = useState("");
|
|
const [previewLines, setPreviewLines] = useState("200");
|
|
|
|
const handleTest = useCallback(async () => {
|
|
if (!logLine.trim() || !pattern.trim()) return;
|
|
await test({ log_line: logLine, fail_regex: pattern });
|
|
}, [logLine, pattern, test]);
|
|
|
|
const handlePreview = useCallback(async () => {
|
|
if (!previewPath.trim() || !pattern.trim()) return;
|
|
await runPreview({
|
|
log_path: previewPath,
|
|
fail_regex: pattern,
|
|
num_lines: Number(previewLines) || 200,
|
|
});
|
|
}, [previewPath, pattern, previewLines, runPreview]);
|
|
|
|
return (
|
|
<div>
|
|
{/* Single-line tester */}
|
|
<Text as="h3" size={500} weight="semibold" block>
|
|
Regex Tester
|
|
</Text>
|
|
<Text size={200} className={styles.infoText} block>
|
|
Test a pattern against a single sample log line.
|
|
</Text>
|
|
<div style={{ marginTop: tokens.spacingVerticalS }}>
|
|
<Field label="Fail Regex Pattern">
|
|
<Input
|
|
className={styles.codeFont}
|
|
value={pattern}
|
|
placeholder="e.g. (?P<host>\S+)"
|
|
onChange={(_e, d) => {
|
|
setPattern(d.value);
|
|
}}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label="Sample Log Line"
|
|
style={{ marginTop: tokens.spacingVerticalS }}
|
|
>
|
|
<Textarea
|
|
className={styles.codeFont}
|
|
value={logLine}
|
|
placeholder="Paste a log line here…"
|
|
rows={3}
|
|
onChange={(_e, d) => {
|
|
setLogLine(d.value);
|
|
}}
|
|
/>
|
|
</Field>
|
|
</div>
|
|
<div className={styles.buttonRow}>
|
|
<Button
|
|
appearance="primary"
|
|
disabled={testing || !logLine.trim() || !pattern.trim()}
|
|
onClick={() => void handleTest()}
|
|
>
|
|
{testing ? "Testing…" : "Test Pattern"}
|
|
</Button>
|
|
</div>
|
|
{result && (
|
|
<div
|
|
style={{
|
|
marginTop: tokens.spacingVerticalM,
|
|
padding: tokens.spacingVerticalS,
|
|
border: `1px solid ${tokens.colorNeutralStroke1}`,
|
|
borderRadius: tokens.borderRadiusMedium,
|
|
}}
|
|
>
|
|
{result.error ? (
|
|
<MessageBar intent="error">
|
|
<MessageBarBody>Regex error: {result.error}</MessageBarBody>
|
|
</MessageBar>
|
|
) : (
|
|
<>
|
|
<Badge
|
|
size="large"
|
|
appearance="filled"
|
|
color={result.matched ? "success" : "danger"}
|
|
icon={
|
|
result.matched ? (
|
|
<Checkmark24Regular />
|
|
) : (
|
|
<Dismiss24Regular />
|
|
)
|
|
}
|
|
>
|
|
{result.matched ? "Matched" : "No match"}
|
|
</Badge>
|
|
{result.matched && result.groups.length > 0 && (
|
|
<div style={{ marginTop: tokens.spacingVerticalS }}>
|
|
<Text size={200} weight="semibold">
|
|
Captured groups:
|
|
</Text>
|
|
{result.groups.map((g, i) => (
|
|
<Badge
|
|
key={i}
|
|
appearance="tint"
|
|
color="informative"
|
|
style={{ marginLeft: tokens.spacingHorizontalXS }}
|
|
className={styles.codeFont}
|
|
>
|
|
{g}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Log file preview */}
|
|
<div style={{ marginTop: tokens.spacingVerticalXL }}>
|
|
<Text as="h3" size={500} weight="semibold" block>
|
|
Log File Preview
|
|
</Text>
|
|
<Text size={200} className={styles.infoText} block>
|
|
Read the last N lines from a log file on the server and highlight
|
|
matches.
|
|
</Text>
|
|
<div
|
|
className={styles.fieldRow}
|
|
style={{ marginTop: tokens.spacingVerticalS }}
|
|
>
|
|
<Field label="Log File Path">
|
|
<Input
|
|
className={styles.codeFont}
|
|
value={previewPath}
|
|
placeholder="/var/log/auth.log"
|
|
onChange={(_e, d) => {
|
|
setPreviewPath(d.value);
|
|
}}
|
|
/>
|
|
</Field>
|
|
<Field label="Lines to Read">
|
|
<Input
|
|
type="number"
|
|
value={previewLines}
|
|
onChange={(_e, d) => {
|
|
setPreviewLines(d.value);
|
|
}}
|
|
/>
|
|
</Field>
|
|
</div>
|
|
<div className={styles.buttonRow}>
|
|
<Button
|
|
appearance="secondary"
|
|
disabled={previewing || !previewPath.trim() || !pattern.trim()}
|
|
onClick={() => void handlePreview()}
|
|
>
|
|
{previewing ? "Loading…" : "Preview Log"}
|
|
</Button>
|
|
</div>
|
|
{preview && (
|
|
<div style={{ marginTop: tokens.spacingVerticalS }}>
|
|
{preview.regex_error ? (
|
|
<MessageBar intent="error">
|
|
<MessageBarBody>{preview.regex_error}</MessageBarBody>
|
|
</MessageBar>
|
|
) : (
|
|
<>
|
|
<Text size={200}>
|
|
{preview.matched_count} / {preview.total_lines} lines matched
|
|
</Text>
|
|
<div className={styles.previewArea}>
|
|
{preview.lines.map((ln, idx) => (
|
|
<div
|
|
key={idx}
|
|
className={`${styles.logLine} ${ln.matched ? styles.matched : styles.notMatched}`}
|
|
>
|
|
{ln.line}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|