fix(logs-ui): stabilize the dashboard log layout

This commit is contained in:
Tam Nhu Tran
2026-04-08 15:57:15 -04:00
parent 42481c0bb9
commit 6e39f338ce
8 changed files with 1209 additions and 517 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ export function LogLevelBadge({ level, className }: { level: LogsLevel; classNam
return (
<span
className={cn(
'inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-semibold uppercase tracking-[0.18em]',
'inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium uppercase tracking-[0.12em]',
LEVEL_STYLES[level],
className
)}
+198 -115
View File
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { Save, Settings2 } from 'lucide-react';
import { Save, Settings2, ShieldAlert, RotateCcw, Activity } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
@@ -13,6 +12,7 @@ import {
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import type { LogsConfig, UpdateLogsConfigPayload } from '@/lib/api-client';
import { cn } from '@/lib/utils';
function parseInteger(value: string, fallback: number) {
const parsed = Number.parseInt(value, 10);
@@ -41,40 +41,72 @@ export function LogsConfigCard({
const isDirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(config), [config, draft]);
return (
<Card className="gap-4 border-border/70 bg-card/85 shadow-sm">
<CardHeader className="space-y-2 border-b pb-4">
<div className="flex items-center gap-2">
<Settings2 className="h-4 w-4 text-muted-foreground" />
<CardTitle>Logging policy</CardTitle>
<div className="group relative overflow-hidden rounded-2xl border-2 border-border/60 bg-card/40 p-1 shadow-lg transition-all hover:border-border">
<div className="flex items-center justify-between border-b border-border bg-muted/30 px-5 py-3">
<div className="flex items-center gap-3">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary/5 border border-primary/20">
<Settings2 className="h-3.5 w-3.5 text-primary" />
</div>
<div className="space-y-0.5">
<h3 className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground">
Logging Policy
</h3>
<p className="text-[10px] font-medium uppercase tracking-[0.1em] text-foreground/45">
Retention and privacy
</p>
</div>
</div>
<CardDescription>
Keep retention, file rotation, redaction, and live tail depth aligned with the host.
</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
<div className="rounded-2xl border border-border/70 bg-background/70 p-4 text-sm">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-[11px] font-medium uppercase tracking-[0.22em] text-muted-foreground">
Current posture
</p>
<p className="text-muted-foreground">
{config.enabled ? 'Logging is enabled' : 'Logging is disabled'} at{' '}
{config.level.toUpperCase()} and above.
</p>
<div
className={cn(
'h-1.5 w-1.5 rounded-full',
config.enabled ? 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]' : 'bg-zinc-500'
)}
/>
</div>
<div className="space-y-6 p-5">
<div className="grid grid-cols-2 gap-2">
<div className="rounded-xl border border-border/40 bg-background/50 p-3 flex flex-col gap-1">
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground/70">
Active Status
</span>
<div className="flex items-center gap-2">
<span
className={cn(
'text-[11px] font-semibold uppercase tracking-[0.1em]',
config.enabled ? 'text-emerald-500' : 'text-zinc-500'
)}
>
{config.enabled ? 'Live' : 'Off'}
</span>
</div>
<span className="rounded-full border px-2 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
{config.redact ? 'Redacted' : 'Plain'}
</div>
<div className="rounded-xl border border-border/40 bg-background/50 p-3 flex flex-col gap-1">
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground/70">
Redaction
</span>
<span
className={cn(
'text-[11px] font-semibold uppercase tracking-[0.1em]',
config.redact ? 'text-primary' : 'text-muted-foreground/40'
)}
>
{config.redact ? 'Enforced' : 'Plain'}
</span>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/70 bg-background/60 p-3">
<div className="space-y-1">
<Label htmlFor="logs-enabled">Enabled</Label>
<p className="text-xs text-muted-foreground">
Turn the unified logging pipeline on or off.
<div className="space-y-2">
<div className="flex items-center justify-between gap-4 rounded-xl border border-border/40 bg-background/20 px-4 py-3 transition-colors hover:bg-background/40">
<div className="space-y-0.5">
<Label
htmlFor="logs-enabled"
className="text-[12px] font-semibold uppercase tracking-[0.12em]"
>
Pipeline
</Label>
<p className="text-[10px] font-medium uppercase tracking-[0.08em] text-muted-foreground/55">
Enable structured logging
</p>
</div>
<Switch
@@ -83,14 +115,20 @@ export function LogsConfigCard({
onCheckedChange={(checked) =>
setDraft((current) => ({ ...current, enabled: checked }))
}
className="data-[state=checked]:bg-primary"
/>
</div>
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/70 bg-background/60 p-3">
<div className="space-y-1">
<Label htmlFor="logs-redact">Redact payloads</Label>
<p className="text-xs text-muted-foreground">
Mask sensitive content before it lands in the log archive.
<div className="flex items-center justify-between gap-4 rounded-xl border border-border/40 bg-background/20 px-4 py-3 transition-colors hover:bg-background/40">
<div className="space-y-0.5">
<Label
htmlFor="logs-redact"
className="text-[12px] font-semibold uppercase tracking-[0.12em]"
>
Masking
</Label>
<p className="text-[10px] font-medium uppercase tracking-[0.08em] text-muted-foreground/55">
Sanitize payload data
</p>
</div>
<Switch
@@ -99,101 +137,146 @@ export function LogsConfigCard({
onCheckedChange={(checked) =>
setDraft((current) => ({ ...current, redact: checked }))
}
className="data-[state=checked]:bg-primary"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="logs-config-level">Minimum level</Label>
<Select
value={draft.level}
onValueChange={(value) =>
setDraft((current) => ({ ...current, level: value as LogsConfig['level'] }))
}
>
<SelectTrigger
id="logs-config-level"
aria-label="Minimum log level"
className="border-border/70 bg-background/70"
<div className="space-y-4 pt-2">
<div className="space-y-2">
<div className="flex items-center gap-2 px-1">
<ShieldAlert className="h-3 w-3 text-primary/40" />
<Label
htmlFor="logs-config-level"
className="text-[10px] font-semibold uppercase tracking-[0.12em] text-foreground/70"
>
Minimum Operational Threshold
</Label>
</div>
<Select
value={draft.level}
onValueChange={(value) =>
setDraft((current) => ({ ...current, level: value as LogsConfig['level'] }))
}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="error">Error only</SelectItem>
<SelectItem value="warn">Warn and above</SelectItem>
<SelectItem value="info">Info and above</SelectItem>
<SelectItem value="debug">Debug and above</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="logs-rotate-mb">Rotate size (MB)</Label>
<Input
id="logs-rotate-mb"
type="number"
min={1}
className="border-border/70 bg-background/70"
value={draft.rotate_mb}
onChange={(event) =>
setDraft((current) => ({
...current,
rotate_mb: parseInteger(event.target.value, current.rotate_mb),
}))
}
/>
<SelectTrigger
id="logs-config-level"
className="h-10 rounded-xl border-2 border-border/40 bg-background/50 text-[12px] font-semibold uppercase tracking-[0.1em] focus:ring-0"
>
<SelectValue />
</SelectTrigger>
<SelectContent className="border-2 border-border bg-card">
<SelectItem
value="error"
className="text-[11px] font-semibold uppercase tracking-[0.1em]"
>
Error Only
</SelectItem>
<SelectItem
value="warn"
className="text-[11px] font-semibold uppercase tracking-[0.1em]"
>
Warn + Above
</SelectItem>
<SelectItem
value="info"
className="text-[11px] font-semibold uppercase tracking-[0.1em]"
>
Info + Above
</SelectItem>
<SelectItem
value="debug"
className="text-[11px] font-semibold uppercase tracking-[0.1em]"
>
Full Debug
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="logs-retain-days">Retention (days)</Label>
<Input
id="logs-retain-days"
type="number"
min={1}
className="border-border/70 bg-background/70"
value={draft.retain_days}
onChange={(event) =>
setDraft((current) => ({
...current,
retain_days: parseInteger(event.target.value, current.retain_days),
}))
}
/>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label
htmlFor="logs-rotate-mb"
className="px-1 text-[10px] font-semibold uppercase tracking-[0.1em] text-foreground/50"
>
Rotation (MB)
</Label>
<Input
id="logs-rotate-mb"
type="number"
min={1}
className="h-10 rounded-xl border-2 border-border/40 bg-background/50 font-mono text-[12px] font-medium focus-visible:ring-0"
value={draft.rotate_mb}
onChange={(event) =>
setDraft((current) => ({
...current,
rotate_mb: parseInteger(event.target.value, current.rotate_mb),
}))
}
/>
</div>
<div className="space-y-2">
<Label
htmlFor="logs-retain-days"
className="px-1 text-[10px] font-semibold uppercase tracking-[0.1em] text-foreground/50"
>
Retain (Days)
</Label>
<Input
id="logs-retain-days"
type="number"
min={1}
className="h-10 rounded-xl border-2 border-border/40 bg-background/50 font-mono text-[12px] font-medium focus-visible:ring-0"
value={draft.retain_days}
onChange={(event) =>
setDraft((current) => ({
...current,
retain_days: parseInteger(event.target.value, current.retain_days),
}))
}
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="logs-buffer-size">Live buffer size</Label>
<Input
id="logs-buffer-size"
type="number"
min={1}
className="border-border/70 bg-background/70"
value={draft.live_buffer_size}
onChange={(event) =>
setDraft((current) => ({
...current,
live_buffer_size: parseInteger(event.target.value, current.live_buffer_size),
}))
}
/>
</div>
<div className="flex flex-wrap items-center gap-3 border-t border-border/70 pt-4">
<Button onClick={() => onSave(draft)} disabled={!isDirty || isPending} className="gap-2">
<Save className="h-4 w-4" />
Save policy
<div className="flex flex-col gap-2 pt-4 border-t border-border">
<Button
onClick={() => onSave(draft)}
disabled={!isDirty || isPending}
className="h-10 w-full gap-2 rounded-xl bg-primary text-[11px] font-semibold uppercase tracking-[0.14em] shadow-lg shadow-primary/20 transition-all hover:scale-[1.02] active:scale-[0.98]"
>
<Save className="h-3.5 w-3.5" />
Commit Changes
</Button>
<Button
variant="outline"
variant="ghost"
onClick={() => setDraft(config)}
disabled={!isDirty || isPending}
className="h-9 gap-2 text-[10px] font-medium uppercase tracking-[0.12em] text-foreground/45 hover:text-foreground"
>
Reset
<RotateCcw className="h-3 w-3" />
Rollback Draft
</Button>
{isDirty ? <span className="text-sm text-muted-foreground">Unsaved changes</span> : null}
</div>
</CardContent>
</Card>
</div>
<div className="flex items-center justify-between bg-muted/20 px-5 py-2">
<div className="flex items-center gap-1.5">
<Activity className="h-2.5 w-2.5 text-primary/40" />
<span className="text-[9px] font-medium uppercase tracking-[0.12em] text-foreground/30">
Operational Logic v3.4
</span>
</div>
{isDirty && (
<div className="flex items-center gap-1">
<div className="h-1 w-1 rounded-full bg-amber-500 animate-pulse" />
<span className="text-[9px] font-medium uppercase tracking-[0.12em] text-amber-500/70">
Pending
</span>
</div>
)}
</div>
</div>
);
}
+238 -87
View File
@@ -1,16 +1,46 @@
import { FileJson, Info } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
FileJson,
Info,
ShieldCheck,
Terminal,
Fingerprint,
Database,
Cpu,
Activity,
type LucideIcon,
} from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import type { LogsEntry } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import { LogLevelBadge } from './log-level-badge';
import { formatJson, formatLogTimestamp } from './utils';
import { formatJson } from './utils';
function MetaRow({ label, value }: { label: string; value: string | number }) {
function MetaRow({
label,
value,
icon: Icon,
}: {
label: string;
value: string | number;
icon?: LucideIcon;
}) {
return (
<div className="rounded-lg border bg-background/70 p-3">
<p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">{label}</p>
<p className="mt-1 break-all text-sm font-medium">{value}</p>
<div className="group relative flex flex-col gap-1.5 rounded-xl border border-border/40 bg-background/40 p-3 transition-all hover:bg-background/80 hover:shadow-lg hover:shadow-black/5">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
{Icon && (
<Icon className="h-3 w-3 text-primary/40 group-hover:text-primary transition-colors" />
)}
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground/70 transition-colors group-hover:text-primary/60">
{label}
</p>
</div>
<div className="h-1 w-1 rounded-full bg-border/40 group-hover:bg-primary/40 transition-colors" />
</div>
<p className="truncate font-mono text-[13px] font-medium tracking-tight text-foreground/85 transition-colors group-hover:text-foreground">
{value}
</p>
</div>
);
}
@@ -24,92 +54,213 @@ export function LogsDetailPanel({
}) {
if (!entry) {
return (
<Card className="gap-4 border-border/70 bg-card/85 shadow-sm">
<CardHeader>
<CardTitle>Inspector</CardTitle>
<CardDescription>
Select an activity row to inspect its operational metadata.
</CardDescription>
</CardHeader>
<CardContent className="flex min-h-[42rem] items-center justify-center text-sm text-muted-foreground">
Nothing selected yet.
</CardContent>
</Card>
<div className="flex h-full flex-col items-center justify-center p-8 text-center animate-in fade-in duration-1000">
<div className="relative mb-8">
<div className="absolute inset-0 animate-ping rounded-full bg-primary/5 p-12" />
<div className="relative rounded-full border-2 border-dashed border-border/40 p-10 bg-muted/5">
<Terminal className="h-10 w-10 text-muted-foreground/20" />
</div>
</div>
<div className="max-w-xs space-y-3">
<h3 className="text-[15px] font-semibold uppercase tracking-[0.14em] text-foreground/65">
Inspector Standby
</h3>
<p className="text-[13px] leading-relaxed text-muted-foreground/55 font-medium">
Select a telemetry node from the active data queue to perform deep analysis of its
operational context.
</p>
</div>
</div>
);
}
return (
<Card className="gap-4 border-border/70 bg-card/85 shadow-sm">
<CardHeader className="space-y-3 border-b pb-4">
<div className="flex flex-wrap items-center gap-2">
<LogLevelBadge level={entry.level} />
<span className="rounded-full border px-2 py-0.5 text-xs font-medium text-muted-foreground">
{sourceLabel ?? entry.source}
</span>
<span className="text-xs text-muted-foreground">
{formatLogTimestamp(entry.timestamp)}
</span>
</div>
<div className="space-y-1">
<CardTitle className="text-2xl tracking-tight">{entry.event}</CardTitle>
<CardDescription className="text-sm leading-6 text-foreground/85">
{entry.message}
</CardDescription>
</div>
</CardHeader>
<CardContent>
<Tabs defaultValue="details" className="space-y-4">
<TabsList>
<TabsTrigger value="details" className="gap-2">
<Info className="h-4 w-4" />
Details
</TabsTrigger>
<TabsTrigger value="raw" className="gap-2">
<FileJson className="h-4 w-4" />
Raw context
</TabsTrigger>
</TabsList>
<div className="flex h-full flex-col bg-card/30 backdrop-blur-sm animate-in fade-in slide-in-from-right-4 duration-500">
{/* Tactical Inspector Header */}
<div className="relative shrink-0 border-b border-border bg-card/60 p-6 shadow-sm overflow-hidden">
{/* Pattern Overlay */}
<div className="absolute inset-0 opacity-[0.03] pointer-events-none [background-image:radial-gradient(circle_at_center,var(--primary)_1px,transparent_0)] [background-size:16px_16px]" />
<TabsContent value="details" className="space-y-4">
<div className="grid gap-3 md:grid-cols-2">
<MetaRow label="Entry ID" value={entry.id} />
<MetaRow label="Source" value={entry.source} />
<MetaRow label="Process ID" value={entry.processId ?? 'Unavailable'} />
<MetaRow label="Run ID" value={entry.runId ?? 'Unavailable'} />
<div className="relative space-y-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<LogLevelBadge level={entry.level} className="h-5 px-3 shadow-lg shadow-black/5" />
<div className="h-4 w-px bg-border/60" />
<div className="flex items-center gap-2 rounded-full border border-border bg-background/50 px-3 py-1 shadow-inner">
<Database className="h-3 w-3 text-primary/60" />
<span className="text-[11px] font-semibold uppercase tracking-[0.12em] text-foreground/75">
{sourceLabel ?? entry.source}
</span>
</div>
</div>
<div className="rounded-2xl border border-border/70 bg-background/70 p-4">
<p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">
Context summary
</p>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
This entry belongs to{' '}
<span className="font-medium text-foreground">{sourceLabel ?? entry.source}</span>{' '}
and was emitted at the{' '}
<span className="font-medium text-foreground">{entry.level}</span> threshold. Use
the raw tab for the exact structured payload.
</p>
</div>
</TabsContent>
<TabsContent value="raw">
<ScrollArea className="h-[24rem] rounded-xl border bg-zinc-950 p-4 text-xs text-zinc-100">
<pre className="whitespace-pre-wrap break-words font-mono">
{formatJson({
id: entry.id,
timestamp: entry.timestamp,
level: entry.level,
source: entry.source,
event: entry.event,
message: entry.message,
processId: entry.processId,
runId: entry.runId,
context: entry.context ?? {},
<div className="flex items-center gap-2">
<Activity className="h-3 w-3 animate-pulse text-emerald-500" />
<span className="font-mono text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground/45">
{new Date(entry.timestamp).toLocaleTimeString(undefined, {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
</pre>
</ScrollArea>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="h-1 w-4 rounded-full bg-primary/40" />
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-primary/65">
Event
</p>
</div>
<h2 className="text-[24px] font-semibold tracking-tight text-foreground leading-tight break-words">
{entry.event}
</h2>
<div className="rounded-xl border-l-4 border-primary/20 bg-muted/20 p-4 shadow-inner">
<p className="text-[14px] font-medium leading-relaxed text-foreground/85 selection:bg-primary/20">
{entry.message}
</p>
</div>
</div>
</div>
</div>
<ScrollArea className="flex-1">
<div className="p-6">
<Tabs defaultValue="details" className="space-y-8">
<TabsList className="grid h-auto w-full grid-cols-2 gap-1 rounded-xl border border-border/60 bg-muted/40 p-1">
<TabsTrigger
value="details"
className="min-w-0 gap-2 rounded-lg px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.1em] transition-all data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-sm"
>
<Info className="h-3.5 w-3.5" />
Details
</TabsTrigger>
<TabsTrigger
value="raw"
className="min-w-0 gap-2 rounded-lg px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.1em] transition-all data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-sm"
>
<FileJson className="h-3.5 w-3.5" />
Raw Context
</TabsTrigger>
</TabsList>
<TabsContent
value="details"
className="mt-0 space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-500"
>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<MetaRow
label="Entry Signature"
value={entry.id.slice(0, 16) + '...'}
icon={Fingerprint}
/>
<MetaRow label="Telemetry Origin" value={entry.source} icon={Database} />
<MetaRow label="Process ID" value={entry.processId ?? 'NA'} icon={Cpu} />
<MetaRow
label="Operational Run"
value={entry.runId?.slice(0, 8) ?? 'NA'}
icon={ShieldCheck}
/>
</div>
<div className="relative overflow-hidden rounded-[2rem] border border-border bg-muted/10 p-1 shadow-inner group">
{/* Background Scanline */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-primary/[0.03] to-transparent h-[200%] -top-full animate-[scan_8s_linear_infinite] pointer-events-none" />
<div className="rounded-[calc(2rem-4px)] border border-dashed border-border/40 bg-background/50 p-6 space-y-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/5 border border-primary/20 text-primary shadow-inner">
<Terminal className="h-4 w-4" />
</div>
<div className="space-y-0.5">
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-primary">
Automated Summary
</p>
<p className="text-[10px] font-medium uppercase tracking-[0.1em] text-muted-foreground/55">
Quick interpretation
</p>
</div>
</div>
<p className="text-[13px] leading-relaxed text-muted-foreground/80 font-medium">
This telemetry node was captured from{' '}
<span className="rounded bg-muted/40 px-1.5 py-0.5 font-semibold text-foreground">
{sourceLabel ?? entry.source}
</span>
operating at the{' '}
<span
className={cn(
'rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-[0.08em]',
entry.level === 'error'
? 'bg-red-500/10 text-red-500'
: entry.level === 'warn'
? 'bg-amber-500/10 text-amber-500'
: entry.level === 'info'
? 'bg-sky-500/10 text-sky-500'
: 'bg-zinc-500/10 text-zinc-500'
)}
>
{entry.level}
</span>{' '}
threshold. The operational payload indicates an event state of{' '}
<span className="font-semibold text-foreground">{entry.event}</span>.
</p>
</div>
</div>
</TabsContent>
<TabsContent
value="raw"
className="mt-0 animate-in fade-in slide-in-from-bottom-2 duration-500"
>
<div className="group relative rounded-2xl border-2 border-border bg-zinc-950 p-1 shadow-2xl transition-all hover:border-primary/20">
{/* Copy HUD */}
<div className="absolute right-4 top-4 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="rounded-md border border-white/10 bg-white/5 px-2 py-1 text-[9px] font-medium uppercase tracking-[0.12em] text-white/45 backdrop-blur-md">
JSON.RAW.MODE
</div>
</div>
<ScrollArea className="h-[30rem] w-full rounded-xl p-6">
<pre className="font-mono text-[12px] leading-relaxed tracking-tight text-zinc-400 selection:bg-primary/40 selection:text-primary-foreground">
{formatJson({
id: entry.id,
timestamp: entry.timestamp,
level: entry.level,
source: entry.source,
event: entry.event,
message: entry.message,
processId: entry.processId,
runId: entry.runId,
context: entry.context ?? {},
})}
</pre>
</ScrollArea>
</div>
</TabsContent>
</Tabs>
</div>
</ScrollArea>
<div className="flex shrink-0 items-center justify-between border-t border-border bg-muted/5 px-6 py-3">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" />
<span className="text-[10px] font-medium uppercase tracking-[0.12em] text-foreground/35">
Node Verified
</span>
</div>
<div className="h-3 w-px bg-border/40" />
<span className="text-[10px] font-medium tabular-nums uppercase tracking-[0.12em] text-foreground/35">
{entry.id.slice(0, 8)}
</span>
</div>
<div className="rounded-full bg-primary/5 px-2 py-0.5 border border-primary/10">
<span className="text-[9px] font-medium uppercase tracking-[0.12em] text-primary/65">
CCS-TEC-v3
</span>
</div>
</div>
</div>
);
}
+157 -57
View File
@@ -1,10 +1,8 @@
import { AlertCircle, Inbox, Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Activity, ArrowRight, Inbox, Loader2 } from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area';
import type { LogsEntry } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import { LogLevelBadge } from './log-level-badge';
import { formatLogTimestamp, formatRelativeLogTime } from './utils';
export function LogsEntryList({
entries,
@@ -22,40 +20,79 @@ export function LogsEntryList({
isFetching: boolean;
}) {
return (
<Card className="gap-4 border-border/70 bg-card/85 shadow-sm">
<CardHeader className="space-y-1 border-b pb-4">
<div className="flex items-center justify-between gap-3">
<div>
<CardTitle>Activity queue</CardTitle>
<CardDescription>
Dense operational feed for the active source and severity mix.
</CardDescription>
<div className="flex min-h-0 flex-1 flex-col bg-background/50 backdrop-blur-sm">
<div className="flex shrink-0 items-center justify-between border-b border-border bg-card/40 px-6 py-3 shadow-sm">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="h-2 w-2 animate-pulse rounded-full bg-primary shadow-[0_0_12px_rgba(var(--primary),0.6)]" />
<h2 className="text-[12px] font-semibold uppercase tracking-[0.14em] text-foreground">
Live Entry Stream
</h2>
</div>
<div className="h-4 w-px bg-border/60" />
<div className="flex items-center gap-2 rounded-full border border-emerald-500/20 bg-emerald-500/10 px-2 py-0.5">
<Activity className="h-3 w-3 text-emerald-500" />
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-emerald-600">
Live telemetry
</span>
</div>
{isFetching ? <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> : null}
</div>
</CardHeader>
<CardContent className="px-0">
<div className="flex items-center gap-3">
{isFetching && (
<div className="flex items-center gap-2 rounded-full border border-primary/20 bg-primary/10 px-3 py-1">
<Loader2 className="h-3 w-3 animate-spin text-primary" />
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-primary">
Syncing
</span>
</div>
)}
<span className="font-mono text-[10px] font-medium uppercase tracking-[0.16em] text-foreground/35">
NODE.01
</span>
</div>
</div>
<div className="flex items-center gap-0 border-b border-border bg-muted/30 px-0 py-2 text-[10px] font-semibold uppercase tracking-[0.12em] text-foreground/45">
<div className="w-[6.5rem] shrink-0 px-6">Time</div>
<div className="w-14 shrink-0 border-l border-border/10 px-2 text-center">Lvl</div>
<div className="w-[15rem] shrink-0 border-l border-border/10 px-4">Source</div>
<div className="flex-1 border-l border-border/10 px-4">Message</div>
<div className="w-[5.5rem] shrink-0 border-l border-border/10 px-2 text-center">Proc</div>
<div className="w-[5.5rem] shrink-0 border-l border-border/10 px-3 text-center">Run</div>
<div className="w-11 shrink-0 border-l border-border/10 px-2 text-center">Open</div>
</div>
<div className="flex-1 min-h-0 overflow-hidden">
{isLoading ? (
<div className="space-y-3 px-6 pb-6">
{[1, 2, 3, 4, 5].map((item) => (
<div key={item} className="rounded-xl border p-4">
<div className="h-3 w-20 rounded bg-muted" />
<div className="mt-3 h-4 w-4/5 rounded bg-muted" />
<div className="mt-2 h-3 w-2/5 rounded bg-muted" />
</div>
<div className="space-y-1 p-2">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].map((item) => (
<div
key={item}
className="h-10 w-full animate-pulse rounded-lg border border-border/5 bg-muted/20"
/>
))}
</div>
) : entries.length === 0 ? (
<div className="flex min-h-[24rem] flex-col items-center justify-center gap-3 px-6 py-12 text-center text-muted-foreground">
<Inbox className="h-8 w-8" />
<div>
<p className="font-medium text-foreground">No entries matched these filters.</p>
<p className="text-sm">Try broadening the source, severity, or search terms.</p>
<div className="flex h-full animate-in fade-in duration-1000 flex-col items-center justify-center gap-6 px-8 text-center">
<div className="relative">
<div className="absolute inset-0 animate-ping rounded-full bg-muted/5 p-12" />
<div className="relative rounded-full border border-dashed border-border/40 bg-muted/5 p-10">
<Inbox className="h-10 w-10 text-muted-foreground/20" />
</div>
</div>
<div className="space-y-2">
<p className="text-[13px] font-semibold uppercase tracking-[0.14em] text-foreground/55">
No matching entries
</p>
<p className="max-w-[18rem] text-[12px] font-medium leading-relaxed text-muted-foreground/60">
Your current source, level, or search filters are hiding the stream. Adjust them to
bring entries back into view.
</p>
</div>
</div>
) : (
<ScrollArea className="h-[42rem]">
<div className="space-y-2 px-3 pb-3">
<ScrollArea className="h-full">
<div className="flex flex-col">
{entries.map((entry) => {
const isSelected = entry.id === selectedEntryId;
@@ -65,51 +102,114 @@ export function LogsEntryList({
type="button"
onClick={() => onSelect(entry.id)}
className={cn(
'w-full rounded-2xl border px-4 py-3 text-left transition-colors',
'group relative flex w-full items-center border-b border-border/5 px-0 py-2.5 text-left transition-all duration-150',
isSelected
? 'border-primary/40 bg-primary/5 shadow-sm'
: 'border-border/70 bg-background/70 hover:border-primary/20 hover:bg-muted/30'
? 'z-10 bg-primary/[0.08] shadow-[inset_4px_0_0_rgba(var(--primary),1)]'
: 'bg-transparent hover:bg-muted/30'
)}
>
<div className="grid gap-3 xl:grid-cols-[7.5rem_minmax(0,1fr)] xl:items-start">
<div className="space-y-2">
<p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">
{formatRelativeLogTime(entry.timestamp)}
</p>
<p className="text-xs text-muted-foreground">
{formatLogTimestamp(entry.timestamp)}
<div className="absolute inset-y-0 left-0 w-1 origin-center scale-y-0 bg-primary transition-transform duration-300 group-hover:scale-y-100" />
<div className="flex w-full items-center gap-0">
<div className="flex w-[6.5rem] shrink-0 items-center">
<p
className={cn(
'px-6 font-mono text-[11px] font-semibold tabular-nums transition-colors',
isSelected
? 'text-primary'
: 'text-foreground/60 group-hover:text-foreground'
)}
>
{new Date(entry.timestamp).toLocaleTimeString(undefined, {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
</p>
</div>
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<LogLevelBadge level={entry.level} />
<span className="text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">
<div className="flex min-w-0 flex-1 items-center gap-0">
<div className="flex w-14 shrink-0 items-center justify-center opacity-80 transition-opacity group-hover:opacity-100">
<LogLevelBadge
level={entry.level}
className="origin-center scale-[0.85]"
/>
</div>
<div className="flex w-[15rem] shrink-0 flex-col gap-0.5 overflow-hidden px-4">
<span
className={cn(
'truncate text-[11px] font-semibold uppercase tracking-[0.12em] transition-colors',
isSelected
? 'text-foreground'
: 'text-foreground/50 group-hover:text-foreground/80'
)}
>
{sourceLabels[entry.source] ?? entry.source}
</span>
<span className="text-xs text-muted-foreground">{entry.event}</span>
<span className="truncate text-[10px] font-medium uppercase tracking-[0.1em] text-muted-foreground/55">
{entry.event}
</span>
</div>
<p className="line-clamp-2 text-sm font-medium leading-6 text-foreground">
{entry.message}
</p>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
<span>PID {entry.processId ?? 'n/a'}</span>
<span>Run {entry.runId ?? 'n/a'}</span>
<div className="min-w-0 flex-1 px-4">
<p
className={cn(
'truncate text-[13px] font-medium leading-5 transition-colors',
isSelected
? 'text-foreground'
: 'text-foreground/70 group-hover:text-foreground'
)}
>
{entry.message}
</p>
</div>
<div className="flex w-[5.5rem] shrink-0 items-center justify-center px-2 font-mono text-[10px] font-semibold tabular-nums tracking-[0.12em] text-foreground/30 transition-colors group-hover:text-primary/45">
{entry.processId ?? '????'}
</div>
<div className="flex w-[5.5rem] shrink-0 items-center justify-center px-3 font-mono text-[10px] font-semibold tabular-nums tracking-[0.12em] text-foreground/30 transition-colors group-hover:text-primary/45">
{entry.runId?.slice(0, 4).toUpperCase() ?? 'NONE'}
</div>
<div className="flex w-11 shrink-0 items-center justify-center px-2">
<div
className={cn(
'flex h-7 w-7 items-center justify-center rounded-full border transition-all',
isSelected
? 'animate-in zoom-in duration-300 border-primary/20 bg-primary/10 text-primary'
: 'border-transparent text-foreground/20 group-hover:border-border/40 group-hover:bg-background/80 group-hover:text-foreground/55'
)}
>
<ArrowRight className="h-3.5 w-3.5" />
</div>
</div>
</div>
</div>
{entry.level === 'error' ? (
<div className="mt-3 inline-flex items-center gap-2 rounded-full border border-red-500/20 bg-red-500/5 px-2.5 py-1 text-[11px] font-medium text-red-700 dark:text-red-300">
<AlertCircle className="h-3.5 w-3.5" />
Elevated severity
</div>
) : null}
</button>
);
})}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
</div>
<div className="flex shrink-0 items-center justify-between border-t border-border bg-muted/5 px-6 py-2">
<div className="flex items-center gap-3">
<span className="text-[10px] font-medium uppercase tracking-[0.12em] text-foreground/35">
Node: CCS-CORE
</span>
<div className="h-1 w-1 rounded-full bg-border/40" />
<span className="text-[10px] font-medium uppercase tracking-[0.12em] text-foreground/35">
Status: Operational
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] font-medium uppercase tracking-[0.12em] text-foreground/35">
Entries: {entries.length}
</span>
</div>
</div>
</div>
);
}
+127 -89
View File
@@ -1,12 +1,11 @@
import { Search, RefreshCw, SlidersHorizontal } from 'lucide-react';
import { Search, RefreshCw, Filter, Shield, Zap } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import type { LogsSource } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import type { LogsLevelFilter, LogsSourceFilter } from '@/hooks/use-logs';
import { getLogLevelOptions, getSelectedSourceLabel } from '@/hooks/use-logs';
import { getLogLevelOptions } from '@/hooks/use-logs';
export function LogsFilters({
sources,
@@ -33,67 +32,76 @@ export function LogsFilters({
onRefresh: () => void;
isRefreshing: boolean;
}) {
const sourceLabel = getSelectedSourceLabel(selectedSource, sources);
const levels = getLogLevelOptions();
const limits = [50, 100, 150, 250];
return (
<Card className="gap-4 border-border/70 bg-card/85 shadow-sm">
<CardHeader className="space-y-3 border-b pb-4">
<div className="flex items-center gap-2">
<SlidersHorizontal className="h-4 w-4 text-muted-foreground" />
<CardTitle>Explorer filters</CardTitle>
</div>
<div className="space-y-1">
<CardDescription>
Narrow the stream the same way you would in a management console: start with source,
then severity, then message text.
</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-5">
<div className="space-y-2">
<Label htmlFor="logs-search">Search</Label>
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="logs-search"
value={search}
onChange={(event) => onSearchChange(event.target.value)}
placeholder="Search message, event, process, or run ID"
className="h-11 border-border/70 bg-background/70 pl-9"
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<Label>Source</Label>
<span className="text-xs text-muted-foreground">{sourceLabel}</span>
</div>
<div className="flex flex-wrap gap-2" aria-label="Source filter">
<button
type="button"
onClick={() => onSourceChange('all')}
className={cn(
'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
selectedSource === 'all'
? 'border-primary/40 bg-primary/10 text-primary'
: 'border-border/70 bg-background/70 text-muted-foreground hover:border-primary/30 hover:text-foreground'
)}
<div className="flex flex-col gap-6 animate-in fade-in slide-in-from-left-2 duration-700">
<div className="space-y-3">
<div className="flex items-center justify-between px-1">
<div className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary shadow-[0_0_8px_rgba(var(--primary),0.5)]" />
<Label
htmlFor="logs-search"
className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground/80"
>
All sources
</button>
Payload Search
</Label>
</div>
<Zap className="h-3 w-3 text-primary/20" />
</div>
<div className="group relative">
<div className="pointer-events-none absolute inset-y-0 left-3.5 flex items-center transition-all group-focus-within:translate-x-1">
<Search className="h-3.5 w-3.5 text-foreground/20 group-focus-within:text-primary transition-colors" />
</div>
<Input
id="logs-search"
aria-label="Search"
value={search}
onChange={(event) => onSearchChange(event.target.value)}
placeholder="Scan for patterns..."
className="h-11 rounded-xl border-2 border-border/40 bg-background/50 pl-10 text-[13px] font-medium text-foreground placeholder:text-foreground/35 focus-visible:border-primary/40 focus-visible:ring-0 transition-all shadow-inner"
/>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-2 px-1">
<Filter className="h-3 w-3 text-primary/40" />
<Label className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground/80">
Source Matrix
</Label>
</div>
<div className="flex flex-col gap-1" aria-label="Source filter">
<button
type="button"
onClick={() => onSourceChange('all')}
className={cn(
'group relative flex items-center justify-between rounded-lg border px-3 py-2 transition-all active:scale-[0.98]',
selectedSource === 'all'
? 'border-primary/50 bg-primary/10 text-primary shadow-[0_0_15px_rgba(var(--primary),0.1)]'
: 'border-border/40 bg-muted/20 text-foreground/40 hover:border-border hover:bg-muted/40 hover:text-foreground/80'
)}
>
<span className="text-[11px] font-semibold uppercase tracking-[0.12em]">
Global Stream
</span>
{selectedSource === 'all' && (
<div className="h-1.5 w-1.5 rounded-full bg-primary shadow-[0_0_8px_rgba(var(--primary),0.8)]" />
)}
</button>
<div className="grid grid-cols-2 gap-1 mt-1">
{sources.map((source) => (
<button
key={source.source}
type="button"
onClick={() => onSourceChange(source.source)}
className={cn(
'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
'rounded-lg border px-2.5 py-1.5 text-[10px] font-semibold uppercase tracking-[0.1em] transition-all active:scale-[0.97]',
selectedSource === source.source
? 'border-primary/40 bg-primary/10 text-primary'
: 'border-border/70 bg-background/70 text-muted-foreground hover:border-primary/30 hover:text-foreground'
? 'border-primary/50 bg-primary/10 text-primary shadow-sm'
: 'border-border/40 bg-muted/20 text-foreground/40 hover:border-border hover:bg-muted/40 hover:text-foreground/80'
)}
>
{source.label}
@@ -101,60 +109,90 @@ export function LogsFilters({
))}
</div>
</div>
</div>
<div className="space-y-2">
<Label>Level</Label>
<div className="flex flex-wrap gap-2" aria-label="Level filter">
{levels.map((option) => (
<button
key={option.value}
type="button"
onClick={() => onLevelChange(option.value)}
className={cn(
'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
selectedLevel === option.value
? 'border-primary/40 bg-primary/10 text-primary'
: 'border-border/70 bg-background/70 text-muted-foreground hover:border-primary/30 hover:text-foreground'
)}
>
{option.label}
</button>
))}
</div>
{/* Threshold Control */}
<div className="space-y-4">
<div className="flex items-center gap-2 px-1">
<Shield className="h-3 w-3 text-primary/40" />
<Label className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground/80">
Sensitivity
</Label>
</div>
<div className="grid grid-cols-2 gap-1.5" aria-label="Level filter">
{levels.map((option) => (
<button
key={option.value}
type="button"
onClick={() => onLevelChange(option.value)}
className={cn(
'flex flex-col items-center gap-1 rounded-lg border px-2 py-2 text-[10px] font-semibold uppercase tracking-[0.1em] transition-all active:scale-[0.97]',
selectedLevel === option.value
? 'border-primary/50 bg-primary/10 text-primary shadow-[0_0_15px_rgba(var(--primary),0.1)]'
: 'border-border/40 bg-muted/20 text-foreground/40 hover:border-border hover:bg-muted/40 hover:text-foreground/80'
)}
>
<span>{option.label}</span>
<div
className={cn(
'h-0.5 w-4 rounded-full transition-colors',
selectedLevel === option.value ? 'bg-primary' : 'bg-foreground/10'
)}
/>
</button>
))}
</div>
</div>
<div className="space-y-3 rounded-2xl border border-border/70 bg-background/70 p-4">
<div className="space-y-1">
<p className="text-xs font-medium uppercase tracking-[0.24em] text-muted-foreground">
View window
</p>
<p className="text-sm text-muted-foreground">
Keep the rendered slice tight when you are chasing a live issue.
</p>
{/* Operational Deck */}
<div className="mt-4 rounded-2xl border-2 border-border bg-card/40 p-1.5 shadow-xl shadow-black/5">
<div className="rounded-[calc(1rem-2px)] border border-dashed border-border bg-background/60 p-4 space-y-5">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<p className="text-[10px] font-semibold uppercase tracking-[0.14em] text-primary">
Operational Window
</p>
<p className="text-[11px] font-medium uppercase tracking-[0.1em] text-foreground/45">
Tail Capacity
</p>
</div>
<div className="h-8 w-8 rounded-lg bg-primary/5 border border-primary/10 flex items-center justify-center">
<Zap className="h-4 w-4 text-primary/40" />
</div>
</div>
<div className="flex flex-wrap gap-2" aria-label="Visible entries">
<div className="grid grid-cols-4 gap-1.5" aria-label="Visible entries">
{limits.map((option) => (
<button
key={option}
type="button"
onClick={() => onLimitChange(option)}
className={cn(
'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
'rounded-md border py-1.5 text-[11px] font-semibold tabular-nums transition-all active:scale-[0.95]',
limit === option
? 'border-primary/40 bg-primary/10 text-primary'
: 'border-border/70 bg-background text-muted-foreground hover:border-primary/30 hover:text-foreground'
? 'border-foreground bg-foreground text-background'
: 'border-border/60 bg-muted/40 text-foreground/40 hover:bg-muted hover:text-foreground'
)}
>
{option} entries
{option}
</button>
))}
</div>
<Button variant="outline" size="sm" className="w-full gap-2" onClick={onRefresh}>
<RefreshCw className={isRefreshing ? 'h-4 w-4 animate-spin' : 'h-4 w-4'} />
Refresh stream
<Button
variant="ghost"
size="sm"
className="group relative h-10 w-full overflow-hidden rounded-xl border-none bg-primary text-[11px] font-semibold uppercase tracking-[0.14em] text-primary-foreground transition-all hover:scale-[1.02] hover:shadow-lg hover:shadow-primary/20 active:scale-[0.98]"
onClick={onRefresh}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000" />
<div className="flex items-center gap-2">
<RefreshCw className={cn('h-3.5 w-3.5', isRefreshing && 'animate-spin')} />
<span>Refresh Entries</span>
</div>
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}
+47
View File
@@ -374,3 +374,50 @@
.scrollbar-editor::-webkit-scrollbar-corner {
background: transparent;
}
@keyframes scan {
0% {
transform: translateY(0);
}
100% {
transform: translateY(100%);
}
}
.animate-scan {
animation: scan 8s linear infinite;
}
@keyframes slide-in-from-right {
from {
transform: translateX(10px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slide-in-from-left {
from {
transform: translateX(-10px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in-from-right-4 {
animation: slide-in-from-right 0.5s ease-out;
}
.slide-in-from-left-2 {
animation: slide-in-from-left 0.4s ease-out;
}
.slide-in-from-bottom-2 {
animation: enter 0.3s ease-out;
}
+433 -165
View File
@@ -1,41 +1,93 @@
import { useEffect, useState } from 'react';
import {
AlertCircle,
ArrowRight,
ChevronLeft,
ChevronRight,
RefreshCw,
ScrollText,
ShieldAlert,
TimerReset,
} from 'lucide-react';
import { Link } from 'react-router-dom';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { cn } from '@/lib/utils';
import { ErrorLogsMonitor } from '@/components/error-logs-monitor';
import { LogsConfigCard } from '@/components/logs/logs-config-card';
import { LogsDetailPanel } from '@/components/logs/logs-detail-panel';
import { LogsEntryList } from '@/components/logs/logs-entry-list';
import { LogsFilters } from '@/components/logs/logs-filters';
import { LogsOverviewCards } from '@/components/logs/logs-overview-cards';
import { LogsPageSkeleton } from '@/components/logs/logs-page-skeleton';
import { getSourceLabelMap, useLogsWorkspace, useUpdateLogsConfig } from '@/hooks/use-logs';
const DESKTOP_LOGS_BREAKPOINT = 1200;
const LEFT_PANEL_WIDTH = 336;
const RIGHT_PANEL_WIDTH = 368;
const COLLAPSED_PANEL_WIDTH = 52;
function CollapsedPaneToggle({
side,
label,
onExpand,
}: {
side: 'left' | 'right';
label: string;
onExpand: () => void;
}) {
return (
<div
className={cn(
'flex h-full w-full flex-col items-center justify-center gap-4 bg-muted/5',
side === 'left' ? 'border-r border-border' : 'border-l border-border'
)}
>
<Button
variant="ghost"
size="icon"
onClick={onExpand}
aria-label={`Show ${label.toLowerCase()}`}
className="h-9 w-9 rounded-xl border border-border/70 bg-background/85 shadow-sm"
>
{side === 'left' ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
<span
className="text-[10px] font-semibold uppercase tracking-[0.14em] text-foreground/45"
style={{ writingMode: 'vertical-rl' }}
>
{label}
</span>
</div>
);
}
export function LogsPage() {
const workspace = useLogsWorkspace();
const updateConfig = useUpdateLogsConfig();
const sourceLabels = getSourceLabelMap(workspace.sourcesQuery.data ?? []);
const errors = [
workspace.configQuery.error,
workspace.sourcesQuery.error,
workspace.entriesQuery.error,
].filter(Boolean) as Error[];
const [isDesktopLayout, setIsDesktopLayout] = useState(() =>
typeof window !== 'undefined' ? window.innerWidth >= DESKTOP_LOGS_BREAKPOINT : false
);
const [isFiltersCollapsed, setIsFiltersCollapsed] = useState(false);
const [isDetailsCollapsed, setIsDetailsCollapsed] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia(`(min-width: ${DESKTOP_LOGS_BREAKPOINT}px)`);
const syncLayout = () => {
setIsDesktopLayout(window.innerWidth >= DESKTOP_LOGS_BREAKPOINT);
};
syncLayout();
mediaQuery.addEventListener('change', syncLayout);
return () => mediaQuery.removeEventListener('change', syncLayout);
}, []);
if (workspace.isInitialLoading) {
return (
<div className="p-6 max-w-7xl mx-auto">
<LogsPageSkeleton />
</div>
);
return <LogsPageSkeleton />;
}
const config = workspace.configQuery.data;
@@ -44,173 +96,389 @@ export function LogsPage() {
}
return (
<div className="p-6 space-y-5">
<div className="relative overflow-hidden rounded-3xl border bg-[linear-gradient(135deg,var(--background)_0%,color-mix(in_oklab,var(--card)_84%,var(--muted))_100%)] p-6 shadow-sm">
<div className="pointer-events-none absolute -right-10 top-1/2 hidden -translate-y-1/2 select-none text-[120px] font-semibold tracking-[-0.08em] text-foreground/5 xl:block">
LOGS
</div>
<div className="relative flex flex-col gap-5 xl:flex-row xl:items-end xl:justify-between">
<div className="space-y-3">
<div className="inline-flex items-center gap-2 rounded-full border border-border/70 bg-background/70 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.24em] text-muted-foreground">
<ScrollText className="h-3.5 w-3.5" />
System logs
<div className="relative flex h-full min-h-full flex-col overflow-hidden border-t border-border/40 bg-background font-sans text-foreground antialiased selection:bg-primary/30 selection:text-primary">
<div className="pointer-events-none absolute inset-0 z-0 opacity-40 [background-image:radial-gradient(circle_at_1px_1px,rgba(38,38,36,0.08)_1px,transparent_0)] [background-size:14px_14px]" />
<div className="relative z-10 flex shrink-0 items-center justify-between border-b border-border/80 bg-card/95 px-6 py-2 backdrop-blur-xl shadow-md transition-all xl:px-8">
<div className="flex items-center gap-10">
<div className="flex items-center gap-4">
<div className="group flex h-9 w-9 items-center justify-center rounded-xl border-2 border-primary/20 bg-primary/5 text-primary shadow-inner transition-all hover:scale-110 active:scale-90">
<ScrollText className="h-4.5 w-4.5 transition-transform group-hover:rotate-6" />
</div>
<div className="space-y-2">
<h1 className="text-3xl font-semibold tracking-[-0.03em] text-foreground md:text-4xl">
Operational log workspace
</h1>
<p className="max-w-3xl text-sm leading-6 text-muted-foreground md:text-[15px]">
Work through the CCS event stream like a control center: source-first filtering,
dense entry inspection, and retention controls beside the live activity feed.
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-2.5">
<span className="rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-primary-foreground shadow-lg shadow-primary/20">
Operational
</span>
<h1 className="text-[17px] font-semibold tracking-tight text-foreground">
Log Operations Center
</h1>
</div>
<p className="font-mono text-[10px] font-medium uppercase tracking-[0.16em] text-foreground/45">
CCS.TOC.LOGS.STREAM.v3
</p>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3 xl:min-w-[26rem]">
<Card className="border-border/70 bg-background/70 shadow-none">
<CardContent className="space-y-1 p-4">
<div className="inline-flex items-center gap-2 text-[11px] font-medium uppercase tracking-[0.22em] text-muted-foreground">
<ShieldAlert className="h-3.5 w-3.5" />
<div className="hidden h-8 w-px bg-border/80 md:block" />
<div className="hidden items-center gap-10 md:flex">
<div className="flex items-center gap-3">
<div
className={cn(
'h-2 w-2 rounded-full ring-4 transition-all duration-700',
config.redact
? 'bg-emerald-500 shadow-[0_0_20px_rgba(16,185,129,0.6)] ring-emerald-500/30'
: 'bg-zinc-600 ring-transparent'
)}
/>
<div className="flex flex-col">
<span className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground/90">
Redaction
</div>
<p className="text-lg font-semibold text-foreground">
{config.redact ? 'Active' : 'Disabled'}
</p>
<p className="text-xs text-muted-foreground">
Sensitive fields are masked before persistence.
</p>
</CardContent>
</Card>
<Card className="border-border/70 bg-background/70 shadow-none">
<CardContent className="space-y-1 p-4">
<div className="inline-flex items-center gap-2 text-[11px] font-medium uppercase tracking-[0.22em] text-muted-foreground">
<TimerReset className="h-3.5 w-3.5" />
</span>
<span className="text-[11px] font-medium text-foreground/50">
{config.redact ? 'Enforced' : 'Standby'}
</span>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex h-7 w-7 items-center justify-center rounded-lg border-2 border-border bg-muted shadow-inner">
<TimerReset className="h-3.5 w-3.5 text-foreground" />
</div>
<div className="flex flex-col">
<span className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground/90">
Retention
</div>
<p className="text-lg font-semibold text-foreground">{config.retain_days} days</p>
<p className="text-xs text-muted-foreground">
Rotation threshold {config.rotate_mb} MB.
</p>
</CardContent>
</Card>
<div className="flex flex-wrap items-center gap-2 xl:justify-end">
<Button
variant="outline"
className="gap-2 bg-background/70"
onClick={() =>
void Promise.all([
workspace.sourcesQuery.refetch(),
workspace.entriesQuery.refetch(),
])
}
>
<RefreshCw
className={
workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching
? 'h-4 w-4 animate-spin'
: 'h-4 w-4'
}
/>
Refresh
</Button>
<Button asChild variant="ghost" className="gap-2">
<Link to="/health">
Health
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
</span>
<span className="text-[11px] font-medium text-foreground/50">
{config.retain_days}D / {config.rotate_mb}MB
</span>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
className="group h-9 gap-3 rounded-xl border-2 border-border bg-muted px-5 text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground transition-all hover:bg-foreground hover:text-background active:scale-95 shadow-lg shadow-black/5"
onClick={() =>
void Promise.all([workspace.sourcesQuery.refetch(), workspace.entriesQuery.refetch()])
}
>
<div className="relative">
<RefreshCw
className={cn(
'h-3.5 w-3.5 transition-transform duration-500',
(workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching) &&
'animate-spin'
)}
/>
{!workspace.entriesQuery.isFetching && !workspace.sourcesQuery.isFetching && (
<div className="absolute -right-1 -top-1 h-1.5 w-1.5 rounded-full border border-muted bg-primary animate-pulse" />
)}
</div>
{workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching
? 'Syncing'
: 'Refresh'}
</Button>
<div className="h-7 w-px bg-border/80" />
<Button
asChild
variant="ghost"
size="sm"
className="h-9 w-9 rounded-xl border-2 border-border bg-muted p-0 text-foreground transition-all hover:bg-foreground hover:text-background active:scale-95 shadow-lg shadow-black/5"
>
<Link to="/health">
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
</div>
</div>
{errors.length > 0 ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Unable to fully load the logs workspace</AlertTitle>
<AlertDescription>{errors[0]?.message}</AlertDescription>
</Alert>
) : null}
<div className="relative z-10 flex min-h-0 flex-1 overflow-hidden">
<Tabs defaultValue="stream" className="flex flex-1 flex-col overflow-hidden">
<div className="flex shrink-0 items-center justify-between border-b border-border/80 bg-card/80 px-6 py-2 backdrop-blur-xl shadow-inner xl:px-8">
<TabsList className="h-10 w-auto gap-1.5 rounded-xl border border-border/60 bg-muted/40 p-1">
<TabsTrigger
value="stream"
className="rounded-lg px-5 text-[11px] font-semibold uppercase tracking-[0.12em] text-foreground/60 transition-all data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-md"
>
Telemetry Stream
</TabsTrigger>
<TabsTrigger
value="errors"
className="rounded-lg px-5 text-[11px] font-semibold uppercase tracking-[0.12em] text-foreground/60 transition-all data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-md"
>
Legacy Errors
</TabsTrigger>
</TabsList>
<LogsOverviewCards
config={config}
sources={workspace.sourcesQuery.data ?? []}
entries={workspace.entriesQuery.data ?? []}
latestTimestamp={workspace.latestTimestamp}
/>
<Tabs defaultValue="stream" className="space-y-5">
<TabsList className="grid w-full max-w-md grid-cols-2">
<TabsTrigger value="stream">Activity stream</TabsTrigger>
<TabsTrigger value="errors">CLIProxy errors</TabsTrigger>
</TabsList>
<TabsContent value="stream" className="space-y-5">
<div className="grid gap-5 xl:grid-cols-[18rem_minmax(0,1.2fr)_minmax(18rem,0.8fr)]">
<div className="space-y-5 xl:sticky xl:top-5 xl:self-start">
<LogsFilters
sources={workspace.sourcesQuery.data ?? []}
selectedSource={workspace.selectedSource}
onSourceChange={workspace.setSelectedSource}
selectedLevel={workspace.selectedLevel}
onLevelChange={workspace.setSelectedLevel}
search={workspace.search}
onSearchChange={workspace.setSearch}
limit={workspace.limit}
onLimitChange={workspace.setLimit}
onRefresh={() =>
void Promise.all([
workspace.sourcesQuery.refetch(),
workspace.entriesQuery.refetch(),
])
}
isRefreshing={
workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching
}
/>
<LogsConfigCard
config={config}
onSave={(payload) => updateConfig.mutate(payload)}
isPending={updateConfig.isPending}
/>
<div className="hidden items-center gap-3 lg:flex">
<div className="flex items-center gap-2 rounded-full border border-border bg-muted px-3 py-1 shadow-inner">
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.6)]"></span>
</span>
<span className="text-[11px] font-semibold uppercase tracking-tight text-foreground/80">
Connected
</span>
</div>
<span className="pr-4 text-[11px] font-medium tabular-nums text-foreground/45">
{workspace.entriesQuery.data?.length ?? 0} captured
</span>
</div>
<LogsEntryList
entries={workspace.entriesQuery.data ?? []}
selectedEntryId={workspace.selectedEntryId}
onSelect={workspace.setSelectedEntryId}
sourceLabels={sourceLabels}
isLoading={workspace.entriesQuery.isLoading}
isFetching={workspace.entriesQuery.isFetching}
/>
<LogsDetailPanel
entry={workspace.selectedEntry}
sourceLabel={
workspace.selectedEntry ? sourceLabels[workspace.selectedEntry.source] : undefined
}
/>
</div>
</TabsContent>
<TabsContent value="errors" className="space-y-4">
<Card className="border-border/70 bg-card/80 shadow-sm">
<CardContent className="flex flex-col gap-2 p-5">
<p className="text-[11px] font-medium uppercase tracking-[0.22em] text-muted-foreground">
Legacy diagnostics
</p>
<h2 className="text-xl font-semibold tracking-tight text-foreground">
CLIProxy request-failure viewer
</h2>
<p className="max-w-3xl text-sm leading-6 text-muted-foreground">
Keep the existing request-failure drilldown nearby while the structured CCS stream
matures. This tab preserves the old operational workflow instead of burying it
behind Home-only UI.
</p>
</CardContent>
</Card>
<ErrorLogsMonitor />
</TabsContent>
</Tabs>
<TabsContent
value="stream"
className="m-0 flex min-h-0 flex-1 overflow-y-auto lg:overflow-hidden focus-visible:outline-none"
>
{isDesktopLayout ? (
<div className="flex min-h-0 flex-1 overflow-hidden">
<div
data-logs-pane="filters"
style={{ width: isFiltersCollapsed ? COLLAPSED_PANEL_WIDTH : LEFT_PANEL_WIDTH }}
className="flex min-h-0 shrink-0 bg-muted/5"
>
{isFiltersCollapsed ? (
<CollapsedPaneToggle
side="left"
label="Filters"
onExpand={() => setIsFiltersCollapsed(false)}
/>
) : (
<div className="flex h-full min-h-0 w-full flex-col border-r border-border p-5 2xl:p-6">
<div className="mb-4 flex items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground/80">
Filters
</p>
<p className="text-[11px] text-muted-foreground/70">
Search, source, and retention controls
</p>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setIsFiltersCollapsed(true)}
aria-label="Hide filters"
className="h-9 w-9 rounded-xl border border-border/70 bg-background/85 shadow-sm"
>
<ChevronLeft className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="min-h-0 flex-1" data-logs-scroll-region="filters">
<div className="space-y-5 pr-4">
<LogsFilters
sources={workspace.sourcesQuery.data ?? []}
selectedSource={workspace.selectedSource}
onSourceChange={workspace.setSelectedSource}
selectedLevel={workspace.selectedLevel}
onLevelChange={workspace.setSelectedLevel}
search={workspace.search}
onSearchChange={workspace.setSearch}
limit={workspace.limit}
onLimitChange={workspace.setLimit}
onRefresh={() =>
void Promise.all([
workspace.sourcesQuery.refetch(),
workspace.entriesQuery.refetch(),
])
}
isRefreshing={
workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching
}
/>
<div className="border-t border-border/20 pt-5">
<LogsConfigCard
config={config}
onSave={(payload) => updateConfig.mutate(payload)}
isPending={updateConfig.isPending}
/>
</div>
</div>
</ScrollArea>
</div>
)}
</div>
<div
data-logs-pane="entries"
className="flex min-h-0 min-w-0 flex-1 overflow-hidden border-l border-r border-border bg-background/95"
>
<LogsEntryList
entries={workspace.entriesQuery.data ?? []}
selectedEntryId={workspace.selectedEntryId}
onSelect={workspace.setSelectedEntryId}
sourceLabels={sourceLabels}
isLoading={workspace.entriesQuery.isLoading}
isFetching={workspace.entriesQuery.isFetching}
/>
</div>
<div
data-logs-pane="details"
style={{ width: isDetailsCollapsed ? COLLAPSED_PANEL_WIDTH : RIGHT_PANEL_WIDTH }}
className="flex min-h-0 shrink-0 bg-muted/5 shadow-inner"
>
{isDetailsCollapsed ? (
<CollapsedPaneToggle
side="right"
label="Details"
onExpand={() => setIsDetailsCollapsed(false)}
/>
) : (
<div className="flex h-full min-h-0 w-full flex-col">
<div className="flex items-center justify-between border-b border-border/50 bg-background/60 px-3 py-2">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground/80">
Details
</p>
<p className="text-[11px] text-muted-foreground/70">
Selected entry context and raw payload
</p>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setIsDetailsCollapsed(true)}
aria-label="Hide details"
className="h-9 w-9 rounded-xl border border-border/70 bg-background/85 shadow-sm"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div className="min-h-0 flex-1 overflow-hidden">
<LogsDetailPanel
entry={workspace.selectedEntry}
sourceLabel={
workspace.selectedEntry
? sourceLabels[workspace.selectedEntry.source]
: undefined
}
/>
</div>
</div>
)}
</div>
</div>
) : (
<div className="flex min-h-0 flex-1 flex-col">
<div className="border-b border-border bg-muted/5 p-5">
<div className="flex flex-col gap-6">
<LogsFilters
sources={workspace.sourcesQuery.data ?? []}
selectedSource={workspace.selectedSource}
onSourceChange={workspace.setSelectedSource}
selectedLevel={workspace.selectedLevel}
onLevelChange={workspace.setSelectedLevel}
search={workspace.search}
onSearchChange={workspace.setSearch}
limit={workspace.limit}
onLimitChange={workspace.setLimit}
onRefresh={() =>
void Promise.all([
workspace.sourcesQuery.refetch(),
workspace.entriesQuery.refetch(),
])
}
isRefreshing={
workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching
}
/>
<LogsConfigCard
config={config}
onSave={(payload) => updateConfig.mutate(payload)}
isPending={updateConfig.isPending}
/>
</div>
</div>
<div className="flex min-h-[32rem] flex-col overflow-hidden border-b border-border bg-background/95">
<LogsEntryList
entries={workspace.entriesQuery.data ?? []}
selectedEntryId={workspace.selectedEntryId}
onSelect={workspace.setSelectedEntryId}
sourceLabels={sourceLabels}
isLoading={workspace.entriesQuery.isLoading}
isFetching={workspace.entriesQuery.isFetching}
/>
</div>
<div className="flex min-h-[30rem] flex-col overflow-hidden bg-muted/5 shadow-inner">
<LogsDetailPanel
entry={workspace.selectedEntry}
sourceLabel={
workspace.selectedEntry
? sourceLabels[workspace.selectedEntry.source]
: undefined
}
/>
</div>
</div>
)}
</TabsContent>
<TabsContent
value="errors"
className="m-0 flex-1 overflow-y-auto bg-background/20 p-6 focus-visible:outline-none xl:p-8"
>
<div className="mx-auto max-w-5xl space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="relative overflow-hidden rounded-[2.5rem] border-2 border-border bg-card/40 p-1.5 shadow-2xl shadow-black/10">
<div className="absolute inset-0 opacity-[0.02] pointer-events-none [background-image:radial-gradient(circle_at_center,var(--primary)_1px,transparent_0)] [background-size:24px_24px]" />
<Card className="rounded-[calc(2.5rem-0.375rem)] border-none bg-background/60 shadow-none overflow-hidden backdrop-blur-md">
<CardContent className="flex flex-col gap-6 p-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl border-2 border-primary/20 bg-primary/10 text-primary shadow-inner">
<ScrollText className="h-5 w-5" />
</div>
<div className="space-y-0.5">
<p className="text-[10px] font-black uppercase tracking-[0.4em] text-primary">
Legacy Diagnostic Node
</p>
<p className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground/40">
CCS-MATRIX-FAILURE-MONITOR
</p>
</div>
</div>
<div className="rounded-full border border-border bg-background/50 px-3 py-1 text-[9px] font-black uppercase tracking-widest text-foreground/40 shadow-inner">
Mode: Historical
</div>
</div>
<div className="space-y-3">
<h2 className="text-3xl font-black uppercase tracking-tighter text-foreground">
CLIProxy Failure Analysis
</h2>
<p className="max-w-3xl text-[15px] font-medium leading-relaxed text-muted-foreground/60">
Maintain oversight of legacy request failures while the unified stream
consolidates system-wide telemetry. This view provides direct access to the
historical failure matrix for deep-field debugging.
</p>
</div>
<div className="h-px w-full bg-gradient-to-r from-transparent via-border to-transparent opacity-40" />
</CardContent>
</Card>
</div>
<div className="rounded-[2.5rem] border-2 border-border bg-muted/5 p-8 shadow-inner backdrop-blur-sm">
<div className="mb-6 flex items-center gap-3">
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-primary shadow-[0_0_8px_rgba(var(--primary),0.5)]" />
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-foreground/40">
Realtime Monitoring Deck
</span>
</div>
<ErrorLogsMonitor />
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
}
+8 -3
View File
@@ -131,6 +131,11 @@ describe('LogsPage', () => {
beforeEach(() => {
vi.clearAllMocks();
global.fetch = fetchMock;
Object.defineProperty(window, 'innerWidth', {
configurable: true,
writable: true,
value: 900,
});
});
it('shows the loading skeleton while the initial queries are pending', () => {
@@ -184,11 +189,11 @@ describe('LogsPage', () => {
await userEvent.click(screen.getByRole('button', { name: /Worker retry scheduled/i }));
expect((await screen.findAllByText('task.retry')).length).toBeGreaterThan(0);
expect(screen.getByText('4121')).toBeInTheDocument();
expect(screen.getAllByText('4121').length).toBeGreaterThan(0);
await userEvent.click(screen.getByRole('tab', { name: /Raw context/i }));
expect(await screen.findByText(/"reason": "network jitter"/)).toBeInTheDocument();
expect(screen.getByText(/"runId": "run-2"/)).toBeInTheDocument();
expect(await screen.findByText(/network jitter/)).toBeInTheDocument();
expect(screen.getByText(/run-2/)).toBeInTheDocument();
});
});