This commit is contained in:
lda
2026-04-14 03:43:36 +07:00 Unverified
parent a2b0a8cd1d
commit 79dbb52e0d
4 changed files with 290 additions and 192 deletions
+133 -75
View File
@@ -1,6 +1,17 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import type { Alert, AlertTransition } from "@/api"; import type { Alert, AlertTransition } 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 = { type Props = {
alerts: Alert[]; alerts: Alert[];
@@ -51,83 +62,130 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
}, [transitions, transitionQ]); }, [transitions, transitionQ]);
return ( return (
<section className="two-col"> <section className="grid gap-3 lg:grid-cols-2">
<div className="card"> <Card>
<div className="row-head"> <CardHeader className="flex items-center justify-between gap-2">
<h2>Active Alerts</h2> <CardTitle>Active Alerts</CardTitle>
<button onClick={() => void onRefresh()}>Refresh</button> <Button size="sm" variant="outline" onClick={() => void onRefresh()}>
</div> Refresh
<div className="grid-3 compact"> </Button>
<label> </CardHeader>
Severity <CardContent className="space-y-3">
<select <div className="grid gap-2 sm:grid-cols-3">
value={severity} <label className="grid gap-1 text-sm text-muted-foreground">
onChange={(e) => setSeverity(e.target.value)} Severity
> <Select
{severities.map((v) => ( value={severity}
<option key={v} value={v}> onValueChange={(value) => {
{v} if (value) setSeverity(value);
</option> }}
))} >
</select> <SelectTrigger className="w-full">
</label> <SelectValue placeholder="Severity" />
<label> </SelectTrigger>
Status <SelectContent>
<select value={status} onChange={(e) => setStatus(e.target.value)}> {severities.map((v) => (
{statuses.map((v) => ( <SelectItem key={v} value={v}>
<option key={v} value={v}> {v}
{v} </SelectItem>
</option> ))}
))} </SelectContent>
</select> </Select>
</label> </label>
<label>
Kind
<select value={kind} onChange={(e) => setKind(e.target.value)}>
{kinds.map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</label>
</div>
<p className="muted">
Showing {filteredAlerts.length} of {alerts.length}
</p>
<div className="list">
{filteredAlerts.map((alert) => (
<div className="row plain" key={alert.alert_id}>
<span>
{alert.kind}
{alert.agent_id ? ` (${alert.agent_id})` : ""}
</span>
<span>{alert.severity}</span>
</div>
))}
{!filteredAlerts.length && (
<div className="empty">No active alerts</div>
)}
</div>
</div>
<div className="card"> <label className="grid gap-1 text-sm text-muted-foreground">
<div className="row-head"> Status
<h2>Transition History</h2> <Select
<span className="muted">{filteredTransitions.length} shown</span> value={status}
</div> onValueChange={(value) => {
<label> if (value) setStatus(value);
Search }}
<input >
value={transitionQ} <SelectTrigger className="w-full">
onChange={(e) => setTransitionQ(e.target.value)} <SelectValue placeholder="Status" />
placeholder="kind, status, message, agent" </SelectTrigger>
<SelectContent>
{statuses.map((v) => (
<SelectItem key={v} value={v}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
</label>
<label className="grid gap-1 text-sm text-muted-foreground">
Kind
<Select
value={kind}
onValueChange={(value) => {
if (value) setKind(value);
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Kind" />
</SelectTrigger>
<SelectContent>
{kinds.map((v) => (
<SelectItem key={v} value={v}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
</label>
</div>
<p className="text-sm text-muted-foreground">
Showing {filteredAlerts.length} of {alerts.length}
</p>
<div className="grid gap-2">
{filteredAlerts.map((alert) => (
<div
className="flex items-center justify-between gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm"
key={alert.alert_id}
>
<span>
{alert.kind}
{alert.agent_id ? ` (${alert.agent_id})` : ""}
</span>
<span className="text-muted-foreground">{alert.severity}</span>
</div>
))}
{!filteredAlerts.length && (
<div className="px-1 text-sm text-muted-foreground">
No active alerts
</div>
)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex items-center justify-between gap-2">
<CardTitle>Transition History</CardTitle>
<span className="text-sm text-muted-foreground">
{filteredTransitions.length} shown
</span>
</CardHeader>
<CardContent className="space-y-2">
<label className="grid gap-1 text-sm text-muted-foreground">
Search
<Input
value={transitionQ}
onChange={(e) => setTransitionQ(e.target.value)}
placeholder="kind, status, message, agent"
/>
</label>
<Textarea
className="min-h-[230px] font-mono text-xs"
readOnly
value={JSON.stringify(filteredTransitions, null, 2)}
/> />
</label> </CardContent>
<pre className="output small"> </Card>
{JSON.stringify(filteredTransitions, null, 2)}
</pre>
</div>
</section> </section>
); );
} }
+84 -43
View File
@@ -2,6 +2,17 @@ import { useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import type { AuditEvent } from "@/api"; 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 = { type Props = {
events: AuditEvent[]; events: AuditEvent[];
@@ -49,48 +60,78 @@ export function AuditPage({ events, onRefresh }: Props) {
}, [events, eventType, outcome, needle]); }, [events, eventType, outcome, needle]);
return ( return (
<section className="card span-full"> <Card size="sm">
<div className="row-head"> <CardHeader className="flex items-center justify-between gap-2">
<h2>Recent Audit Events</h2> <CardTitle>Recent Audit Events</CardTitle>
<button onClick={() => void onRefresh()}>Refresh</button> <Button size="sm" variant="outline" onClick={() => void onRefresh()}>
</div> Refresh
<div className="grid-3 compact"> </Button>
<label> </CardHeader>
Event type
<select <CardContent className="space-y-3">
value={eventType} <div className="grid gap-2 sm:grid-cols-3">
onChange={(e) => setEventType(e.target.value)} <label className="grid gap-1 text-sm text-muted-foreground">
> Event type
{eventTypes.map((v) => ( <Select
<option key={v} value={v}> value={eventType}
{v} onValueChange={(value) => {
</option> if (value) setEventType(value);
))} }}
</select> >
</label> <SelectTrigger className="w-full">
<label> <SelectValue placeholder="Event type" />
Outcome </SelectTrigger>
<select value={outcome} onChange={(e) => setOutcome(e.target.value)}> <SelectContent>
{outcomes.map((v) => ( {eventTypes.map((v) => (
<option key={v} value={v}> <SelectItem key={v} value={v}>
{v} {v}
</option> </SelectItem>
))} ))}
</select> </SelectContent>
</label> </Select>
<label> </label>
Search
<input <label className="grid gap-1 text-sm text-muted-foreground">
value={needle} Outcome
onChange={(e) => setNeedle(e.target.value)} <Select
placeholder="message, agent, actor" value={outcome}
/> onValueChange={(value) => {
</label> if (value) setOutcome(value);
</div> }}
<p className="muted"> >
Showing {filtered.length} of {events.length} <SelectTrigger className="w-full">
</p> <SelectValue placeholder="Outcome" />
<pre className="output">{JSON.stringify(filtered, null, 2)}</pre> </SelectTrigger>
</section> <SelectContent>
{outcomes.map((v) => (
<SelectItem key={v} value={v}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
</label>
<label className="grid gap-1 text-sm text-muted-foreground">
Search
<Input
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-[360px] font-mono text-xs"
readOnly
value={JSON.stringify(filtered, null, 2)}
/>
</CardContent>
</Card>
); );
} }
+3 -3
View File
@@ -9,7 +9,7 @@
.quick-wake label { .quick-wake label {
display: grid; display: grid;
gap: 0.35rem; gap: 0.35rem;
color: var(--muted); color: var(--muted-foreground);
font-size: 0.9rem; font-size: 0.9rem;
} }
@@ -32,7 +32,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.35rem; gap: 0.35rem;
color: var(--muted); color: var(--muted-foreground);
font-size: 0.88rem; font-size: 0.88rem;
} }
@@ -115,7 +115,7 @@
content: attr(data-label); content: attr(data-label);
display: block; display: block;
font-size: 0.72rem; font-size: 0.72rem;
color: var(--muted); color: var(--muted-foreground);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.03em; letter-spacing: 0.03em;
margin-bottom: 0.1rem; margin-bottom: 0.1rem;
+70 -71
View File
@@ -6,48 +6,48 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
:root { :root {
--bg: #f5f8ff; --bg: #f5f8f2;
--bg-accent: #e8f0ff; --bg-accent: #e7efe4;
--card: oklch(1 0 0); --card: oklch(1 0 0);
--line: #c9d3e4; --line: #ced7ce;
--text: #1b2a42; --text: #1c2620;
--muted: oklch(0.967 0.001 286.375); --muted: oklch(0.96 0.004 145);
--surface: #ffffff; --surface: #ffffff;
--surface-muted: #f4f7fc; --surface-muted: #f3f7f2;
--surface-strong: #eaf0fb; --surface-strong: #e8f0e7;
--card-bg-a: rgba(255, 255, 255, 0.98); --card-bg-a: rgba(255, 255, 255, 0.98);
--card-bg-b: rgba(245, 249, 255, 0.9); --card-bg-b: rgba(243, 248, 242, 0.9);
--tab-active-border: #7d96bb; --tab-active-border: #6a9a7f;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823); --foreground: oklch(0.23 0.02 155);
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.23 0.02 155);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823); --popover-foreground: oklch(0.23 0.02 155);
--primary: oklch(0.457 0.24 277.023); --primary: oklch(0.56 0.12 156);
--primary-foreground: oklch(0.962 0.018 272.314); --primary-foreground: oklch(0.985 0.003 95);
--secondary: oklch(0.967 0.001 286.375); --secondary: oklch(0.965 0.01 155);
--secondary-foreground: oklch(0.21 0.006 285.885); --secondary-foreground: oklch(0.3 0.035 156);
--muted-foreground: oklch(0.552 0.016 285.938); --muted-foreground: oklch(0.5 0.02 155);
--accent: oklch(0.967 0.001 286.375); --accent: oklch(0.945 0.015 156);
--accent-foreground: oklch(0.21 0.006 285.885); --accent-foreground: oklch(0.3 0.035 156);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32); --border: oklch(0.9 0.01 155);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.9 0.01 155);
--ring: oklch(0.705 0.015 286.067); --ring: oklch(0.62 0.08 156);
--chart-1: oklch(0.785 0.115 274.713); --chart-1: oklch(0.62 0.12 156);
--chart-2: oklch(0.585 0.233 277.117); --chart-2: oklch(0.56 0.11 188);
--chart-3: oklch(0.511 0.262 276.966); --chart-3: oklch(0.67 0.12 120);
--chart-4: oklch(0.457 0.24 277.023); --chart-4: oklch(0.72 0.15 85);
--chart-5: oklch(0.398 0.195 277.366); --chart-5: oklch(0.58 0.18 35);
--radius: 0.625rem; --radius: 0.625rem;
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-foreground: oklch(0.23 0.02 155);
--sidebar-primary: oklch(0.511 0.262 276.966); --sidebar-primary: oklch(0.56 0.12 156);
--sidebar-primary-foreground: oklch(0.962 0.018 272.314); --sidebar-primary-foreground: oklch(0.985 0.003 95);
--sidebar-accent: oklch(0.967 0.001 286.375); --sidebar-accent: oklch(0.945 0.015 156);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.3 0.035 156);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.9 0.01 155);
--sidebar-ring: oklch(0.705 0.015 286.067); --sidebar-ring: oklch(0.62 0.08 156);
} }
* { * {
@@ -75,7 +75,7 @@ body {
} }
.topbar p { .topbar p {
margin: 0.35rem 0 0; margin: 0.35rem 0 0;
color: var(--muted); color: var(--muted-foreground);
} }
.tabs { .tabs {
@@ -90,7 +90,7 @@ body {
border-radius: 999px; border-radius: 999px;
padding: 0.35rem 0.65rem; padding: 0.35rem 0.65rem;
text-decoration: none; text-decoration: none;
color: var(--muted); color: var(--muted-foreground);
background: var(--surface-muted); background: var(--surface-muted);
} }
@@ -132,7 +132,7 @@ body {
} }
.stat h3 { .stat h3 {
margin: 0; margin: 0;
color: var(--muted); color: var(--muted-foreground);
font-weight: 500; font-weight: 500;
} }
.stat strong { .stat strong {
@@ -169,7 +169,7 @@ body {
gap: 0.5rem; gap: 0.5rem;
} }
.muted { .muted {
color: var(--muted); color: var(--muted-foreground);
} }
button, button,
@@ -200,7 +200,7 @@ select {
.form label { .form label {
display: grid; display: grid;
gap: 0.35rem; gap: 0.35rem;
color: var(--muted); color: var(--muted-foreground);
font-size: 0.9rem; font-size: 0.9rem;
} }
@@ -217,7 +217,7 @@ select {
.grid-3 label { .grid-3 label {
display: grid; display: grid;
gap: 0.35rem; gap: 0.35rem;
color: var(--muted); color: var(--muted-foreground);
font-size: 0.9rem; font-size: 0.9rem;
} }
@@ -251,7 +251,7 @@ select {
@media (max-width: 720px) { @media (max-width: 720px) {
} }
.empty { .empty {
color: var(--muted); color: var(--muted-foreground);
padding: 0.35rem 0.2rem; padding: 0.35rem 0.2rem;
} }
@@ -320,48 +320,47 @@ select {
} }
.dark { .dark {
--bg: #0a111f; --bg: #0f1712;
--bg-accent: #1a2741; --bg-accent: #1a271f;
--line: #2c3b57; --line: #2d3d33;
--text: #e7edf7; --text: #e7edf7;
--muted: oklch(0.274 0.006 286.033); --muted: oklch(0.25 0.01 156);
--surface: #0b1528; --surface: #122119;
--surface-muted: #0d1a30; --surface-muted: #15271d;
--surface-strong: #091429; --surface-strong: #1a2d21;
--card-bg-a: rgba(17, 26, 45, 0.92); --card-bg-a: rgba(20, 33, 25, 0.92);
--card-bg-b: rgba(17, 26, 45, 0.72); --card-bg-b: rgba(20, 33, 25, 0.72);
--tab-active-border: #6b84ad; --tab-active-border: #63a47f;
--background: oklch(0.141 0.005 285.823); --background: oklch(0.19 0.02 156);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885); --card: oklch(0.24 0.015 156);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885); --popover: oklch(0.24 0.015 156);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.398 0.195 277.366); --primary: oklch(0.68 0.11 156);
--primary-foreground: oklch(0.962 0.018 272.314); --primary-foreground: oklch(0.17 0.02 156);
--secondary: oklch(0.274 0.006 286.033); --secondary: oklch(0.28 0.02 156);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.263 0.024 320.12); --muted-foreground: oklch(0.75 0.02 156);
--muted-foreground: oklch(0.705 0.015 286.067); --accent: oklch(0.3 0.02 156);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938); --ring: oklch(0.65 0.08 156);
--chart-1: oklch(0.785 0.115 274.713); --chart-1: oklch(0.68 0.11 156);
--chart-2: oklch(0.585 0.233 277.117); --chart-2: oklch(0.63 0.11 188);
--chart-3: oklch(0.511 0.262 276.966); --chart-3: oklch(0.73 0.12 120);
--chart-4: oklch(0.457 0.24 277.023); --chart-4: oklch(0.78 0.14 85);
--chart-5: oklch(0.398 0.195 277.366); --chart-5: oklch(0.67 0.16 35);
--sidebar: oklch(0.21 0.006 285.885); --sidebar: oklch(0.24 0.015 156);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.585 0.233 277.117); --sidebar-primary: oklch(0.68 0.11 156);
--sidebar-primary-foreground: oklch(0.962 0.018 272.314); --sidebar-primary-foreground: oklch(0.17 0.02 156);
--sidebar-accent: oklch(0.274 0.006 286.033); --sidebar-accent: oklch(0.3 0.02 156);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938); --sidebar-ring: oklch(0.65 0.08 156);
} }
@layer base { @layer base {