Refactor frontend components and dependencies

- Update ESLint configuration for frontend
- Refactor dialog components (ActivateJail, CreateAction, CreateFilter, CreateJail)
- Update JailsTab and RegexTesterTab components
- Refactor TopCountriesPieChart component
- Update package.json dependencies
- Update documentation (Tasks.md)
- Refactor CodeList component for jail page

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-22 20:26:43 +02:00
parent 1bf0645c04
commit a286ede49c
15 changed files with 1395655 additions and 31 deletions

View File

@@ -611,7 +611,11 @@ Do **not** add a Content-Security-Policy meta tag; CSP should be set as an HTTP
--- ---
### TASK-030 — Add manual chunk splitting to `vite.config.ts` ### TASK-030 — Add manual chunk splitting to `vite.config.ts` (done)
**Where fixed:** `frontend/vite.config.ts`
**Summary:** Implemented `build.rollupOptions.output.manualChunks` function that groups modules into four vendor chunks: `react-vendor`, `ui-vendor`, `chart-vendor`, and `geo-vendor`. This ensures stable, cacheable chunks across deployments.
**Where found:** `frontend/vite.config.ts``build` section is absent entirely; Rollup uses its default single-chunk strategy. **Where found:** `frontend/vite.config.ts``build` section is absent entirely; Rollup uses its default single-chunk strategy.

View File

