improve UX
This commit is contained in:
@@ -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))}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user