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
This commit is contained in:
222
frontend/src/components/config/RegexTesterTab.tsx
Normal file
222
frontend/src/components/config/RegexTesterTab.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user