Skip to content

Commit 3bb5c84

Browse files
committed
feat: redesign trace ui
1 parent 0afcf07 commit 3bb5c84

13 files changed

Lines changed: 1218 additions & 874 deletions

File tree

tools/trace-ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"preview": "vite preview"
1414
},
1515
"dependencies": {
16+
"@fontsource-variable/hanken-grotesk": "^5.3.0",
17+
"@fontsource-variable/jetbrains-mono": "^5.3.0",
1618
"@radix-ui/react-dropdown-menu": "^2.1.24",
1719
"@radix-ui/react-slot": "^1.3.3",
1820
"class-variance-authority": "^0.7.1",

tools/trace-ui/pnpm-lock.yaml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/trace-ui/src/app.tsx

Lines changed: 128 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,79 @@
1-
import { FileText } from "lucide-react";
2-
import { useCallback, useState } from "react";
1+
import { CircleAlert, FileDown, type LucideIcon, Signal } from "lucide-react";
2+
import { useCallback, useMemo, useState } from "react";
33

44
import { FileUpload } from "@/components/file-upload";
55
import { ModeToggle } from "@/components/mode-toggle";
66
import { TraceView } from "@/components/trace-view";
7-
import { type TraceFile, parseTraceLog } from "@/lib/trace";
7+
import { formatDuration } from "@/components/trace-view/span-utils";
8+
import {
9+
type TraceFile,
10+
extractSpansFromEntries,
11+
parseTraceLog,
12+
} from "@/lib/trace";
13+
14+
function useTraceStats(file: TraceFile | null) {
15+
return useMemo(() => {
16+
if (!file) return null;
17+
const spans = extractSpansFromEntries(file.entries);
18+
if (spans.length === 0) {
19+
return { spans: 0, entries: file.entries.length, duration: 0, errors: 0 };
20+
}
21+
const min = Math.min(...spans.map((s) => s.startTime));
22+
const max = Math.max(...spans.map((s) => s.endTime));
23+
return {
24+
spans: spans.length,
25+
entries: file.entries.length,
26+
duration: max - min,
27+
errors: spans.filter((s) => s.status === "ERROR").length,
28+
};
29+
}, [file]);
30+
}
31+
32+
function Stat({
33+
label,
34+
value,
35+
tone = "default",
36+
icon: Icon,
37+
}: {
38+
label: string;
39+
value: string;
40+
tone?: "default" | "error";
41+
icon?: LucideIcon;
42+
}) {
43+
const active = tone === "error" && value !== "0";
44+
return (
45+
<div className="flex flex-col leading-none">
46+
<span className="text-muted-foreground font-mono text-[9px] tracking-[0.18em] uppercase">
47+
{label}
48+
</span>
49+
<span
50+
className={`mt-1 flex items-center gap-1.5 font-mono text-sm font-semibold tabular-nums ${
51+
active ? "text-kind-error" : "text-foreground"
52+
}`}
53+
>
54+
{Icon && (
55+
<Icon
56+
className={`h-3.5 w-3.5 ${active ? "text-kind-error" : "text-muted-foreground/50"}`}
57+
aria-hidden
58+
/>
59+
)}
60+
{value}
61+
</span>
62+
</div>
63+
);
64+
}
865

966
function App() {
1067
const [file, setFile] = useState<TraceFile | null>(null);
1168
const [isDragging, setIsDragging] = useState(false);
69+
const stats = useTraceStats(file);
1270

1371
const handleFileLoaded = useCallback((loadedFile: TraceFile) => {
1472
setFile(loadedFile);
1573
setIsDragging(false);
1674
}, []);
1775

18-
const handleClearFile = useCallback(() => {
19-
setFile(null);
20-
}, []);
76+
const handleClearFile = useCallback(() => setFile(null), []);
2177

2278
const handleDrop = useCallback(
2379
(e: React.DragEvent) => {
@@ -27,26 +83,21 @@ function App() {
2783

2884
const droppedFile = e.dataTransfer.files[0];
2985
if (!droppedFile) return;
30-
31-
// Only handle JSONL files
3286
if (!droppedFile.name.endsWith(".jsonl")) {
3387
console.error("Only .jsonl files are supported");
3488
return;
3589
}
3690

3791
const reader = new FileReader();
38-
reader.onload = (e) => {
92+
reader.onload = (ev) => {
3993
try {
40-
const content = e.target?.result as string;
41-
const traceFile = parseTraceLog(droppedFile.name, content);
42-
handleFileLoaded(traceFile);
94+
const content = ev.target?.result as string;
95+
handleFileLoaded(parseTraceLog(droppedFile.name, content));
4396
} catch (err) {
4497
console.error("Failed to parse trace file:", err);
4598
}
4699
};
47-
reader.onerror = () => {
48-
console.error("Failed to read file");
49-
};
100+
reader.onerror = () => console.error("Failed to read file");
50101
reader.readAsText(droppedFile);
51102
},
52103
[handleFileLoaded],
@@ -61,81 +112,89 @@ function App() {
61112
const handleDragLeave = useCallback((e: React.DragEvent) => {
62113
e.preventDefault();
63114
e.stopPropagation();
64-
// Only set dragging to false if we're leaving the main container
65-
if (e.currentTarget === e.target) {
66-
setIsDragging(false);
67-
}
115+
if (e.currentTarget === e.target) setIsDragging(false);
68116
}, []);
69117

70118
return (
71119
<div
72-
className="relative flex h-screen flex-col bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-950 dark:to-slate-900"
120+
className="instrument-backdrop relative flex h-screen flex-col overflow-hidden"
73121
onDrop={handleDrop}
74122
onDragOver={handleDragOver}
75123
onDragLeave={handleDragLeave}
76124
>
77-
{/* Drag Overlay */}
125+
{/* Drag overlay */}
78126
{isDragging && (
79-
<div className="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-blue-500/10 backdrop-blur-sm">
80-
<div className="rounded-lg border-2 border-dashed border-blue-500 bg-white/90 px-12 py-8 text-center shadow-lg dark:bg-slate-900/90">
81-
<FileText className="mx-auto mb-4 h-16 w-16 text-blue-500" />
82-
<p className="text-xl font-semibold text-slate-900 dark:text-slate-50">
83-
Drop trace file to load
127+
<div className="bg-primary/10 pointer-events-none absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm">
128+
<div className="border-primary bg-card/90 rounded-xl border-2 border-dashed px-14 py-10 text-center shadow-2xl">
129+
<FileDown className="text-primary mx-auto mb-4 h-14 w-14" />
130+
<p className="text-foreground text-lg font-semibold">
131+
Drop trace to load
84132
</p>
85-
<p className="mt-2 text-sm text-slate-600 dark:text-slate-400">
86-
.jsonl files only
133+
<p className="text-muted-foreground mt-1 font-mono text-xs">
134+
.jsonl signal logs only
87135
</p>
88136
</div>
89137
</div>
90138
)}
91139

92-
{/* Header */}
93-
<div className="flex-shrink-0 border-b border-slate-200 dark:border-slate-800">
94-
<div className="px-4 py-4">
95-
<div className="flex min-h-[40px] items-center justify-between gap-4">
96-
<div className="flex items-center gap-4">
97-
<h1
98-
className={`text-xl font-semibold text-slate-900 dark:text-slate-50 ${
99-
file ? "cursor-pointer" : ""
100-
}`}
101-
onClick={file ? handleClearFile : undefined}
102-
>
103-
🌀 Duron
104-
</h1>
105-
{file && (
106-
<>
107-
<div className="h-6 w-px bg-slate-300 dark:bg-slate-700" />
108-
<div className="flex items-center gap-2">
109-
<FileText className="h-4 w-4 text-blue-500" />
110-
<div className="flex items-center gap-2 text-sm">
111-
<span className="font-medium text-slate-900 dark:text-slate-50">
112-
{file.name}
113-
</span>
114-
<span className="text-slate-500 dark:text-slate-400">
115-
{file.entries.length} entries
116-
</span>
117-
</div>
118-
</div>
119-
</>
120-
)}
121-
</div>
122-
<div className="flex items-center gap-2">
123-
<ModeToggle />
124-
</div>
140+
{/* Instrument top bar */}
141+
<header className="border-border bg-surface/60 relative z-10 flex-shrink-0 border-b backdrop-blur">
142+
<div className="flex min-h-[52px] items-center justify-between gap-4 px-4 py-2">
143+
<div className="flex items-center gap-4">
144+
<button
145+
type="button"
146+
onClick={file ? handleClearFile : undefined}
147+
className={`flex items-center gap-2 ${file ? "cursor-pointer" : "cursor-default"}`}
148+
>
149+
<span className="text-lg leading-none" aria-hidden>
150+
🌀
151+
</span>
152+
<span className="text-foreground font-mono text-sm font-bold tracking-[0.16em] uppercase">
153+
Duron
154+
</span>
155+
</button>
156+
157+
{file && (
158+
<>
159+
<div className="bg-border h-7 w-px" />
160+
<div className="flex items-center gap-2">
161+
<Signal className="text-primary h-3.5 w-3.5" aria-hidden />
162+
<span className="text-foreground font-mono text-xs">
163+
{file.name}
164+
</span>
165+
</div>
166+
</>
167+
)}
168+
</div>
169+
170+
<div className="flex items-center gap-5">
171+
{stats && (
172+
<div className="hidden items-center gap-5 sm:flex">
173+
<Stat label="Spans" value={String(stats.spans)} />
174+
<Stat label="Events" value={String(stats.entries)} />
175+
<Stat label="Elapsed" value={formatDuration(stats.duration)} />
176+
<Stat
177+
label="Errors"
178+
value={String(stats.errors)}
179+
tone="error"
180+
icon={CircleAlert}
181+
/>
182+
</div>
183+
)}
184+
<ModeToggle />
125185
</div>
126186
</div>
127-
</div>
187+
</header>
128188

129-
{/* Main Content */}
189+
{/* Body */}
130190
{!file ? (
131-
<div className="flex flex-1 items-center justify-center overflow-auto p-4">
132-
<div className="w-full max-w-2xl space-y-6">
133-
{/* File Upload */}
134-
<FileUpload onFileLoaded={handleFileLoaded} />
135-
</div>
191+
<div className="relative z-10 flex flex-1 items-center justify-center overflow-auto p-4">
192+
<FileUpload onFileLoaded={handleFileLoaded} />
136193
</div>
137194
) : (
138-
<TraceView file={file} />
195+
<div className="relative z-10 flex flex-1 overflow-hidden">
196+
<TraceView file={file} />
197+
</div>
139198
)}
140199
</div>
141200
);

0 commit comments

Comments
 (0)