Files
BanGUI/frontend/src/components/config/RegexTesterTab.tsx
Lukas 9b73f6719d refactor(frontend): decompose ConfigPage into dedicated config components
- 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
2026-03-13 13:48:09 +01:00

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