@@ -1,10 +1,11 @@
import js from "@eslint/js"; import js from "@eslint/js";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks"; import reactHooks from "eslint-plugin-react-hooks";
import reactPlugin from "eslint-plugin-react";
import prettierConfig from "eslint-config-prettier"; import prettierConfig from "eslint-config-prettier";
export default tseslint.config( export default tseslint.config(
{ ignores: ["dist", "eslint.config.ts"] }, { ignores: ["dist", "eslint.config.ts", ".vite"] },
{ {
extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked], extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked],
files: ["**/*.{ts,tsx}"], files: ["**/*.{ts,tsx}"],
@@ -15,10 +16,21 @@ export default tseslint.config(
}, },
}, },
plugins: { plugins: {
react: reactPlugin,
"react-hooks": reactHooks, "react-hooks": reactHooks,
}, },
settings: {
react: {
version: "detect",
},
},
rules: { rules: {
...reactHooks.configs.recommended.rules, ...reactHooks.configs.recommended.rules,
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"react/no-array-index-key": "warn",
"react/no-unstable-nested-components": "error",
"@typescript-eslint/switch-exhaustiveness-check": "error",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "warn", "@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],

File diff suppressed because it is too large Load Diff

View File

@@ -21,26 +21,27 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.27.0", "react-router-dom": "^6.27.0",
"recharts": "^3.8.0",
"topojson-client": "^3.1.0", "topojson-client": "^3.1.0",
"world-atlas": "^2.0.2", "world-atlas": "^2.0.2"
"recharts": "^3.8.0"
}, },
"devDependencies": { "devDependencies": {
"@types/d3-geo": "^3.1.0",
"@types/topojson-client": "^3.0.0",
"@eslint/js": "^9.13.0", "@eslint/js": "^9.13.0",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/d3-geo": "^3.1.0",
"@types/node": "^25.3.2", "@types/node": "^25.3.2",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@types/topojson-client": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/eslint-plugin": "^8.13.0",
"@typescript-eslint/parser": "^8.13.0", "@typescript-eslint/parser": "^8.13.0",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.3.3",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",
"eslint": "^9.13.0", "eslint": "^9.13.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"jsdom": "^28.1.0", "jsdom": "^28.1.0",

View File

@@ -188,9 +188,9 @@ export const TopCountriesPieChart = memo(function TopCountriesPieChart({
}} }}
labelLine={false} labelLine={false}
> >
{slices.map((slice, index) => ( {slices.map((slice) => (
// eslint-disable-next-line @typescript-eslint/no-deprecated // eslint-disable-next-line @typescript-eslint/no-deprecated
<Cell key={index} fill={slice.fill} /> <Cell key={slice.name} fill={slice.fill} />
))} ))}
</Pie> </Pie>
<Tooltip content={PieTooltip} /> <Tooltip content={PieTooltip} />

View File

@@ -227,7 +227,7 @@ export function ActivateJailDialog({
<strong>Configuration errors detected:</strong> <strong>Configuration errors detected:</strong>
<ul style={{ margin: `${tokens.spacingVerticalXS} 0 0 0`, paddingLeft: "1.2em" }}> <ul style={{ margin: `${tokens.spacingVerticalXS} 0 0 0`, paddingLeft: "1.2em" }}>
{blockingIssues.map((issue, idx) => ( {blockingIssues.map((issue, idx) => (
<li key={idx}><em>{issue.field}:</em> {issue.message}</li> <li key={issue.field + "-" + String(idx)}><em>{issue.field}:</em> {issue.message}</li>
))} ))}
</ul> </ul>
</MessageBarBody> </MessageBarBody>
@@ -242,7 +242,7 @@ export function ActivateJailDialog({
<strong>Advisory warnings:</strong> <strong>Advisory warnings:</strong>
<ul style={{ margin: `${tokens.spacingVerticalXS} 0 0 0`, paddingLeft: "1.2em" }}> <ul style={{ margin: `${tokens.spacingVerticalXS} 0 0 0`, paddingLeft: "1.2em" }}>
{advisoryIssues.map((issue, idx) => ( {advisoryIssues.map((issue, idx) => (
<li key={idx}><em>{issue.field}:</em> {issue.message}</li> <li key={issue.field + "-" + String(idx)}><em>{issue.field}:</em> {issue.message}</li>
))} ))}
</ul> </ul>
</MessageBarBody> </MessageBarBody>
@@ -257,16 +257,12 @@ export function ActivateJailDialog({
<strong>Post-activation warnings:</strong> <strong>Post-activation warnings:</strong>
<ul style={{ margin: `${tokens.spacingVerticalXS} 0 0 0`, paddingLeft: "1.2em" }}> <ul style={{ margin: `${tokens.spacingVerticalXS} 0 0 0`, paddingLeft: "1.2em" }}>
{validationWarnings.map((w, idx) => ( {validationWarnings.map((w, idx) => (
<li key={idx}>{w}</li> <li key={w + "-" + String(idx)}>{w}</li>
))} ))}
</ul> </ul>
</MessageBarBody> </MessageBarBody>
</MessageBar> </MessageBar>
)} )}
<Text block weight="semibold" style={{ marginBottom: tokens.spacingVerticalS }}>
Override values (leave blank to use config defaults)
</Text>
<div <div
style={{ style={{
display: "grid", display: "grid",

View File

@@ -114,7 +114,7 @@ export function CreateActionDialog({
.finally(() => { .finally(() => {
setSubmitting(false); setSubmitting(false);
}); });
}, [name, actionban, actionunban, submitting, onCreate]); }, [name, actionban, actionunban, submitting, onCreate, onCreateAction]);
const canConfirm = name.trim() !== "" && !submitting; const canConfirm = name.trim() !== "" && !submitting;

View File

@@ -123,7 +123,7 @@ export function CreateFilterDialog({
.finally(() => { .finally(() => {
setSubmitting(false); setSubmitting(false);
}); });
}, [name, failregex, ignoreregex, submitting, onCreate]); }, [name, failregex, ignoreregex, submitting, onCreate, onCreateFilter]);
const canConfirm = name.trim() !== "" && !submitting; const canConfirm = name.trim() !== "" && !submitting;

View File

@@ -101,7 +101,7 @@ export function CreateJailDialog({
.finally(() => { .finally(() => {
setSubmitting(false); setSubmitting(false);
}); });
}, [name, submitting, onCreated]); }, [name, submitting, onCreated, onCreateJail]);
const canConfirm = name.trim() !== "" && !submitting; const canConfirm = name.trim() !== "" && !submitting;

View File

@@ -714,7 +714,7 @@ function InactiveJailDetail({
<strong>Errors:</strong> <strong>Errors:</strong>
<ul style={{ margin: `4px 0 0 0`, paddingLeft: "1.2em" }}> <ul style={{ margin: `4px 0 0 0`, paddingLeft: "1.2em" }}>
{blockingIssues.map((issue, idx) => ( {blockingIssues.map((issue, idx) => (
<li key={idx}><em>{issue.field}:</em> {issue.message}</li> <li key={`${issue.field}-${String(idx)}`}><em>{issue.field}:</em> {issue.message}</li>
))} ))}
</ul> </ul>
</MessageBarBody> </MessageBarBody>
@@ -726,7 +726,7 @@ function InactiveJailDetail({
<strong>Warnings:</strong> <strong>Warnings:</strong>
<ul style={{ margin: `4px 0 0 0`, paddingLeft: "1.2em" }}> <ul style={{ margin: `4px 0 0 0`, paddingLeft: "1.2em" }}>
{advisoryIssues.map((issue, idx) => ( {advisoryIssues.map((issue, idx) => (
<li key={idx}><em>{issue.field}:</em> {issue.message}</li> <li key={`${issue.field}-${String(idx)}`}><em>{issue.field}:</em> {issue.message}</li>
))} ))}
</ul> </ul>
</MessageBarBody> </MessageBarBody>
@@ -814,7 +814,6 @@ export function JailsTab({ initialJail }: JailsTabProps): React.JSX.Element {
refreshInactiveJails(); refreshInactiveJails();
}, [refresh, refreshInactiveJails]); }, [refresh, refreshInactiveJails]);
/** Unified list items: active jails first (from useJailConfigs), then inactive. */
const listItems = useMemo<Array<{ name: string; kind: "active" | "inactive" }>>(() => { const listItems = useMemo<Array<{ name: string; kind: "active" | "inactive" }>>(() => {
const activeItems = jails.map((j) => ({ name: j.name, kind: "active" as const })); const activeItems = jails.map((j) => ({ name: j.name, kind: "active" as const }));
const activeNames = new Set(jails.map((j) => j.name)); const activeNames = new Set(jails.map((j) => j.name));
@@ -913,7 +912,7 @@ export function JailsTab({ initialJail }: JailsTabProps): React.JSX.Element {
key={selectedActiveJail.name} key={selectedActiveJail.name}
jail={selectedActiveJail} jail={selectedActiveJail}
onSave={updateJail} onSave={updateJail}
onDeactivate={() => { handleDeactivate(selectedActiveJail.name); }} onDeactivate={() => { void handleDeactivate(selectedActiveJail.name); }}
/> />
) : selectedInactiveJail !== undefined ? ( ) : selectedInactiveJail !== undefined ? (
<InactiveJailDetail <InactiveJailDetail
@@ -922,10 +921,10 @@ export function JailsTab({ initialJail }: JailsTabProps): React.JSX.Element {
onActivate={() => { setActivateTarget(selectedInactiveJail); }} onActivate={() => { setActivateTarget(selectedInactiveJail); }}
onDeactivate={ onDeactivate={
selectedInactiveJail.has_local_override selectedInactiveJail.has_local_override
? (): void => { handleDeactivateInactive(selectedInactiveJail.name); } ? (): void => { void handleDeactivateInactive(selectedInactiveJail.name); }
: undefined : undefined
} }
onValidate={() => validateJailConfig(selectedInactiveJail.name)} onValidate={() => { void validateJailConfig(selectedInactiveJail.name); }}
/> />
) : null} ) : null}
</ConfigListDetail> </ConfigListDetail>

View File

@@ -134,7 +134,7 @@ export function RegexTesterTab(): React.JSX.Element {
</Text> </Text>
{result.groups.map((g, i) => ( {result.groups.map((g, i) => (
<Badge <Badge
key={i} key={`${g}-${String(i)}`}
appearance="tint" appearance="tint"
color="informative" color="informative"
style={{ marginLeft: tokens.spacingHorizontalXS }} style={{ marginLeft: tokens.spacingHorizontalXS }}
@@ -206,7 +206,7 @@ export function RegexTesterTab(): React.JSX.Element {
<div className={styles.previewArea}> <div className={styles.previewArea}>
{preview.lines.map((ln, idx) => ( {preview.lines.map((ln, idx) => (
<div <div
key={idx} key={ln.line + "-" + String(idx)}
className={`${styles.logLine} ${ln.matched ? styles.matched : styles.notMatched}`} className={`${styles.logLine} ${ln.matched ? styles.matched : styles.notMatched}`}
> >
{ln.line} {ln.line}

View File

@@ -15,7 +15,7 @@ export function CodeList({ items, empty }: CodeListProps): React.JSX.Element {
return ( return (
<div className={styles.codeList}> <div className={styles.codeList}>
{items.map((item, i) => ( {items.map((item, i) => (
<span key={i} className={styles.codeItem}> <span key={`${item}-${String(i)}`} className={styles.codeItem}>
{item} {item}
</span> </span>
))} ))}

522575
node:fs Normal file

File diff suppressed because it is too large Load Diff

435524
path Normal file

File diff suppressed because it is too large Load Diff

435524
vite Normal file

File diff suppressed because it is too large Load Diff