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

This commit is contained in:
Tam Nhu Tran committed 2026-04-08 15:57:15 -04:00
1 parent 42481c0bb9
commit 6e39f338ce
8 files changed
+1041 -349

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ export function LogLevelBadge({ level, className }: { level: LogsLevel; classNam
return ( return (
<span <span
className={cn( 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], LEVEL_STYLES[level],
className className
)} )}
+155 -72
View File
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react'; 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 { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { import {
@@ -13,6 +12,7 @@ import {
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import type { LogsConfig, UpdateLogsConfigPayload } from '@/lib/api-client'; import type { LogsConfig, UpdateLogsConfigPayload } from '@/lib/api-client';
import { cn } from '@/lib/utils';
function parseInteger(value: string, fallback: number) { function parseInteger(value: string, fallback: number) {
const parsed = Number.parseInt(value, 10); const parsed = Number.parseInt(value, 10);
@@ -41,40 +41,72 @@ export function LogsConfigCard({
const isDirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(config), [config, draft]); const isDirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(config), [config, draft]);
return ( return (
<Card className="gap-4 border-border/70 bg-card/85 shadow-sm"> <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">
<CardHeader className="space-y-2 border-b pb-4"> <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>
<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"> <div className="flex items-center gap-2">
<Settings2 className="h-4 w-4 text-muted-foreground" /> <span
<CardTitle>Logging policy</CardTitle> className={cn(
'text-[11px] font-semibold uppercase tracking-[0.1em]',
config.enabled ? 'text-emerald-500' : 'text-zinc-500'
)}
>
{config.enabled ? 'Live' : 'Off'}
</span>
</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> </div>
<span className="rounded-full border px-2 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground"> <div className="rounded-xl border border-border/40 bg-background/50 p-3 flex flex-col gap-1">
{config.redact ? 'Redacted' : 'Plain'} <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> </span>
</div> </div>
</div> </div>
<div className="space-y-3"> <div className="space-y-2">
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/70 bg-background/60 p-3"> <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-1"> <div className="space-y-0.5">
<Label htmlFor="logs-enabled">Enabled</Label> <Label
<p className="text-xs text-muted-foreground"> htmlFor="logs-enabled"
Turn the unified logging pipeline on or off. 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> </p>
</div> </div>
<Switch <Switch
@@ -83,14 +115,20 @@ export function LogsConfigCard({
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setDraft((current) => ({ ...current, enabled: checked })) setDraft((current) => ({ ...current, enabled: checked }))
} }
className="data-[state=checked]:bg-primary"
/> />
</div> </div>
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/70 bg-background/60 p-3"> <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-1"> <div className="space-y-0.5">
<Label htmlFor="logs-redact">Redact payloads</Label> <Label
<p className="text-xs text-muted-foreground"> htmlFor="logs-redact"
Mask sensitive content before it lands in the log archive. 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> </p>
</div> </div>
<Switch <Switch
@@ -99,12 +137,22 @@ export function LogsConfigCard({
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setDraft((current) => ({ ...current, redact: checked })) setDraft((current) => ({ ...current, redact: checked }))
} }
className="data-[state=checked]:bg-primary"
/> />
</div> </div>
</div> </div>
<div className="space-y-4 pt-2">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="logs-config-level">Minimum level</Label> <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 <Select
value={draft.level} value={draft.level}
onValueChange={(value) => onValueChange={(value) =>
@@ -113,28 +161,52 @@ export function LogsConfigCard({
> >
<SelectTrigger <SelectTrigger
id="logs-config-level" id="logs-config-level"
aria-label="Minimum log 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"
className="border-border/70 bg-background/70"
> >
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent className="border-2 border-border bg-card">
<SelectItem value="error">Error only</SelectItem> <SelectItem
<SelectItem value="warn">Warn and above</SelectItem> value="error"
<SelectItem value="info">Info and above</SelectItem> className="text-[11px] font-semibold uppercase tracking-[0.1em]"
<SelectItem value="debug">Debug and above</SelectItem> >
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> </SelectContent>
</Select> </Select>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid grid-cols-2 gap-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="logs-rotate-mb">Rotate size (MB)</Label> <Label
htmlFor="logs-rotate-mb"
className="px-1 text-[10px] font-semibold uppercase tracking-[0.1em] text-foreground/50"
>
Rotation (MB)
</Label>
<Input <Input
id="logs-rotate-mb" id="logs-rotate-mb"
type="number" type="number"
min={1} min={1}
className="border-border/70 bg-background/70" 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} value={draft.rotate_mb}
onChange={(event) => onChange={(event) =>
setDraft((current) => ({ setDraft((current) => ({
@@ -145,12 +217,17 @@ export function LogsConfigCard({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="logs-retain-days">Retention (days)</Label> <Label
htmlFor="logs-retain-days"
className="px-1 text-[10px] font-semibold uppercase tracking-[0.1em] text-foreground/50"
>
Retain (Days)
</Label>
<Input <Input
id="logs-retain-days" id="logs-retain-days"
type="number" type="number"
min={1} min={1}
className="border-border/70 bg-background/70" 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} value={draft.retain_days}
onChange={(event) => onChange={(event) =>
setDraft((current) => ({ setDraft((current) => ({
@@ -161,39 +238,45 @@ export function LogsConfigCard({
/> />
</div> </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>
<div className="flex flex-wrap items-center gap-3 border-t border-border/70 pt-4"> <div className="flex flex-col gap-2 pt-4 border-t border-border">
<Button onClick={() => onSave(draft)} disabled={!isDirty || isPending} className="gap-2"> <Button
<Save className="h-4 w-4" /> onClick={() => onSave(draft)}
Save policy 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>
<Button <Button
variant="outline" variant="ghost"
onClick={() => setDraft(config)} onClick={() => setDraft(config)}
disabled={!isDirty || isPending} 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> </Button>
{isDirty ? <span className="text-sm text-muted-foreground">Unsaved changes</span> : null}
</div> </div>
</CardContent> </div>
</Card>
<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>
); );
} }
+210 -59
View File
@@ -1,16 +1,46 @@
import { FileJson, Info } from 'lucide-react'; import {
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; FileJson,
Info,
ShieldCheck,
Terminal,
Fingerprint,
Database,
Cpu,
Activity,
type LucideIcon,
} from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import type { LogsEntry } from '@/lib/api-client'; import type { LogsEntry } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import { LogLevelBadge } from './log-level-badge'; 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 ( return (
<div className="rounded-lg border bg-background/70 p-3"> <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">
<p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">{label}</p> <div className="flex items-center justify-between gap-2">
<p className="mt-1 break-all text-sm font-medium">{value}</p> <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> </div>
); );
} }
@@ -24,76 +54,175 @@ export function LogsDetailPanel({
}) { }) {
if (!entry) { if (!entry) {
return ( return (
<Card className="gap-4 border-border/70 bg-card/85 shadow-sm"> <div className="flex h-full flex-col items-center justify-center p-8 text-center animate-in fade-in duration-1000">
<CardHeader> <div className="relative mb-8">
<CardTitle>Inspector</CardTitle> <div className="absolute inset-0 animate-ping rounded-full bg-primary/5 p-12" />
<CardDescription> <div className="relative rounded-full border-2 border-dashed border-border/40 p-10 bg-muted/5">
Select an activity row to inspect its operational metadata. <Terminal className="h-10 w-10 text-muted-foreground/20" />
</CardDescription> </div>
</CardHeader> </div>
<CardContent className="flex min-h-[42rem] items-center justify-center text-sm text-muted-foreground"> <div className="max-w-xs space-y-3">
Nothing selected yet. <h3 className="text-[15px] font-semibold uppercase tracking-[0.14em] text-foreground/65">
</CardContent> Inspector Standby
</Card> </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 ( return (
<Card className="gap-4 border-border/70 bg-card/85 shadow-sm"> <div className="flex h-full flex-col bg-card/30 backdrop-blur-sm animate-in fade-in slide-in-from-right-4 duration-500">
<CardHeader className="space-y-3 border-b pb-4"> {/* Tactical Inspector Header */}
<div className="flex flex-wrap items-center gap-2"> <div className="relative shrink-0 border-b border-border bg-card/60 p-6 shadow-sm overflow-hidden">
<LogLevelBadge level={entry.level} /> {/* Pattern Overlay */}
<span className="rounded-full border px-2 py-0.5 text-xs font-medium text-muted-foreground"> <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]" />
<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} {sourceLabel ?? entry.source}
</span> </span>
<span className="text-xs text-muted-foreground"> </div>
{formatLogTimestamp(entry.timestamp)} </div>
<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',
})}
</span> </span>
</div> </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> </div>
</CardHeader>
<CardContent> <div className="space-y-2">
<Tabs defaultValue="details" className="space-y-4"> <div className="flex items-center gap-2">
<TabsList> <div className="h-1 w-4 rounded-full bg-primary/40" />
<TabsTrigger value="details" className="gap-2"> <p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-primary/65">
<Info className="h-4 w-4" /> 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 Details
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="raw" className="gap-2"> <TabsTrigger
<FileJson className="h-4 w-4" /> value="raw"
Raw context 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> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="details" className="space-y-4"> <TabsContent
<div className="grid gap-3 md:grid-cols-2"> value="details"
<MetaRow label="Entry ID" value={entry.id} /> className="mt-0 space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-500"
<MetaRow label="Source" value={entry.source} /> >
<MetaRow label="Process ID" value={entry.processId ?? 'Unavailable'} /> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<MetaRow label="Run ID" value={entry.runId ?? 'Unavailable'} /> <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>
<div className="rounded-2xl border border-border/70 bg-background/70 p-4">
<p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground"> <div className="relative overflow-hidden rounded-[2rem] border border-border bg-muted/10 p-1 shadow-inner group">
Context summary {/* 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>
<p className="mt-2 text-sm leading-6 text-muted-foreground"> <p className="text-[10px] font-medium uppercase tracking-[0.1em] text-muted-foreground/55">
This entry belongs to{' '} Quick interpretation
<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> </p>
</div> </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>
<TabsContent value="raw"> <TabsContent
<ScrollArea className="h-[24rem] rounded-xl border bg-zinc-950 p-4 text-xs text-zinc-100"> value="raw"
<pre className="whitespace-pre-wrap break-words font-mono"> 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({ {formatJson({
id: entry.id, id: entry.id,
timestamp: entry.timestamp, timestamp: entry.timestamp,
@@ -107,9 +236,31 @@ export function LogsDetailPanel({
})} })}
</pre> </pre>
</ScrollArea> </ScrollArea>
</div>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</CardContent> </div>
</Card> </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>
); );
} }
+154 -54
View File
@@ -1,10 +1,8 @@
import { AlertCircle, Inbox, Loader2 } from 'lucide-react'; import { Activity, ArrowRight, Inbox, Loader2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import type { LogsEntry } from '@/lib/api-client'; import type { LogsEntry } from '@/lib/api-client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { LogLevelBadge } from './log-level-badge'; import { LogLevelBadge } from './log-level-badge';
import { formatLogTimestamp, formatRelativeLogTime } from './utils';
export function LogsEntryList({ export function LogsEntryList({
entries, entries,
@@ -22,40 +20,79 @@ export function LogsEntryList({
isFetching: boolean; isFetching: boolean;
}) { }) {
return ( return (
<Card className="gap-4 border-border/70 bg-card/85 shadow-sm"> <div className="flex min-h-0 flex-1 flex-col bg-background/50 backdrop-blur-sm">
<CardHeader className="space-y-1 border-b pb-4"> <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 justify-between gap-3"> <div className="flex items-center gap-4">
<div> <div className="flex items-center gap-2">
<CardTitle>Activity queue</CardTitle> <div className="h-2 w-2 animate-pulse rounded-full bg-primary shadow-[0_0_12px_rgba(var(--primary),0.6)]" />
<CardDescription> <h2 className="text-[12px] font-semibold uppercase tracking-[0.14em] text-foreground">
Dense operational feed for the active source and severity mix. Live Entry Stream
</CardDescription> </h2>
</div> </div>
{isFetching ? <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> : null} <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> </div>
</CardHeader> </div>
<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 ? ( {isLoading ? (
<div className="space-y-3 px-6 pb-6"> <div className="space-y-1 p-2">
{[1, 2, 3, 4, 5].map((item) => ( {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].map((item) => (
<div key={item} className="rounded-xl border p-4"> <div
<div className="h-3 w-20 rounded bg-muted" /> key={item}
<div className="mt-3 h-4 w-4/5 rounded bg-muted" /> className="h-10 w-full animate-pulse rounded-lg border border-border/5 bg-muted/20"
<div className="mt-2 h-3 w-2/5 rounded bg-muted" /> />
</div>
))} ))}
</div> </div>
) : entries.length === 0 ? ( ) : 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"> <div className="flex h-full animate-in fade-in duration-1000 flex-col items-center justify-center gap-6 px-8 text-center">
<Inbox className="h-8 w-8" /> <div className="relative">
<div> <div className="absolute inset-0 animate-ping rounded-full bg-muted/5 p-12" />
<p className="font-medium text-foreground">No entries matched these filters.</p> <div className="relative rounded-full border border-dashed border-border/40 bg-muted/5 p-10">
<p className="text-sm">Try broadening the source, severity, or search terms.</p> <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>
</div> </div>
) : ( ) : (
<ScrollArea className="h-[42rem]"> <ScrollArea className="h-full">
<div className="space-y-2 px-3 pb-3"> <div className="flex flex-col">
{entries.map((entry) => { {entries.map((entry) => {
const isSelected = entry.id === selectedEntryId; const isSelected = entry.id === selectedEntryId;
@@ -65,51 +102,114 @@ export function LogsEntryList({
type="button" type="button"
onClick={() => onSelect(entry.id)} onClick={() => onSelect(entry.id)}
className={cn( 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 isSelected
? 'border-primary/40 bg-primary/5 shadow-sm' ? 'z-10 bg-primary/[0.08] shadow-[inset_4px_0_0_rgba(var(--primary),1)]'
: 'border-border/70 bg-background/70 hover:border-primary/20 hover:bg-muted/30' : 'bg-transparent hover:bg-muted/30'
)} )}
> >
<div className="grid gap-3 xl:grid-cols-[7.5rem_minmax(0,1fr)] xl:items-start"> <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="space-y-2">
<p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground"> <div className="flex w-full items-center gap-0">
{formatRelativeLogTime(entry.timestamp)} <div className="flex w-[6.5rem] shrink-0 items-center">
</p> <p
<p className="text-xs text-muted-foreground"> className={cn(
{formatLogTimestamp(entry.timestamp)} '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> </p>
</div> </div>
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2"> <div className="flex min-w-0 flex-1 items-center gap-0">
<LogLevelBadge level={entry.level} /> <div className="flex w-14 shrink-0 items-center justify-center opacity-80 transition-opacity group-hover:opacity-100">
<span className="text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground"> <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} {sourceLabels[entry.source] ?? entry.source}
</span> </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> </div>
<p className="line-clamp-2 text-sm font-medium leading-6 text-foreground">
<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} {entry.message}
</p> </p>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground"> </div>
<span>PID {entry.processId ?? 'n/a'}</span>
<span>Run {entry.runId ?? 'n/a'}</span> <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>
</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> </div>
) : null}
</button> </button>
); );
})} })}
</div> </div>
</ScrollArea> </ScrollArea>
)} )}
</CardContent> </div>
</Card>
<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>
); );
} }
+94 -56
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 { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import type { LogsSource } from '@/lib/api-client'; import type { LogsSource } from '@/lib/api-client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { LogsLevelFilter, LogsSourceFilter } from '@/hooks/use-logs'; import type { LogsLevelFilter, LogsSourceFilter } from '@/hooks/use-logs';
import { getLogLevelOptions, getSelectedSourceLabel } from '@/hooks/use-logs'; import { getLogLevelOptions } from '@/hooks/use-logs';
export function LogsFilters({ export function LogsFilters({
sources, sources,
@@ -33,67 +32,76 @@ export function LogsFilters({
onRefresh: () => void; onRefresh: () => void;
isRefreshing: boolean; isRefreshing: boolean;
}) { }) {
const sourceLabel = getSelectedSourceLabel(selectedSource, sources);
const levels = getLogLevelOptions(); const levels = getLogLevelOptions();
const limits = [50, 100, 150, 250]; const limits = [50, 100, 150, 250];
return ( return (
<Card className="gap-4 border-border/70 bg-card/85 shadow-sm"> <div className="flex flex-col gap-6 animate-in fade-in slide-in-from-left-2 duration-700">
<CardHeader className="space-y-3 border-b pb-4"> <div className="space-y-3">
<div className="flex items-center justify-between px-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SlidersHorizontal className="h-4 w-4 text-muted-foreground" /> <div className="h-1.5 w-1.5 rounded-full bg-primary shadow-[0_0_8px_rgba(var(--primary),0.5)]" />
<CardTitle>Explorer filters</CardTitle> <Label
htmlFor="logs-search"
className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground/80"
>
Payload Search
</Label>
</div> </div>
<div className="space-y-1"> <Zap className="h-3 w-3 text-primary/20" />
<CardDescription> </div>
Narrow the stream the same way you would in a management console: start with source, <div className="group relative">
then severity, then message text. <div className="pointer-events-none absolute inset-y-0 left-3.5 flex items-center transition-all group-focus-within:translate-x-1">
</CardDescription> <Search className="h-3.5 w-3.5 text-foreground/20 group-focus-within:text-primary transition-colors" />
</div> </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 <Input
id="logs-search" id="logs-search"
aria-label="Search"
value={search} value={search}
onChange={(event) => onSearchChange(event.target.value)} onChange={(event) => onSearchChange(event.target.value)}
placeholder="Search message, event, process, or run ID" placeholder="Scan for patterns..."
className="h-11 border-border/70 bg-background/70 pl-9" 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> </div>
<div className="space-y-2"> <div className="space-y-4">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center gap-2 px-1">
<Label>Source</Label> <Filter className="h-3 w-3 text-primary/40" />
<span className="text-xs text-muted-foreground">{sourceLabel}</span> <Label className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground/80">
Source Matrix
</Label>
</div> </div>
<div className="flex flex-wrap gap-2" aria-label="Source filter"> <div className="flex flex-col gap-1" aria-label="Source filter">
<button <button
type="button" type="button"
onClick={() => onSourceChange('all')} onClick={() => onSourceChange('all')}
className={cn( className={cn(
'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors', 'group relative flex items-center justify-between rounded-lg border px-3 py-2 transition-all active:scale-[0.98]',
selectedSource === 'all' selectedSource === 'all'
? 'border-primary/40 bg-primary/10 text-primary' ? 'border-primary/50 bg-primary/10 text-primary shadow-[0_0_15px_rgba(var(--primary),0.1)]'
: 'border-border/70 bg-background/70 text-muted-foreground hover:border-primary/30 hover:text-foreground' : 'border-border/40 bg-muted/20 text-foreground/40 hover:border-border hover:bg-muted/40 hover:text-foreground/80'
)} )}
> >
All sources <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> </button>
<div className="grid grid-cols-2 gap-1 mt-1">
{sources.map((source) => ( {sources.map((source) => (
<button <button
key={source.source} key={source.source}
type="button" type="button"
onClick={() => onSourceChange(source.source)} onClick={() => onSourceChange(source.source)}
className={cn( 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 selectedSource === source.source
? 'border-primary/40 bg-primary/10 text-primary' ? 'border-primary/50 bg-primary/10 text-primary shadow-sm'
: 'border-border/70 bg-background/70 text-muted-foreground hover:border-primary/30 hover:text-foreground' : 'border-border/40 bg-muted/20 text-foreground/40 hover:border-border hover:bg-muted/40 hover:text-foreground/80'
)} )}
> >
{source.label} {source.label}
@@ -101,60 +109,90 @@ export function LogsFilters({
))} ))}
</div> </div>
</div> </div>
</div>
<div className="space-y-2"> {/* Threshold Control */}
<Label>Level</Label> <div className="space-y-4">
<div className="flex flex-wrap gap-2" aria-label="Level filter"> <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) => ( {levels.map((option) => (
<button <button
key={option.value} key={option.value}
type="button" type="button"
onClick={() => onLevelChange(option.value)} onClick={() => onLevelChange(option.value)}
className={cn( className={cn(
'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors', '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 selectedLevel === option.value
? 'border-primary/40 bg-primary/10 text-primary' ? 'border-primary/50 bg-primary/10 text-primary shadow-[0_0_15px_rgba(var(--primary),0.1)]'
: 'border-border/70 bg-background/70 text-muted-foreground hover:border-primary/30 hover:text-foreground' : 'border-border/40 bg-muted/20 text-foreground/40 hover:border-border hover:bg-muted/40 hover:text-foreground/80'
)} )}
> >
{option.label} <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> </button>
))} ))}
</div> </div>
</div> </div>
<div className="space-y-3 rounded-2xl border border-border/70 bg-background/70 p-4"> {/* Operational Deck */}
<div className="space-y-1"> <div className="mt-4 rounded-2xl border-2 border-border bg-card/40 p-1.5 shadow-xl shadow-black/5">
<p className="text-xs font-medium uppercase tracking-[0.24em] text-muted-foreground"> <div className="rounded-[calc(1rem-2px)] border border-dashed border-border bg-background/60 p-4 space-y-5">
View window <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>
<p className="text-sm text-muted-foreground"> <p className="text-[11px] font-medium uppercase tracking-[0.1em] text-foreground/45">
Keep the rendered slice tight when you are chasing a live issue. Tail Capacity
</p> </p>
</div> </div>
<div className="flex flex-wrap gap-2" aria-label="Visible entries"> <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="grid grid-cols-4 gap-1.5" aria-label="Visible entries">
{limits.map((option) => ( {limits.map((option) => (
<button <button
key={option} key={option}
type="button" type="button"
onClick={() => onLimitChange(option)} onClick={() => onLimitChange(option)}
className={cn( 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 limit === option
? 'border-primary/40 bg-primary/10 text-primary' ? 'border-foreground bg-foreground text-background'
: 'border-border/70 bg-background text-muted-foreground hover:border-primary/30 hover:text-foreground' : 'border-border/60 bg-muted/40 text-foreground/40 hover:bg-muted hover:text-foreground'
)} )}
> >
{option} entries {option}
</button> </button>
))} ))}
</div> </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'} /> <Button
Refresh stream 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> </Button>
</div> </div>
</CardContent> </div>
</Card> </div>
); );
} }
+47
View File
@@ -374,3 +374,50 @@
.scrollbar-editor::-webkit-scrollbar-corner { .scrollbar-editor::-webkit-scrollbar-corner {
background: transparent; 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;
}
+372 -104
View File
@@ -1,41 +1,93 @@
import { useEffect, useState } from 'react';
import { import {
AlertCircle,
ArrowRight, ArrowRight,
ChevronLeft,
ChevronRight,
RefreshCw, RefreshCw,
ScrollText, ScrollText,
ShieldAlert,
TimerReset, TimerReset,
} from 'lucide-react'; } from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { cn } from '@/lib/utils';
import { ErrorLogsMonitor } from '@/components/error-logs-monitor'; import { ErrorLogsMonitor } from '@/components/error-logs-monitor';
import { LogsConfigCard } from '@/components/logs/logs-config-card'; import { LogsConfigCard } from '@/components/logs/logs-config-card';
import { LogsDetailPanel } from '@/components/logs/logs-detail-panel'; import { LogsDetailPanel } from '@/components/logs/logs-detail-panel';
import { LogsEntryList } from '@/components/logs/logs-entry-list'; import { LogsEntryList } from '@/components/logs/logs-entry-list';
import { LogsFilters } from '@/components/logs/logs-filters'; import { LogsFilters } from '@/components/logs/logs-filters';
import { LogsOverviewCards } from '@/components/logs/logs-overview-cards';
import { LogsPageSkeleton } from '@/components/logs/logs-page-skeleton'; import { LogsPageSkeleton } from '@/components/logs/logs-page-skeleton';
import { getSourceLabelMap, useLogsWorkspace, useUpdateLogsConfig } from '@/hooks/use-logs'; 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() { export function LogsPage() {
const workspace = useLogsWorkspace(); const workspace = useLogsWorkspace();
const updateConfig = useUpdateLogsConfig(); const updateConfig = useUpdateLogsConfig();
const sourceLabels = getSourceLabelMap(workspace.sourcesQuery.data ?? []); const sourceLabels = getSourceLabelMap(workspace.sourcesQuery.data ?? []);
const errors = [ const [isDesktopLayout, setIsDesktopLayout] = useState(() =>
workspace.configQuery.error, typeof window !== 'undefined' ? window.innerWidth >= DESKTOP_LOGS_BREAKPOINT : false
workspace.sourcesQuery.error, );
workspace.entriesQuery.error, const [isFiltersCollapsed, setIsFiltersCollapsed] = useState(false);
].filter(Boolean) as Error[]; 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) { if (workspace.isInitialLoading) {
return ( return <LogsPageSkeleton />;
<div className="p-6 max-w-7xl mx-auto">
<LogsPageSkeleton />
</div>
);
} }
const config = workspace.configQuery.data; const config = workspace.configQuery.data;
@@ -44,110 +96,278 @@ export function LogsPage() {
} }
return ( return (
<div className="p-6 space-y-5"> <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="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 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="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 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>
<div className="relative flex flex-col gap-5 xl:flex-row xl:items-end xl:justify-between"> <div className="flex flex-col gap-0.5">
<div className="space-y-3"> <div className="flex items-center gap-2.5">
<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"> <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">
<ScrollText className="h-3.5 w-3.5" /> Operational
System logs </span>
</div> <h1 className="text-[17px] font-semibold tracking-tight text-foreground">
<div className="space-y-2"> Log Operations Center
<h1 className="text-3xl font-semibold tracking-[-0.03em] text-foreground md:text-4xl">
Operational log workspace
</h1> </h1>
<p className="max-w-3xl text-sm leading-6 text-muted-foreground md:text-[15px]"> </div>
Work through the CCS event stream like a control center: source-first filtering, <p className="font-mono text-[10px] font-medium uppercase tracking-[0.16em] text-foreground/45">
dense entry inspection, and retention controls beside the live activity feed. CCS.TOC.LOGS.STREAM.v3
</p> </p>
</div> </div>
</div> </div>
<div className="grid gap-3 sm:grid-cols-3 xl:min-w-[26rem]"> <div className="hidden h-8 w-px bg-border/80 md:block" />
<Card className="border-border/70 bg-background/70 shadow-none">
<CardContent className="space-y-1 p-4"> <div className="hidden items-center gap-10 md:flex">
<div className="inline-flex items-center gap-2 text-[11px] font-medium uppercase tracking-[0.22em] text-muted-foreground"> <div className="flex items-center gap-3">
<ShieldAlert className="h-3.5 w-3.5" /> <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 Redaction
</span>
<span className="text-[11px] font-medium text-foreground/50">
{config.redact ? 'Enforced' : 'Standby'}
</span>
</div> </div>
<p className="text-lg font-semibold text-foreground"> </div>
{config.redact ? 'Active' : 'Disabled'} <div className="flex items-center gap-3">
</p> <div className="flex h-7 w-7 items-center justify-center rounded-lg border-2 border-border bg-muted shadow-inner">
<p className="text-xs text-muted-foreground"> <TimerReset className="h-3.5 w-3.5 text-foreground" />
Sensitive fields are masked before persistence. </div>
</p> <div className="flex flex-col">
</CardContent> <span className="text-[11px] font-semibold uppercase tracking-[0.14em] text-foreground/90">
</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" />
Retention Retention
</span>
<span className="text-[11px] font-medium text-foreground/50">
{config.retain_days}D / {config.rotate_mb}MB
</span>
</div> </div>
<p className="text-lg font-semibold text-foreground">{config.retain_days} days</p> </div>
<p className="text-xs text-muted-foreground"> </div>
Rotation threshold {config.rotate_mb} MB. </div>
</p>
</CardContent> <div className="flex items-center gap-4">
</Card>
<div className="flex flex-wrap items-center gap-2 xl:justify-end">
<Button <Button
variant="outline" variant="ghost"
className="gap-2 bg-background/70" 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={() => onClick={() =>
void Promise.all([ void Promise.all([workspace.sourcesQuery.refetch(), workspace.entriesQuery.refetch()])
workspace.sourcesQuery.refetch(),
workspace.entriesQuery.refetch(),
])
} }
> >
<div className="relative">
<RefreshCw <RefreshCw
className={ className={cn(
workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching 'h-3.5 w-3.5 transition-transform duration-500',
? 'h-4 w-4 animate-spin' (workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching) &&
: 'h-4 w-4' 'animate-spin'
} )}
/> />
Refresh {!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> </Button>
<Button asChild variant="ghost" className="gap-2"> <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"> <Link to="/health">
Health
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
</Link> </Link>
</Button> </Button>
</div> </div>
</div> </div>
</div>
</div>
{errors.length > 0 ? ( <div className="relative z-10 flex min-h-0 flex-1 overflow-hidden">
<Alert variant="destructive"> <Tabs defaultValue="stream" className="flex flex-1 flex-col overflow-hidden">
<AlertCircle className="h-4 w-4" /> <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">
<AlertTitle>Unable to fully load the logs workspace</AlertTitle> <TabsList className="h-10 w-auto gap-1.5 rounded-xl border border-border/60 bg-muted/40 p-1">
<AlertDescription>{errors[0]?.message}</AlertDescription> <TabsTrigger
</Alert> value="stream"
) : null} 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"
>
<LogsOverviewCards Telemetry Stream
config={config} </TabsTrigger>
sources={workspace.sourcesQuery.data ?? []} <TabsTrigger
entries={workspace.entriesQuery.data ?? []} value="errors"
latestTimestamp={workspace.latestTimestamp} 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
<Tabs defaultValue="stream" className="space-y-5"> </TabsTrigger>
<TabsList className="grid w-full max-w-md grid-cols-2">
<TabsTrigger value="stream">Activity stream</TabsTrigger>
<TabsTrigger value="errors">CLIProxy errors</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="stream" className="space-y-5"> <div className="hidden items-center gap-3 lg:flex">
<div className="grid gap-5 xl:grid-cols-[18rem_minmax(0,1.2fr)_minmax(18rem,0.8fr)]"> <div className="flex items-center gap-2 rounded-full border border-border bg-muted px-3 py-1 shadow-inner">
<div className="space-y-5 xl:sticky xl:top-5 xl:self-start"> <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>
</div>
<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 <LogsFilters
sources={workspace.sourcesQuery.data ?? []} sources={workspace.sourcesQuery.data ?? []}
selectedSource={workspace.selectedSource} selectedSource={workspace.selectedSource}
@@ -174,7 +394,9 @@ export function LogsPage() {
isPending={updateConfig.isPending} isPending={updateConfig.isPending}
/> />
</div> </div>
</div>
<div className="flex min-h-[32rem] flex-col overflow-hidden border-b border-border bg-background/95">
<LogsEntryList <LogsEntryList
entries={workspace.entriesQuery.data ?? []} entries={workspace.entriesQuery.data ?? []}
selectedEntryId={workspace.selectedEntryId} selectedEntryId={workspace.selectedEntryId}
@@ -183,34 +405,80 @@ export function LogsPage() {
isLoading={workspace.entriesQuery.isLoading} isLoading={workspace.entriesQuery.isLoading}
isFetching={workspace.entriesQuery.isFetching} isFetching={workspace.entriesQuery.isFetching}
/> />
</div>
<div className="flex min-h-[30rem] flex-col overflow-hidden bg-muted/5 shadow-inner">
<LogsDetailPanel <LogsDetailPanel
entry={workspace.selectedEntry} entry={workspace.selectedEntry}
sourceLabel={ sourceLabel={
workspace.selectedEntry ? sourceLabels[workspace.selectedEntry.source] : undefined workspace.selectedEntry
? sourceLabels[workspace.selectedEntry.source]
: undefined
} }
/> />
</div> </div>
</div>
)}
</TabsContent> </TabsContent>
<TabsContent value="errors" className="space-y-4"> <TabsContent
<Card className="border-border/70 bg-card/80 shadow-sm"> value="errors"
<CardContent className="flex flex-col gap-2 p-5"> className="m-0 flex-1 overflow-y-auto bg-background/20 p-6 focus-visible:outline-none xl:p-8"
<p className="text-[11px] font-medium uppercase tracking-[0.22em] text-muted-foreground"> >
Legacy diagnostics <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>
<h2 className="text-xl font-semibold tracking-tight text-foreground"> <p className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground/40">
CLIProxy request-failure viewer 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> </h2>
<p className="max-w-3xl text-sm leading-6 text-muted-foreground"> <p className="max-w-3xl text-[15px] font-medium leading-relaxed text-muted-foreground/60">
Keep the existing request-failure drilldown nearby while the structured CCS stream Maintain oversight of legacy request failures while the unified stream
matures. This tab preserves the old operational workflow instead of burying it consolidates system-wide telemetry. This view provides direct access to the
behind Home-only UI. historical failure matrix for deep-field debugging.
</p> </p>
</div>
<div className="h-px w-full bg-gradient-to-r from-transparent via-border to-transparent opacity-40" />
</CardContent> </CardContent>
</Card> </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 /> <ErrorLogsMonitor />
</div>
</div>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>
</div>
); );
} }
+8 -3
View File
@@ -131,6 +131,11 @@ describe('LogsPage', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
global.fetch = fetchMock; global.fetch = fetchMock;
Object.defineProperty(window, 'innerWidth', {
configurable: true,
writable: true,
value: 900,
});
}); });
it('shows the loading skeleton while the initial queries are pending', () => { 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 })); await userEvent.click(screen.getByRole('button', { name: /Worker retry scheduled/i }));
expect((await screen.findAllByText('task.retry')).length).toBeGreaterThan(0); 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 })); await userEvent.click(screen.getByRole('tab', { name: /Raw context/i }));
expect(await screen.findByText(/"reason": "network jitter"/)).toBeInTheDocument(); expect(await screen.findByText(/network jitter/)).toBeInTheDocument();
expect(screen.getByText(/"runId": "run-2"/)).toBeInTheDocument(); expect(screen.getByText(/run-2/)).toBeInTheDocument();
}); });
}); });