Merge Map tab into Server tab and remove Map tab
The Map tab provided a form for editing world-map color thresholds (low, medium, high). Moving this into the Server tab consolidates all server-side configuration in one place. - Add map color thresholds section to ServerTab with full validation - Load map thresholds on component mount with useEffect - Implement auto-save for threshold changes via useAutoSave hook - Display threshold color interpolation guide - Remove MapTab component import from ConfigPage - Remove 'map' from TabValue type - Remove Map tab element from TabList - Remove conditional render for MapTab - Remove MapTab from barrel export (index.ts) - Delete MapTab.tsx file - Update ConfigPage test to remove MapTab mock All 123 frontend tests pass.
This commit is contained in:
@@ -1,212 +0,0 @@
|
|||||||
/**
|
|
||||||
* MapTab — world map color threshold configuration editor.
|
|
||||||
*
|
|
||||||
* Allows the user to set the low / medium / high ban-count thresholds
|
|
||||||
* that drive country fill colors on the World Map page.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
Input,
|
|
||||||
MessageBar,
|
|
||||||
MessageBarBody,
|
|
||||||
Skeleton,
|
|
||||||
SkeletonItem,
|
|
||||||
Text,
|
|
||||||
tokens,
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import { ApiError } from "../../api/client";
|
|
||||||
import {
|
|
||||||
fetchMapColorThresholds,
|
|
||||||
updateMapColorThresholds,
|
|
||||||
} from "../../api/config";
|
|
||||||
import type { MapColorThresholdsResponse, MapColorThresholdsUpdate } from "../../types/config";
|
|
||||||
import { useAutoSave } from "../../hooks/useAutoSave";
|
|
||||||
import { AutoSaveIndicator } from "./AutoSaveIndicator";
|
|
||||||
import { useConfigStyles } from "./configStyles";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Inner form — only mounted after data is loaded.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
interface MapFormProps {
|
|
||||||
initial: MapColorThresholdsResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MapForm({ initial }: MapFormProps): React.JSX.Element {
|
|
||||||
const styles = useConfigStyles();
|
|
||||||
const [thresholdHigh, setThresholdHigh] = useState(String(initial.threshold_high));
|
|
||||||
const [thresholdMedium, setThresholdMedium] = useState(String(initial.threshold_medium));
|
|
||||||
const [thresholdLow, setThresholdLow] = useState(String(initial.threshold_low));
|
|
||||||
|
|
||||||
const high = Number(thresholdHigh);
|
|
||||||
const medium = Number(thresholdMedium);
|
|
||||||
const low = Number(thresholdLow);
|
|
||||||
|
|
||||||
const validationError = useMemo<string | null>(() => {
|
|
||||||
if (isNaN(high) || isNaN(medium) || isNaN(low))
|
|
||||||
return "All thresholds must be valid numbers.";
|
|
||||||
if (high <= 0 || medium <= 0 || low <= 0)
|
|
||||||
return "All thresholds must be positive integers.";
|
|
||||||
if (!(high > medium && medium > low))
|
|
||||||
return "Thresholds must satisfy: high > medium > low.";
|
|
||||||
return null;
|
|
||||||
}, [high, medium, low]);
|
|
||||||
|
|
||||||
// Only pass a new payload to useAutoSave when all values are valid.
|
|
||||||
const [validPayload, setValidPayload] = useState<MapColorThresholdsUpdate>({
|
|
||||||
threshold_high: initial.threshold_high,
|
|
||||||
threshold_medium: initial.threshold_medium,
|
|
||||||
threshold_low: initial.threshold_low,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (validationError !== null) return;
|
|
||||||
setValidPayload({ threshold_high: high, threshold_medium: medium, threshold_low: low });
|
|
||||||
}, [high, medium, low, validationError]);
|
|
||||||
|
|
||||||
const saveThresholds = useCallback(
|
|
||||||
async (payload: MapColorThresholdsUpdate): Promise<void> => {
|
|
||||||
await updateMapColorThresholds(payload);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { status: saveStatus, errorText: saveErrorText, retry: retrySave } =
|
|
||||||
useAutoSave(validPayload, saveThresholds);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.sectionCard}>
|
|
||||||
<Text as="h3" size={500} weight="semibold" block>
|
|
||||||
Map Color Thresholds
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
as="p"
|
|
||||||
size={300}
|
|
||||||
className={styles.infoText}
|
|
||||||
block
|
|
||||||
style={{ marginBottom: tokens.spacingVerticalM }}
|
|
||||||
>
|
|
||||||
Configure the ban count thresholds that determine country fill colors on
|
|
||||||
the World Map. Countries with zero bans remain transparent. Colors
|
|
||||||
smoothly interpolate between thresholds.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: tokens.spacingVerticalS }}>
|
|
||||||
<AutoSaveIndicator
|
|
||||||
status={validationError ? "idle" : saveStatus}
|
|
||||||
errorText={saveErrorText}
|
|
||||||
onRetry={retrySave}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{validationError && (
|
|
||||||
<MessageBar intent="error" style={{ marginBottom: tokens.spacingVerticalS }}>
|
|
||||||
<MessageBarBody>{validationError}</MessageBarBody>
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={styles.fieldRowThree}>
|
|
||||||
<Field label="Low Threshold (Green)" required>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={thresholdLow}
|
|
||||||
onChange={(_, d) => {
|
|
||||||
setThresholdLow(d.value);
|
|
||||||
}}
|
|
||||||
min={1}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Medium Threshold (Yellow)" required>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={thresholdMedium}
|
|
||||||
onChange={(_, d) => {
|
|
||||||
setThresholdMedium(d.value);
|
|
||||||
}}
|
|
||||||
min={1}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="High Threshold (Red)" required>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={thresholdHigh}
|
|
||||||
onChange={(_, d) => {
|
|
||||||
setThresholdHigh(d.value);
|
|
||||||
}}
|
|
||||||
min={1}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
as="p"
|
|
||||||
size={200}
|
|
||||||
className={styles.infoText}
|
|
||||||
style={{ marginTop: tokens.spacingVerticalS }}
|
|
||||||
>
|
|
||||||
• 1 to {thresholdLow}: Light green → Full green
|
|
||||||
<br />• {thresholdLow} to {thresholdMedium}: Green → Yellow
|
|
||||||
<br />• {thresholdMedium} to {thresholdHigh}: Yellow → Red
|
|
||||||
<br />• {thresholdHigh}+: Solid red
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Outer loader component.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tab component for editing world-map ban-count color thresholds.
|
|
||||||
*
|
|
||||||
* @returns JSX element.
|
|
||||||
*/
|
|
||||||
export function MapTab(): React.JSX.Element {
|
|
||||||
const [thresholds, setThresholds] = useState<MapColorThresholdsResponse | null>(null);
|
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const load = useCallback(async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const data = await fetchMapColorThresholds();
|
|
||||||
setThresholds(data);
|
|
||||||
} catch (err) {
|
|
||||||
setLoadError(
|
|
||||||
err instanceof ApiError ? err.message : "Failed to load map color thresholds",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void load();
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
if (!thresholds && !loadError) {
|
|
||||||
return (
|
|
||||||
<Skeleton aria-label="Loading map settings…">
|
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8 }}>
|
|
||||||
<SkeletonItem size={32} />
|
|
||||||
<SkeletonItem size={32} />
|
|
||||||
<SkeletonItem size={32} />
|
|
||||||
</div>
|
|
||||||
</Skeleton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadError)
|
|
||||||
return (
|
|
||||||
<MessageBar intent="error">
|
|
||||||
<MessageBarBody>{loadError}</MessageBarBody>
|
|
||||||
</MessageBar>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!thresholds) return <></>;
|
|
||||||
|
|
||||||
return <MapForm initial={thresholds} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2,10 +2,11 @@
|
|||||||
* ServerTab — fail2ban server-level settings editor.
|
* ServerTab — fail2ban server-level settings editor.
|
||||||
*
|
*
|
||||||
* Provides form fields for live server settings (log level, log target,
|
* Provides form fields for live server settings (log level, log target,
|
||||||
* DB purge age, DB max matches) and a "Flush Logs" action button.
|
* DB purge age, DB max matches), a "Flush Logs" action button, and
|
||||||
|
* world map color threshold configuration.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Field,
|
Field,
|
||||||
@@ -15,15 +16,20 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
SkeletonItem,
|
SkeletonItem,
|
||||||
|
Text,
|
||||||
tokens,
|
tokens,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import {
|
import {
|
||||||
DocumentArrowDown24Regular,
|
DocumentArrowDown24Regular,
|
||||||
} from "@fluentui/react-icons";
|
} from "@fluentui/react-icons";
|
||||||
import { ApiError } from "../../api/client";
|
import { ApiError } from "../../api/client";
|
||||||
import type { ServerSettingsUpdate } from "../../types/config";
|
import type { ServerSettingsUpdate, MapColorThresholdsResponse, MapColorThresholdsUpdate } from "../../types/config";
|
||||||
import { useServerSettings } from "../../hooks/useConfig";
|
import { useServerSettings } from "../../hooks/useConfig";
|
||||||
import { useAutoSave } from "../../hooks/useAutoSave";
|
import { useAutoSave } from "../../hooks/useAutoSave";
|
||||||
|
import {
|
||||||
|
fetchMapColorThresholds,
|
||||||
|
updateMapColorThresholds,
|
||||||
|
} from "../../api/config";
|
||||||
import { AutoSaveIndicator } from "./AutoSaveIndicator";
|
import { AutoSaveIndicator } from "./AutoSaveIndicator";
|
||||||
import { useConfigStyles } from "./configStyles";
|
import { useConfigStyles } from "./configStyles";
|
||||||
|
|
||||||
@@ -46,6 +52,13 @@ export function ServerTab(): React.JSX.Element {
|
|||||||
const [flushing, setFlushing] = useState(false);
|
const [flushing, setFlushing] = useState(false);
|
||||||
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null);
|
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null);
|
||||||
|
|
||||||
|
// Map color thresholds
|
||||||
|
const [mapThresholds, setMapThresholds] = useState<MapColorThresholdsResponse | null>(null);
|
||||||
|
const [mapThresholdHigh, setMapThresholdHigh] = useState("");
|
||||||
|
const [mapThresholdMedium, setMapThresholdMedium] = useState("");
|
||||||
|
const [mapThresholdLow, setMapThresholdLow] = useState("");
|
||||||
|
const [mapLoadError, setMapLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
const effectiveLogLevel = logLevel || settings?.log_level || "";
|
const effectiveLogLevel = logLevel || settings?.log_level || "";
|
||||||
const effectiveLogTarget = logTarget || settings?.log_target || "";
|
const effectiveLogTarget = logTarget || settings?.log_target || "";
|
||||||
const effectiveDbPurgeAge =
|
const effectiveDbPurgeAge =
|
||||||
@@ -83,6 +96,67 @@ export function ServerTab(): React.JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [flush]);
|
}, [flush]);
|
||||||
|
|
||||||
|
// Load map color thresholds on mount.
|
||||||
|
const loadMapThresholds = useCallback(async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const data = await fetchMapColorThresholds();
|
||||||
|
setMapThresholds(data);
|
||||||
|
setMapThresholdHigh(String(data.threshold_high));
|
||||||
|
setMapThresholdMedium(String(data.threshold_medium));
|
||||||
|
setMapThresholdLow(String(data.threshold_low));
|
||||||
|
setMapLoadError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setMapLoadError(
|
||||||
|
err instanceof ApiError ? err.message : "Failed to load map color thresholds",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadMapThresholds();
|
||||||
|
}, [loadMapThresholds]);
|
||||||
|
|
||||||
|
// Map threshold validation and auto-save.
|
||||||
|
const mapHigh = Number(mapThresholdHigh);
|
||||||
|
const mapMedium = Number(mapThresholdMedium);
|
||||||
|
const mapLow = Number(mapThresholdLow);
|
||||||
|
|
||||||
|
const mapValidationError = useMemo<string | null>(() => {
|
||||||
|
if (!mapThresholds) return null;
|
||||||
|
if (isNaN(mapHigh) || isNaN(mapMedium) || isNaN(mapLow))
|
||||||
|
return "All thresholds must be valid numbers.";
|
||||||
|
if (mapHigh <= 0 || mapMedium <= 0 || mapLow <= 0)
|
||||||
|
return "All thresholds must be positive integers.";
|
||||||
|
if (!(mapHigh > mapMedium && mapMedium > mapLow))
|
||||||
|
return "Thresholds must satisfy: high > medium > low.";
|
||||||
|
return null;
|
||||||
|
}, [mapHigh, mapMedium, mapLow, mapThresholds]);
|
||||||
|
|
||||||
|
const [mapValidPayload, setMapValidPayload] = useState<MapColorThresholdsUpdate>({
|
||||||
|
threshold_high: mapThresholds?.threshold_high ?? 0,
|
||||||
|
threshold_medium: mapThresholds?.threshold_medium ?? 0,
|
||||||
|
threshold_low: mapThresholds?.threshold_low ?? 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapValidationError !== null || !mapThresholds) return;
|
||||||
|
setMapValidPayload({
|
||||||
|
threshold_high: mapHigh,
|
||||||
|
threshold_medium: mapMedium,
|
||||||
|
threshold_low: mapLow,
|
||||||
|
});
|
||||||
|
}, [mapHigh, mapMedium, mapLow, mapValidationError, mapThresholds]);
|
||||||
|
|
||||||
|
const saveMapThresholds = useCallback(
|
||||||
|
async (payload: MapColorThresholdsUpdate): Promise<void> => {
|
||||||
|
await updateMapColorThresholds(payload);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { status: mapSaveStatus, errorText: mapSaveErrorText, retry: retryMapSave } =
|
||||||
|
useAutoSave(mapValidPayload, saveMapThresholds);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Skeleton aria-label="Loading server settings…">
|
<Skeleton aria-label="Loading server settings…">
|
||||||
@@ -195,6 +269,91 @@ export function ServerTab(): React.JSX.Element {
|
|||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Map Color Thresholds section */}
|
||||||
|
{mapLoadError ? (
|
||||||
|
<div className={styles.sectionCard}>
|
||||||
|
<MessageBar intent="error">
|
||||||
|
<MessageBarBody>{mapLoadError}</MessageBarBody>
|
||||||
|
</MessageBar>
|
||||||
|
</div>
|
||||||
|
) : mapThresholds ? (
|
||||||
|
<div className={styles.sectionCard}>
|
||||||
|
<Text as="h3" size={500} weight="semibold" block>
|
||||||
|
Map Color Thresholds
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
as="p"
|
||||||
|
size={300}
|
||||||
|
className={styles.infoText}
|
||||||
|
block
|
||||||
|
style={{ marginBottom: tokens.spacingVerticalM }}
|
||||||
|
>
|
||||||
|
Configure the ban count thresholds that determine country fill colors on
|
||||||
|
the World Map. Countries with zero bans remain transparent. Colors
|
||||||
|
smoothly interpolate between thresholds.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: tokens.spacingVerticalS }}>
|
||||||
|
<AutoSaveIndicator
|
||||||
|
status={mapValidationError ? "idle" : mapSaveStatus}
|
||||||
|
errorText={mapSaveErrorText}
|
||||||
|
onRetry={retryMapSave}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mapValidationError && (
|
||||||
|
<MessageBar intent="error" style={{ marginBottom: tokens.spacingVerticalS }}>
|
||||||
|
<MessageBarBody>{mapValidationError}</MessageBarBody>
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.fieldRowThree}>
|
||||||
|
<Field label="Low Threshold (Green)" required>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={mapThresholdLow}
|
||||||
|
onChange={(_, d) => {
|
||||||
|
setMapThresholdLow(d.value);
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Medium Threshold (Yellow)" required>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={mapThresholdMedium}
|
||||||
|
onChange={(_, d) => {
|
||||||
|
setMapThresholdMedium(d.value);
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="High Threshold (Red)" required>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={mapThresholdHigh}
|
||||||
|
onChange={(_, d) => {
|
||||||
|
setMapThresholdHigh(d.value);
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
as="p"
|
||||||
|
size={200}
|
||||||
|
className={styles.infoText}
|
||||||
|
style={{ marginTop: tokens.spacingVerticalS }}
|
||||||
|
>
|
||||||
|
• 1 to {mapThresholdLow}: Light green → Full green
|
||||||
|
<br />• {mapThresholdLow} to {mapThresholdMedium}: Green → Yellow
|
||||||
|
<br />• {mapThresholdMedium} to {mapThresholdHigh}: Yellow → Red
|
||||||
|
<br />• {mapThresholdHigh}+: Solid red
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export { JailFilesTab } from "./JailFilesTab";
|
|||||||
export { JailFileForm } from "./JailFileForm";
|
export { JailFileForm } from "./JailFileForm";
|
||||||
export { JailsTab } from "./JailsTab";
|
export { JailsTab } from "./JailsTab";
|
||||||
export { LogTab } from "./LogTab";
|
export { LogTab } from "./LogTab";
|
||||||
export { MapTab } from "./MapTab";
|
|
||||||
export { RawConfigSection } from "./RawConfigSection";
|
export { RawConfigSection } from "./RawConfigSection";
|
||||||
export type { RawConfigSectionProps } from "./RawConfigSection";
|
export type { RawConfigSectionProps } from "./RawConfigSection";
|
||||||
export { RegexList } from "./RegexList";
|
export { RegexList } from "./RegexList";
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
* Jails — per-jail config accordion with inline editing
|
* Jails — per-jail config accordion with inline editing
|
||||||
* Filters — structured filter.d form editor
|
* Filters — structured filter.d form editor
|
||||||
* Actions — structured action.d form editor
|
* Actions — structured action.d form editor
|
||||||
* Server — server-level settings, logging, database config + flush logs
|
* Server — server-level settings, logging, database config, map thresholds + flush logs
|
||||||
* Map — map color threshold configuration
|
|
||||||
* Regex Tester — live pattern tester
|
* Regex Tester — live pattern tester
|
||||||
* Export — raw file editors for jail, filter, and action files
|
* Export — raw file editors for jail, filter, and action files
|
||||||
*/
|
*/
|
||||||
@@ -21,7 +20,6 @@ import {
|
|||||||
FiltersTab,
|
FiltersTab,
|
||||||
JailsTab,
|
JailsTab,
|
||||||
LogTab,
|
LogTab,
|
||||||
MapTab,
|
|
||||||
RegexTesterTab,
|
RegexTesterTab,
|
||||||
ServerTab,
|
ServerTab,
|
||||||
} from "../components/config";
|
} from "../components/config";
|
||||||
@@ -57,7 +55,6 @@ type TabValue =
|
|||||||
| "filters"
|
| "filters"
|
||||||
| "actions"
|
| "actions"
|
||||||
| "server"
|
| "server"
|
||||||
| "map"
|
|
||||||
| "regex"
|
| "regex"
|
||||||
| "log";
|
| "log";
|
||||||
|
|
||||||
@@ -87,7 +84,6 @@ export function ConfigPage(): React.JSX.Element {
|
|||||||
<Tab value="filters">Filters</Tab>
|
<Tab value="filters">Filters</Tab>
|
||||||
<Tab value="actions">Actions</Tab>
|
<Tab value="actions">Actions</Tab>
|
||||||
<Tab value="server">Server</Tab>
|
<Tab value="server">Server</Tab>
|
||||||
<Tab value="map">Map</Tab>
|
|
||||||
<Tab value="regex">Regex Tester</Tab>
|
<Tab value="regex">Regex Tester</Tab>
|
||||||
<Tab value="log">Log</Tab>
|
<Tab value="log">Log</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
@@ -97,7 +93,6 @@ export function ConfigPage(): React.JSX.Element {
|
|||||||
{tab === "filters" && <FiltersTab />}
|
{tab === "filters" && <FiltersTab />}
|
||||||
{tab === "actions" && <ActionsTab />}
|
{tab === "actions" && <ActionsTab />}
|
||||||
{tab === "server" && <ServerTab />}
|
{tab === "server" && <ServerTab />}
|
||||||
{tab === "map" && <MapTab />}
|
|
||||||
{tab === "regex" && <RegexTesterTab />}
|
{tab === "regex" && <RegexTesterTab />}
|
||||||
{tab === "log" && <LogTab />}
|
{tab === "log" && <LogTab />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ vi.mock("../../components/config", () => ({
|
|||||||
FiltersTab: () => <div data-testid="filters-tab">FiltersTab</div>,
|
FiltersTab: () => <div data-testid="filters-tab">FiltersTab</div>,
|
||||||
ActionsTab: () => <div data-testid="actions-tab">ActionsTab</div>,
|
ActionsTab: () => <div data-testid="actions-tab">ActionsTab</div>,
|
||||||
ServerTab: () => <div data-testid="server-tab">ServerTab</div>,
|
ServerTab: () => <div data-testid="server-tab">ServerTab</div>,
|
||||||
MapTab: () => <div data-testid="map-tab">MapTab</div>,
|
|
||||||
RegexTesterTab: () => <div data-testid="regex-tab">RegexTesterTab</div>,
|
RegexTesterTab: () => <div data-testid="regex-tab">RegexTesterTab</div>,
|
||||||
ExportTab: () => <div data-testid="export-tab">ExportTab</div>,
|
ExportTab: () => <div data-testid="export-tab">ExportTab</div>,
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user