Files
wakey/ui/src/pages/AuditPage.tsx
T
lda Verified 134d3d0ef9 fmt + add and wire observation retention
what ever new thing could add stuff in CONTEXT.md and docs/adr/ hmmmmmmmmmmmmmmmmmmmmmm
2026-05-15 16:15:09 +07:00

148 lines
4.5 KiB
TypeScript

import { useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import type { AuditEvent } from "@/api";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
type Props = {
events: AuditEvent[];
onRefresh: () => Promise<void>;
};
export function AuditPage({ events, onRefresh }: Props) {
const [searchParams] = useSearchParams();
const initialEventType = searchParams.get("event_type") || "all";
const initialOutcome = searchParams.get("outcome") || "all";
const initialNeedle = searchParams.get("q") || "";
const [eventType, setEventType] = useState(initialEventType);
const [outcome, setOutcome] = useState(initialOutcome);
const [needle, setNeedle] = useState(initialNeedle);
const eventTypes = useMemo(
() => [
"all",
...Array.from(new Set(events.map((e) => e.event_type))).sort(),
],
[events],
);
const outcomes = useMemo(
() => ["all", ...Array.from(new Set(events.map((e) => e.outcome))).sort()],
[events],
);
const filtered = useMemo(() => {
const q = needle.trim().toLowerCase();
return events.filter((e) => {
if (eventType !== "all" && e.event_type !== eventType) return false;
if (outcome !== "all" && e.outcome !== outcome) return false;
if (!q) return true;
return (
e.message.toLowerCase().includes(q) ||
e.event_type.toLowerCase().includes(q) ||
e.outcome.toLowerCase().includes(q) ||
e.request_id?.toLowerCase().includes(q) || // why does the one flow from devicespage use this
(e.actor_id || "").toLowerCase().includes(q) ||
(e.agent_id || "").toLowerCase().includes(q)
);
});
}, [events, eventType, outcome, needle]);
return (
<Card size="sm">
<CardHeader className="flex items-center justify-between gap-2">
<CardTitle>Recent Audit Events</CardTitle>
<Button size="sm" variant="outline" onClick={() => void onRefresh()}>
Refresh
</Button>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid gap-2 sm:grid-cols-3">
<label
htmlFor="audit-event-type"
className="grid gap-1 text-sm text-muted-foreground"
>
Event type
<Select
value={eventType}
onValueChange={(value) => {
if (value) setEventType(value);
}}
>
<SelectTrigger id="audit-event-type" className="w-full">
<SelectValue placeholder="Event type" />
</SelectTrigger>
<SelectContent>
{eventTypes.map((v) => (
<SelectItem key={v} value={v}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
</label>
<label
htmlFor="audit-outcome"
className="grid gap-1 text-sm text-muted-foreground"
>
Outcome
<Select
value={outcome}
onValueChange={(value) => {
if (value) setOutcome(value);
}}
>
<SelectTrigger id="audit-outcome" className="w-full">
<SelectValue placeholder="Outcome" />
</SelectTrigger>
<SelectContent>
{outcomes.map((v) => (
<SelectItem key={v} value={v}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
</label>
<label
htmlFor="audit-search"
className="grid gap-1 text-sm text-muted-foreground"
>
Search
<Input
id="audit-search"
value={needle}
onChange={(e) => setNeedle(e.target.value)}
placeholder="message, agent, actor"
/>
</label>
</div>
<p className="text-sm text-muted-foreground">
Showing {filtered.length} of {events.length}
</p>
<Textarea
className="min-h-90 font-mono text-xs"
readOnly
value={JSON.stringify(filtered, null, 2)}
/>
</CardContent>
</Card>
);
}