@kopai/ui 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -0
- package/dist/index.cjs +5069 -3
- package/dist/index.d.cts +301 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +302 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5010 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +25 -7
- package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +113 -0
- package/src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx +82 -0
- package/src/components/KeyboardShortcuts/context.ts +23 -0
- package/src/components/KeyboardShortcuts/index.ts +8 -0
- package/src/components/KeyboardShortcuts/types.ts +11 -0
- package/src/components/dashboard/Badge/Badge.stories.tsx +29 -0
- package/src/components/dashboard/Badge/index.tsx +32 -0
- package/src/components/dashboard/Button/Button.stories.tsx +107 -0
- package/src/components/dashboard/Button/index.tsx +63 -0
- package/src/components/dashboard/Card/Card.stories.tsx +81 -0
- package/src/components/dashboard/Card/index.tsx +58 -0
- package/src/components/dashboard/Chart/Chart.stories.tsx +48 -0
- package/src/components/dashboard/Chart/index.tsx +74 -0
- package/src/components/dashboard/DatePicker/DatePicker.stories.tsx +33 -0
- package/src/components/dashboard/DatePicker/index.tsx +41 -0
- package/src/components/dashboard/Divider/Divider.stories.tsx +17 -0
- package/src/components/dashboard/Divider/index.tsx +49 -0
- package/src/components/dashboard/Empty/Empty.stories.tsx +48 -0
- package/src/components/dashboard/Empty/index.tsx +46 -0
- package/src/components/dashboard/Grid/Grid.stories.tsx +52 -0
- package/src/components/dashboard/Grid/index.tsx +26 -0
- package/src/components/dashboard/Heading/Heading.stories.tsx +25 -0
- package/src/components/dashboard/Heading/index.tsx +27 -0
- package/src/components/dashboard/List/List.stories.tsx +37 -0
- package/src/components/dashboard/List/index.tsx +24 -0
- package/src/components/dashboard/Metric/Metric.stories.tsx +65 -0
- package/src/components/dashboard/Metric/index.tsx +36 -0
- package/src/components/dashboard/Stack/Stack.stories.tsx +61 -0
- package/src/components/dashboard/Stack/index.tsx +33 -0
- package/src/components/dashboard/Table/Table.stories.tsx +38 -0
- package/src/components/dashboard/Table/index.tsx +104 -0
- package/src/components/dashboard/Text/Text.stories.tsx +53 -0
- package/src/components/dashboard/Text/index.tsx +18 -0
- package/src/components/dashboard/index.ts +46 -0
- package/src/components/index.ts +17 -0
- package/src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx +56 -0
- package/src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx +139 -0
- package/src/components/observability/LogTimeline/LogDetailPane/index.tsx +271 -0
- package/src/components/observability/LogTimeline/LogFilter.stories.tsx +66 -0
- package/src/components/observability/LogTimeline/LogFilter.test.tsx +696 -0
- package/src/components/observability/LogTimeline/LogFilter.tsx +674 -0
- package/src/components/observability/LogTimeline/LogRow.tsx +174 -0
- package/src/components/observability/LogTimeline/LogTimeline.stories.tsx +154 -0
- package/src/components/observability/LogTimeline/index.tsx +542 -0
- package/src/components/observability/LogTimeline/shortcuts.ts +18 -0
- package/src/components/observability/MetricHistogram/MetricHistogram.stories.tsx +20 -0
- package/src/components/observability/MetricHistogram/index.tsx +303 -0
- package/src/components/observability/MetricStat/MetricStat.stories.tsx +30 -0
- package/src/components/observability/MetricStat/index.tsx +281 -0
- package/src/components/observability/MetricTable/MetricTable.stories.tsx +20 -0
- package/src/components/observability/MetricTable/index.tsx +194 -0
- package/src/components/observability/MetricTimeSeries/MetricTimeSeries.stories.tsx +28 -0
- package/src/components/observability/MetricTimeSeries/index.tsx +462 -0
- package/src/components/observability/RawDataTable/RawDataTable.stories.tsx +27 -0
- package/src/components/observability/RawDataTable/index.tsx +131 -0
- package/src/components/observability/ServiceList/ServiceList.stories.tsx +20 -0
- package/src/components/observability/ServiceList/index.tsx +60 -0
- package/src/components/observability/ServiceList/shortcuts.ts +6 -0
- package/src/components/observability/TabBar/TabBar.stories.tsx +34 -0
- package/src/components/observability/TabBar/index.tsx +46 -0
- package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +51 -0
- package/src/components/observability/TraceDetail/index.tsx +53 -0
- package/src/components/observability/TraceSearch/TraceSearch.stories.tsx +49 -0
- package/src/components/observability/TraceSearch/index.tsx +292 -0
- package/src/components/observability/TraceTimeline/DetailPane/AttributesTab.tsx +152 -0
- package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +128 -0
- package/src/components/observability/TraceTimeline/DetailPane/LinksTab.tsx +210 -0
- package/src/components/observability/TraceTimeline/DetailPane/index.tsx +174 -0
- package/src/components/observability/TraceTimeline/SpanRow.tsx +173 -0
- package/src/components/observability/TraceTimeline/TimelineBar.tsx +41 -0
- package/src/components/observability/TraceTimeline/Tooltip.tsx +42 -0
- package/src/components/observability/TraceTimeline/TraceHeader.tsx +88 -0
- package/src/components/observability/TraceTimeline/TraceTimeline.stories.tsx +25 -0
- package/src/components/observability/TraceTimeline/index.tsx +478 -0
- package/src/components/observability/TraceTimeline/shortcuts.ts +16 -0
- package/src/components/observability/__fixtures__/logs.ts +476 -0
- package/src/components/observability/__fixtures__/metrics.ts +216 -0
- package/src/components/observability/__fixtures__/raw-table.ts +204 -0
- package/src/components/observability/__fixtures__/services.ts +8 -0
- package/src/components/observability/__fixtures__/trace-summaries.ts +81 -0
- package/src/components/observability/__fixtures__/traces.ts +396 -0
- package/src/components/observability/index.ts +66 -0
- package/src/components/observability/renderers/OtelMetricDiscovery.tsx +77 -0
- package/src/components/observability/renderers/OtelMetricHistogram.tsx +29 -0
- package/src/components/observability/renderers/OtelMetricStat.tsx +44 -0
- package/src/components/observability/renderers/OtelMetricTable.tsx +29 -0
- package/src/components/observability/renderers/OtelMetricTimeSeries.tsx +30 -0
- package/src/components/observability/renderers/index.ts +5 -0
- package/src/components/observability/shared/LoadingSkeleton.tsx +43 -0
- package/src/components/observability/types.ts +113 -0
- package/src/components/observability/utils/attributes.ts +17 -0
- package/src/components/observability/utils/colors.ts +29 -0
- package/src/components/observability/utils/flatten-tree.ts +53 -0
- package/src/components/observability/utils/lttb.ts +121 -0
- package/src/components/observability/utils/time.ts +46 -0
- package/src/hooks/use-kopai-data.test.ts +296 -0
- package/src/hooks/use-kopai-data.ts +64 -0
- package/src/hooks/use-live-logs.test.ts +193 -0
- package/src/hooks/use-live-logs.ts +113 -0
- package/src/index.ts +15 -0
- package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +33 -0
- package/src/lib/catalog.ts +165 -0
- package/src/lib/component-catalog.test.ts +357 -0
- package/src/lib/component-catalog.ts +171 -0
- package/src/lib/dashboard-datasource.ts +76 -0
- package/src/lib/generate-prompt-instructions.test.ts +27 -0
- package/src/lib/generate-prompt-instructions.ts +185 -0
- package/src/lib/log-buffer.test.ts +88 -0
- package/src/lib/log-buffer.ts +62 -0
- package/src/lib/observability-catalog.ts +143 -0
- package/src/lib/renderer.test.tsx +693 -0
- package/src/lib/renderer.tsx +276 -0
- package/src/pages/observability.tsx +828 -0
- package/src/providers/kopai-provider.tsx +51 -0
- package/src/styles/globals.css +46 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { formatDuration, formatTimestamp } from "../utils/time.js";
|
|
3
|
+
import { getServiceColor } from "../utils/colors.js";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Public types
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export interface TraceSummary {
|
|
10
|
+
traceId: string;
|
|
11
|
+
rootSpanName: string;
|
|
12
|
+
serviceName: string;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
statusCode: string;
|
|
15
|
+
timestampMs: number;
|
|
16
|
+
spanCount: number;
|
|
17
|
+
services: { name: string; count: number; hasError: boolean }[];
|
|
18
|
+
errorCount: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TraceSearchFilters {
|
|
22
|
+
operation?: string;
|
|
23
|
+
lookbackMs?: number;
|
|
24
|
+
minDuration?: string;
|
|
25
|
+
maxDuration?: string;
|
|
26
|
+
limit: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TraceSearchProps {
|
|
30
|
+
service: string;
|
|
31
|
+
traces: TraceSummary[];
|
|
32
|
+
operations?: string[];
|
|
33
|
+
isLoading?: boolean;
|
|
34
|
+
error?: Error;
|
|
35
|
+
onSelectTrace: (traceId: string) => void;
|
|
36
|
+
onBack: () => void;
|
|
37
|
+
onSearch?: (filters: TraceSearchFilters) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Lookback presets
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const LOOKBACK_OPTIONS = [
|
|
45
|
+
{ label: "Last 5 Minutes", ms: 5 * 60_000 },
|
|
46
|
+
{ label: "Last 15 Minutes", ms: 15 * 60_000 },
|
|
47
|
+
{ label: "Last 30 Minutes", ms: 30 * 60_000 },
|
|
48
|
+
{ label: "Last 1 Hour", ms: 60 * 60_000 },
|
|
49
|
+
{ label: "Last 2 Hours", ms: 2 * 60 * 60_000 },
|
|
50
|
+
{ label: "Last 6 Hours", ms: 6 * 60 * 60_000 },
|
|
51
|
+
{ label: "Last 12 Hours", ms: 12 * 60 * 60_000 },
|
|
52
|
+
{ label: "Last 24 Hours", ms: 24 * 60 * 60_000 },
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Component
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export function TraceSearch({
|
|
60
|
+
service,
|
|
61
|
+
traces,
|
|
62
|
+
operations = [],
|
|
63
|
+
isLoading,
|
|
64
|
+
error,
|
|
65
|
+
onSelectTrace,
|
|
66
|
+
onBack,
|
|
67
|
+
onSearch,
|
|
68
|
+
}: TraceSearchProps) {
|
|
69
|
+
const [operation, setOperation] = useState("all");
|
|
70
|
+
const [lookbackIdx, setLookbackIdx] = useState<number>(-1);
|
|
71
|
+
const [minDuration, setMinDuration] = useState("");
|
|
72
|
+
const [maxDuration, setMaxDuration] = useState("");
|
|
73
|
+
const [limit, setLimit] = useState(20);
|
|
74
|
+
const [filtersOpen, setFiltersOpen] = useState(true);
|
|
75
|
+
|
|
76
|
+
const handleFindTraces = () => {
|
|
77
|
+
onSearch?.({
|
|
78
|
+
operation: operation !== "all" ? operation : undefined,
|
|
79
|
+
lookbackMs:
|
|
80
|
+
lookbackIdx >= 0 ? LOOKBACK_OPTIONS[lookbackIdx]!.ms : undefined,
|
|
81
|
+
minDuration: minDuration || undefined,
|
|
82
|
+
maxDuration: maxDuration || undefined,
|
|
83
|
+
limit,
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div>
|
|
89
|
+
{/* Breadcrumb */}
|
|
90
|
+
<div className="flex items-center gap-1.5 text-sm text-muted-foreground mb-4">
|
|
91
|
+
<button
|
|
92
|
+
onClick={onBack}
|
|
93
|
+
className="hover:text-foreground transition-colors"
|
|
94
|
+
>
|
|
95
|
+
Services
|
|
96
|
+
</button>
|
|
97
|
+
<span>/</span>
|
|
98
|
+
<span className="text-foreground">{service}</span>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Filter panel */}
|
|
102
|
+
{onSearch && (
|
|
103
|
+
<div className="border border-border rounded-lg mb-4">
|
|
104
|
+
<button
|
|
105
|
+
onClick={() => setFiltersOpen((v) => !v)}
|
|
106
|
+
className="w-full flex items-center justify-between px-4 py-2.5 text-sm font-medium text-foreground hover:bg-muted/30 transition-colors"
|
|
107
|
+
>
|
|
108
|
+
<span>Filters</span>
|
|
109
|
+
<span className="text-muted-foreground text-xs">
|
|
110
|
+
{filtersOpen ? "▲" : "▼"}
|
|
111
|
+
</span>
|
|
112
|
+
</button>
|
|
113
|
+
{filtersOpen && (
|
|
114
|
+
<div className="px-4 pb-4 pt-1 border-t border-border space-y-3">
|
|
115
|
+
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
116
|
+
{/* Operation */}
|
|
117
|
+
<label className="space-y-1">
|
|
118
|
+
<span className="text-xs text-muted-foreground">
|
|
119
|
+
Operation
|
|
120
|
+
</span>
|
|
121
|
+
<select
|
|
122
|
+
value={operation}
|
|
123
|
+
onChange={(e) => setOperation(e.target.value)}
|
|
124
|
+
className="w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground"
|
|
125
|
+
>
|
|
126
|
+
<option value="all">All</option>
|
|
127
|
+
{operations.map((op) => (
|
|
128
|
+
<option key={op} value={op}>
|
|
129
|
+
{op}
|
|
130
|
+
</option>
|
|
131
|
+
))}
|
|
132
|
+
</select>
|
|
133
|
+
</label>
|
|
134
|
+
|
|
135
|
+
{/* Lookback */}
|
|
136
|
+
<label className="space-y-1">
|
|
137
|
+
<span className="text-xs text-muted-foreground">
|
|
138
|
+
Lookback
|
|
139
|
+
</span>
|
|
140
|
+
<select
|
|
141
|
+
value={lookbackIdx}
|
|
142
|
+
onChange={(e) => setLookbackIdx(Number(e.target.value))}
|
|
143
|
+
className="w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground"
|
|
144
|
+
>
|
|
145
|
+
<option value={-1}>All time</option>
|
|
146
|
+
{LOOKBACK_OPTIONS.map((opt, i) => (
|
|
147
|
+
<option key={i} value={i}>
|
|
148
|
+
{opt.label}
|
|
149
|
+
</option>
|
|
150
|
+
))}
|
|
151
|
+
</select>
|
|
152
|
+
</label>
|
|
153
|
+
|
|
154
|
+
{/* Limit */}
|
|
155
|
+
<label className="space-y-1">
|
|
156
|
+
<span className="text-xs text-muted-foreground">Limit</span>
|
|
157
|
+
<input
|
|
158
|
+
type="number"
|
|
159
|
+
min={1}
|
|
160
|
+
max={1000}
|
|
161
|
+
value={limit}
|
|
162
|
+
onChange={(e) => {
|
|
163
|
+
const n = Number(e.target.value);
|
|
164
|
+
setLimit(
|
|
165
|
+
Number.isNaN(n) ? 20 : Math.max(1, Math.min(1000, n))
|
|
166
|
+
);
|
|
167
|
+
}}
|
|
168
|
+
className="w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground"
|
|
169
|
+
/>
|
|
170
|
+
</label>
|
|
171
|
+
|
|
172
|
+
{/* Min Duration */}
|
|
173
|
+
<label className="space-y-1">
|
|
174
|
+
<span className="text-xs text-muted-foreground">
|
|
175
|
+
Min Duration
|
|
176
|
+
</span>
|
|
177
|
+
<input
|
|
178
|
+
type="text"
|
|
179
|
+
placeholder="e.g. 100ms"
|
|
180
|
+
value={minDuration}
|
|
181
|
+
onChange={(e) => setMinDuration(e.target.value)}
|
|
182
|
+
className="w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/50"
|
|
183
|
+
/>
|
|
184
|
+
</label>
|
|
185
|
+
|
|
186
|
+
{/* Max Duration */}
|
|
187
|
+
<label className="space-y-1">
|
|
188
|
+
<span className="text-xs text-muted-foreground">
|
|
189
|
+
Max Duration
|
|
190
|
+
</span>
|
|
191
|
+
<input
|
|
192
|
+
type="text"
|
|
193
|
+
placeholder="e.g. 5s"
|
|
194
|
+
value={maxDuration}
|
|
195
|
+
onChange={(e) => setMaxDuration(e.target.value)}
|
|
196
|
+
className="w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/50"
|
|
197
|
+
/>
|
|
198
|
+
</label>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<button
|
|
202
|
+
onClick={handleFindTraces}
|
|
203
|
+
className="px-4 py-1.5 text-sm font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors"
|
|
204
|
+
>
|
|
205
|
+
Find Traces
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Results */}
|
|
213
|
+
{isLoading && (
|
|
214
|
+
<div className="flex items-center gap-2 text-muted-foreground py-8">
|
|
215
|
+
<div className="w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" />
|
|
216
|
+
Loading traces...
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{error && (
|
|
221
|
+
<div className="text-red-400 py-4">
|
|
222
|
+
Error loading traces: {error.message}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{!isLoading && !error && traces.length === 0 && (
|
|
227
|
+
<div className="text-muted-foreground py-8">
|
|
228
|
+
No traces found for {service}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{traces.length > 0 && (
|
|
233
|
+
<div className="space-y-2">
|
|
234
|
+
{traces.map((t) => (
|
|
235
|
+
<div
|
|
236
|
+
key={t.traceId}
|
|
237
|
+
onClick={() => onSelectTrace(t.traceId)}
|
|
238
|
+
className="border border-border rounded-lg px-4 py-3 hover:border-foreground/30 hover:bg-muted/30 cursor-pointer transition-colors"
|
|
239
|
+
>
|
|
240
|
+
{/* Title line */}
|
|
241
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
242
|
+
<div className="flex items-baseline gap-1.5 min-w-0">
|
|
243
|
+
<span className="font-medium text-foreground truncate">
|
|
244
|
+
{t.serviceName}: {t.rootSpanName}
|
|
245
|
+
</span>
|
|
246
|
+
<span className="text-xs font-mono text-muted-foreground shrink-0">
|
|
247
|
+
{t.traceId.slice(0, 7)}
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
<span className="text-sm text-foreground/80 shrink-0">
|
|
251
|
+
{formatDuration(t.durationMs)}
|
|
252
|
+
</span>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Tags line */}
|
|
256
|
+
<div className="flex items-center flex-wrap gap-1.5 mt-1.5">
|
|
257
|
+
<span className="text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
|
|
258
|
+
{t.spanCount} Span{t.spanCount !== 1 ? "s" : ""}
|
|
259
|
+
</span>
|
|
260
|
+
{t.errorCount > 0 && (
|
|
261
|
+
<span className="text-xs px-1.5 py-0.5 rounded bg-red-500/20 text-red-400">
|
|
262
|
+
{t.errorCount} Error{t.errorCount !== 1 ? "s" : ""}
|
|
263
|
+
</span>
|
|
264
|
+
)}
|
|
265
|
+
{t.services.map((svc) => (
|
|
266
|
+
<span
|
|
267
|
+
key={svc.name}
|
|
268
|
+
className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded"
|
|
269
|
+
style={{
|
|
270
|
+
backgroundColor: `${getServiceColor(svc.name)}20`,
|
|
271
|
+
color: getServiceColor(svc.name),
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
{svc.hasError && (
|
|
275
|
+
<span className="w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" />
|
|
276
|
+
)}
|
|
277
|
+
{svc.name} ({svc.count})
|
|
278
|
+
</span>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* Timestamp */}
|
|
283
|
+
<div className="text-xs text-muted-foreground mt-1 text-right">
|
|
284
|
+
{formatTimestamp(t.timestampMs)}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { SpanNode } from "../../types.js";
|
|
3
|
+
import {
|
|
4
|
+
formatAttributeValue,
|
|
5
|
+
isComplexValue,
|
|
6
|
+
} from "../../utils/attributes.js";
|
|
7
|
+
|
|
8
|
+
export interface AttributesTabProps {
|
|
9
|
+
span: SpanNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const HTTP_SEMANTIC_CONVENTIONS = new Set([
|
|
13
|
+
"http.method",
|
|
14
|
+
"http.url",
|
|
15
|
+
"http.status_code",
|
|
16
|
+
"http.target",
|
|
17
|
+
"http.host",
|
|
18
|
+
"http.scheme",
|
|
19
|
+
"http.route",
|
|
20
|
+
"http.user_agent",
|
|
21
|
+
"http.request_content_length",
|
|
22
|
+
"http.response_content_length",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export function AttributesTab({ span }: AttributesTabProps) {
|
|
26
|
+
const { httpAttributes, otherAttributes, resourceAttributes } =
|
|
27
|
+
useMemo(() => {
|
|
28
|
+
const http: Array<[string, unknown]> = [];
|
|
29
|
+
const other: Array<[string, unknown]> = [];
|
|
30
|
+
const resource: Array<[string, unknown]> = [];
|
|
31
|
+
|
|
32
|
+
if (span.attributes) {
|
|
33
|
+
Object.entries(span.attributes).forEach(([key, value]) => {
|
|
34
|
+
if (HTTP_SEMANTIC_CONVENTIONS.has(key)) {
|
|
35
|
+
http.push([key, value]);
|
|
36
|
+
} else {
|
|
37
|
+
other.push([key, value]);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (span.resourceAttributes) {
|
|
43
|
+
Object.entries(span.resourceAttributes).forEach(([key, value]) => {
|
|
44
|
+
resource.push([key, value]);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
http.sort(([a], [b]) => a.localeCompare(b));
|
|
49
|
+
other.sort(([a], [b]) => a.localeCompare(b));
|
|
50
|
+
resource.sort(([a], [b]) => a.localeCompare(b));
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
httpAttributes: http,
|
|
54
|
+
otherAttributes: other,
|
|
55
|
+
resourceAttributes: resource,
|
|
56
|
+
};
|
|
57
|
+
}, [span]);
|
|
58
|
+
|
|
59
|
+
const hasAttributes =
|
|
60
|
+
httpAttributes.length > 0 ||
|
|
61
|
+
otherAttributes.length > 0 ||
|
|
62
|
+
resourceAttributes.length > 0;
|
|
63
|
+
|
|
64
|
+
if (!hasAttributes) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="text-sm text-muted-foreground text-center py-8">
|
|
67
|
+
No attributes available
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="space-y-6">
|
|
74
|
+
{httpAttributes.length > 0 && (
|
|
75
|
+
<section>
|
|
76
|
+
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center">
|
|
77
|
+
<span className="w-2 h-2 bg-blue-500 rounded-full mr-2" />
|
|
78
|
+
HTTP Attributes
|
|
79
|
+
</h3>
|
|
80
|
+
<div className="space-y-2">
|
|
81
|
+
{httpAttributes.map(([key, value]) => (
|
|
82
|
+
<AttributeRow key={key} attrKey={key} value={value} highlighted />
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{otherAttributes.length > 0 && (
|
|
89
|
+
<section>
|
|
90
|
+
<h3 className="text-sm font-semibold text-foreground mb-3">
|
|
91
|
+
Span Attributes
|
|
92
|
+
</h3>
|
|
93
|
+
<div className="space-y-2">
|
|
94
|
+
{otherAttributes.map(([key, value]) => (
|
|
95
|
+
<AttributeRow key={key} attrKey={key} value={value} />
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{resourceAttributes.length > 0 && (
|
|
102
|
+
<section>
|
|
103
|
+
<h3 className="text-sm font-semibold text-foreground mb-3">
|
|
104
|
+
Resource Attributes
|
|
105
|
+
</h3>
|
|
106
|
+
<div className="space-y-2">
|
|
107
|
+
{resourceAttributes.map(([key, value]) => (
|
|
108
|
+
<AttributeRow key={key} attrKey={key} value={value} />
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
</section>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface AttributeRowProps {
|
|
118
|
+
attrKey: string;
|
|
119
|
+
value: unknown;
|
|
120
|
+
highlighted?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function AttributeRow({ attrKey, value, highlighted }: AttributeRowProps) {
|
|
124
|
+
const isComplex = isComplexValue(value);
|
|
125
|
+
const formattedValue = formatAttributeValue(value);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
className={`grid grid-cols-[minmax(150px,1fr)_2fr] gap-4 p-2 rounded text-sm ${
|
|
130
|
+
highlighted
|
|
131
|
+
? "bg-blue-50 dark:bg-blue-950 border-l-2 border-blue-500"
|
|
132
|
+
: "bg-muted"
|
|
133
|
+
}`}
|
|
134
|
+
>
|
|
135
|
+
<div
|
|
136
|
+
className={`font-mono font-medium break-words ${highlighted ? "text-blue-700 dark:text-blue-300" : "text-foreground"}`}
|
|
137
|
+
title={attrKey}
|
|
138
|
+
>
|
|
139
|
+
{attrKey}
|
|
140
|
+
</div>
|
|
141
|
+
<div className="break-words">
|
|
142
|
+
{isComplex ? (
|
|
143
|
+
<pre className="text-xs text-foreground bg-background p-2 rounded border border-border overflow-x-auto">
|
|
144
|
+
{formattedValue}
|
|
145
|
+
</pre>
|
|
146
|
+
) : (
|
|
147
|
+
<span className="text-foreground">{formattedValue}</span>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { SpanNode } from "../../types.js";
|
|
3
|
+
import { formatDuration } from "../../utils/time.js";
|
|
4
|
+
import { formatAttributeValue } from "../../utils/attributes.js";
|
|
5
|
+
|
|
6
|
+
export interface EventsTabProps {
|
|
7
|
+
span: SpanNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatRelativeTime(eventTimeMs: number, spanStartMs: number): string {
|
|
11
|
+
const relativeMs = eventTimeMs - spanStartMs;
|
|
12
|
+
const prefix = relativeMs < 0 ? "-" : "+";
|
|
13
|
+
return `${prefix}${formatDuration(Math.abs(relativeMs))}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function EventsTab({ span }: EventsTabProps) {
|
|
17
|
+
const [expandedEvents, setExpandedEvents] = useState<Set<number>>(new Set());
|
|
18
|
+
|
|
19
|
+
const toggleEventExpanded = (index: number) => {
|
|
20
|
+
setExpandedEvents((prev) => {
|
|
21
|
+
const next = new Set(prev);
|
|
22
|
+
if (next.has(index)) next.delete(index);
|
|
23
|
+
else next.add(index);
|
|
24
|
+
return next;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (!span.events || span.events.length === 0) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="text-sm text-muted-foreground text-center py-8">
|
|
31
|
+
No events available
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="space-y-3">
|
|
38
|
+
{span.events.map((event, index) => {
|
|
39
|
+
const isExpanded = expandedEvents.has(index);
|
|
40
|
+
const hasAttributes =
|
|
41
|
+
event.attributes && Object.keys(event.attributes).length > 0;
|
|
42
|
+
const relativeTime = formatRelativeTime(
|
|
43
|
+
event.timeUnixMs,
|
|
44
|
+
span.startTimeUnixMs
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
key={index}
|
|
50
|
+
className="border border-border rounded-lg overflow-hidden"
|
|
51
|
+
>
|
|
52
|
+
<div className="bg-muted p-3">
|
|
53
|
+
<div className="flex items-start justify-between gap-2">
|
|
54
|
+
<div className="flex-1 min-w-0">
|
|
55
|
+
<div className="font-medium text-sm text-foreground truncate">
|
|
56
|
+
{event.name}
|
|
57
|
+
</div>
|
|
58
|
+
<div className="text-xs text-muted-foreground mt-1 font-mono">
|
|
59
|
+
{relativeTime} from span start
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
{hasAttributes && (
|
|
63
|
+
<button
|
|
64
|
+
onClick={() => toggleEventExpanded(index)}
|
|
65
|
+
className="p-1 hover:bg-muted/80 rounded transition-colors"
|
|
66
|
+
aria-label={
|
|
67
|
+
isExpanded ? "Collapse attributes" : "Expand attributes"
|
|
68
|
+
}
|
|
69
|
+
aria-expanded={isExpanded}
|
|
70
|
+
>
|
|
71
|
+
<svg
|
|
72
|
+
className={`w-4 h-4 text-muted-foreground transition-transform ${isExpanded ? "rotate-180" : ""}`}
|
|
73
|
+
fill="none"
|
|
74
|
+
stroke="currentColor"
|
|
75
|
+
viewBox="0 0 24 24"
|
|
76
|
+
>
|
|
77
|
+
<path
|
|
78
|
+
strokeLinecap="round"
|
|
79
|
+
strokeLinejoin="round"
|
|
80
|
+
strokeWidth={2}
|
|
81
|
+
d="M19 9l-7 7-7-7"
|
|
82
|
+
/>
|
|
83
|
+
</svg>
|
|
84
|
+
</button>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{hasAttributes && isExpanded && (
|
|
90
|
+
<div className="p-3 bg-background border-t border-border">
|
|
91
|
+
<div className="text-xs font-semibold text-foreground mb-2">
|
|
92
|
+
Attributes
|
|
93
|
+
</div>
|
|
94
|
+
<div className="space-y-2">
|
|
95
|
+
{Object.entries(event.attributes).map(([key, value]) => (
|
|
96
|
+
<div
|
|
97
|
+
key={key}
|
|
98
|
+
className="grid grid-cols-[minmax(100px,1fr)_2fr] gap-3 text-xs"
|
|
99
|
+
>
|
|
100
|
+
<div className="font-mono font-medium text-foreground break-words">
|
|
101
|
+
{key}
|
|
102
|
+
</div>
|
|
103
|
+
<div className="text-foreground break-words">
|
|
104
|
+
{typeof value === "object" ? (
|
|
105
|
+
<pre className="text-xs bg-muted p-2 rounded border border-border overflow-x-auto">
|
|
106
|
+
{formatAttributeValue(value)}
|
|
107
|
+
</pre>
|
|
108
|
+
) : (
|
|
109
|
+
<span>{formatAttributeValue(value)}</span>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{!hasAttributes && (
|
|
119
|
+
<div className="px-3 pb-3 text-xs text-muted-foreground italic">
|
|
120
|
+
No attributes
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|