improve UX

This commit is contained in:
fuomag9
2026-03-04 22:26:11 +01:00
parent d959fdf836
commit 7ceeb84fc2
3 changed files with 68 additions and 24 deletions

View File

@@ -18,6 +18,7 @@ import {
Tooltip,
Typography,
} from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import CloseIcon from "@mui/icons-material/Close";
import BlockIcon from "@mui/icons-material/Block";
@@ -35,6 +36,7 @@ type Props = {
pagination: { total: number; page: number; perPage: number };
initialSearch: string;
globalExcluded: number[];
globalExcludedMessages: Record<number, string | null>;
globalWafEnabled: boolean;
hostWafMap: Record<string, number[]>;
};
@@ -222,10 +224,12 @@ function WafEventDrawer({
function GlobalSuppressedRules({
excluded,
messages,
wafEnabled,
onRemove,
}: {
excluded: number[];
messages: Record<number, string | null>;
wafEnabled: boolean;
onRemove: (ruleId: number) => void;
}) {
@@ -265,18 +269,46 @@ function GlobalSuppressedRules({
<Typography variant="caption">Open a WAF event and click "Suppress Globally" to add one.</Typography>
</Box>
) : (
<Stack direction="row" flexWrap="wrap" gap={1}>
<Stack spacing={1}>
{excluded.map((id) => (
<Chip
<Box
key={id}
label={id}
onDelete={() => handleRemove(id)}
deleteIcon={<DeleteIcon />}
disabled={pending}
sx={{ fontFamily: "monospace", fontWeight: 600 }}
color="error"
variant="outlined"
/>
sx={{
display: "flex",
alignItems: "center",
gap: 2,
px: 2,
py: 1.5,
borderRadius: 1.5,
border: "1px solid",
borderColor: "divider",
bgcolor: "action.hover",
}}
>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="body2" fontFamily="monospace" fontWeight={700} color="error.light">
Rule {id}
</Typography>
<Typography
variant="caption"
color={messages[id] ? "text.secondary" : "text.disabled"}
sx={{ display: "block", mt: 0.25 }}
>
{messages[id] ?? "No description available — rule has not triggered yet"}
</Typography>
</Box>
<Tooltip title="Remove suppression">
<IconButton
size="small"
onClick={() => handleRemove(id)}
disabled={pending}
color="error"
sx={{ flexShrink: 0 }}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
))}
</Stack>
)}
@@ -295,7 +327,7 @@ function GlobalSuppressedRules({
);
}
export default function WafEventsClient({ events, pagination, initialSearch, globalExcluded, globalWafEnabled, hostWafMap }: Props) {
export default function WafEventsClient({ events, pagination, initialSearch, globalExcluded, globalExcludedMessages, globalWafEnabled, hostWafMap }: Props) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
@@ -407,16 +439,7 @@ export default function WafEventsClient({ events, pagination, initialSearch, glo
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tab label="Events" />
<Tab
label={
<Stack direction="row" alignItems="center" spacing={0.75}>
<span>Suppressed Rules</span>
{localGlobalExcluded.length > 0 && (
<Chip label={localGlobalExcluded.length} size="small" color="error" sx={{ height: 18, fontSize: "0.65rem" }} />
)}
</Stack>
}
/>
<Tab label="Suppressed Rules" />
</Tabs>
{tab === 0 && (
@@ -453,6 +476,7 @@ export default function WafEventsClient({ events, pagination, initialSearch, glo
{tab === 1 && (
<GlobalSuppressedRules
excluded={localGlobalExcluded}
messages={globalExcludedMessages}
wafEnabled={globalWafEnabled}
onRemove={(ruleId) => setLocalGlobalExcluded((prev) => prev.filter((id) => id !== ruleId))}
/>

View File

@@ -1,7 +1,7 @@
export const dynamic = 'force-dynamic';
import WafEventsClient from "./WafEventsClient";
import { listWafEvents, countWafEvents } from "@/src/lib/models/waf-events";
import { listWafEvents, countWafEvents, getWafRuleMessages } from "@/src/lib/models/waf-events";
import { getWafSettings } from "@/src/lib/settings";
import { listProxyHosts } from "@/src/lib/models/proxy-hosts";
import { requireAdmin } from "@/src/lib/auth";
@@ -26,6 +26,9 @@ export default async function WafEventsPage({ searchParams }: PageProps) {
listProxyHosts(),
]);
const globalExcludedIds = globalWaf?.excluded_rule_ids ?? [];
const globalExcludedMessages = await getWafRuleMessages(globalExcludedIds);
const hostWafMap: Record<string, number[]> = {};
for (const host of hosts) {
const ids = host.waf?.excluded_rule_ids ?? [];
@@ -39,7 +42,8 @@ export default async function WafEventsPage({ searchParams }: PageProps) {
events={events}
pagination={{ total, page, perPage: PER_PAGE }}
initialSearch={search ?? ""}
globalExcluded={globalWaf?.excluded_rule_ids ?? []}
globalExcluded={globalExcludedIds}
globalExcludedMessages={globalExcludedMessages}
globalWafEnabled={globalWaf?.enabled ?? false}
hostWafMap={hostWafMap}
/>

View File

@@ -1,6 +1,6 @@
import db from "../db";
import { wafEvents } from "../db/schema";
import { desc, like, or, count, and, gte, lte, sql } from "drizzle-orm";
import { desc, like, or, count, and, gte, lte, sql, inArray } from "drizzle-orm";
export type WafEvent = {
id: number;
@@ -61,6 +61,22 @@ export async function getTopWafRules(from: number, to: number, limit = 10): Prom
.map((r) => ({ ruleId: r.ruleId, count: r.count, message: r.message ?? null }));
}
export async function getWafRuleMessages(ruleIds: number[]): Promise<Record<number, string | null>> {
if (ruleIds.length === 0) return {};
const rows = await db
.select({
ruleId: wafEvents.ruleId,
message: sql<string | null>`MAX(${wafEvents.ruleMessage})`,
})
.from(wafEvents)
.where(inArray(wafEvents.ruleId, ruleIds))
.groupBy(wafEvents.ruleId);
return Object.fromEntries(
rows.filter((r): r is typeof r & { ruleId: number } => r.ruleId != null)
.map((r) => [r.ruleId, r.message ?? null])
);
}
export async function listWafEvents(limit = 50, offset = 0, search?: string): Promise<WafEvent[]> {
const rows = await db
.select()