@kopai/ui 0.7.0 → 0.9.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/dist/index.cjs +2451 -1157
- package/dist/index.d.cts +36 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +36 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2399 -1099
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -13
- package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +7 -7
- package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +5 -0
- package/src/components/observability/LogTimeline/LogFilter.tsx +5 -1
- package/src/components/observability/LogTimeline/index.tsx +6 -2
- package/src/components/observability/MetricHistogram/index.tsx +20 -19
- package/src/components/observability/MetricTimeSeries/index.tsx +25 -14
- package/src/components/observability/ServiceList/shortcuts.ts +1 -1
- package/src/components/observability/TraceComparison/index.tsx +332 -0
- package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +0 -4
- package/src/components/observability/TraceDetail/index.tsx +4 -3
- package/src/components/observability/TraceSearch/DurationBar.tsx +38 -0
- package/src/components/observability/TraceSearch/ScatterPlot.tsx +135 -0
- package/src/components/observability/TraceSearch/SearchForm.tsx +195 -0
- package/src/components/observability/TraceSearch/SortDropdown.tsx +32 -0
- package/src/components/observability/TraceSearch/index.tsx +211 -218
- package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +1 -7
- package/src/components/observability/TraceTimeline/FlamegraphView.tsx +232 -0
- package/src/components/observability/TraceTimeline/GraphView.tsx +322 -0
- package/src/components/observability/TraceTimeline/Minimap.tsx +260 -0
- package/src/components/observability/TraceTimeline/SpanDetailInline.tsx +184 -0
- package/src/components/observability/TraceTimeline/SpanRow.tsx +14 -24
- package/src/components/observability/TraceTimeline/SpanSearch.tsx +76 -0
- package/src/components/observability/TraceTimeline/StatisticsView.tsx +223 -0
- package/src/components/observability/TraceTimeline/TimeRuler.tsx +54 -0
- package/src/components/observability/TraceTimeline/TimelineBar.tsx +18 -2
- package/src/components/observability/TraceTimeline/TraceHeader.tsx +116 -51
- package/src/components/observability/TraceTimeline/ViewTabs.tsx +34 -0
- package/src/components/observability/TraceTimeline/index.tsx +254 -110
- package/src/components/observability/index.ts +15 -0
- package/src/components/observability/renderers/OtelLogTimeline.tsx +9 -5
- package/src/components/observability/renderers/OtelTraceDetail.tsx +1 -4
- package/src/components/observability/shared/TooltipEntryList.tsx +25 -0
- package/src/components/observability/utils/flatten-tree.ts +15 -0
- package/src/components/observability/utils/time.ts +9 -0
- package/src/hooks/use-kopai-data.test.ts +4 -0
- package/src/hooks/use-kopai-data.ts +11 -0
- package/src/hooks/use-live-logs.test.ts +4 -0
- package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +1 -1
- package/src/lib/component-catalog.ts +15 -0
- package/src/pages/observability.test.tsx +16 -12
- package/src/pages/observability.tsx +323 -245
- package/src/providers/kopai-provider.tsx +4 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useState, useMemo } from "react";
|
|
2
|
+
import { formatTimestamp } from "../utils/time.js";
|
|
3
3
|
import { getServiceColor } from "../utils/colors.js";
|
|
4
|
+
import { SearchForm } from "./SearchForm.js";
|
|
5
|
+
import type { SearchFormValues } from "./SearchForm.js";
|
|
6
|
+
import { ScatterPlot } from "./ScatterPlot.js";
|
|
7
|
+
import { SortDropdown } from "./SortDropdown.js";
|
|
8
|
+
import { DurationBar } from "./DurationBar.js";
|
|
4
9
|
|
|
5
10
|
// ---------------------------------------------------------------------------
|
|
6
11
|
// Public types
|
|
@@ -19,274 +24,262 @@ export interface TraceSummary {
|
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface TraceSearchFilters {
|
|
27
|
+
service?: string;
|
|
22
28
|
operation?: string;
|
|
23
|
-
|
|
29
|
+
tags?: string;
|
|
30
|
+
lookback?: string;
|
|
24
31
|
minDuration?: string;
|
|
25
32
|
maxDuration?: string;
|
|
26
33
|
limit: number;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export interface TraceSearchProps {
|
|
37
|
+
// Search form data
|
|
38
|
+
services?: string[];
|
|
30
39
|
service: string;
|
|
31
|
-
traces: TraceSummary[];
|
|
32
40
|
operations?: string[];
|
|
41
|
+
// Results
|
|
42
|
+
traces: TraceSummary[];
|
|
33
43
|
isLoading?: boolean;
|
|
34
44
|
error?: Error;
|
|
45
|
+
// Callbacks
|
|
35
46
|
onSelectTrace: (traceId: string) => void;
|
|
36
|
-
onBack: () => void;
|
|
37
47
|
onSearch?: (filters: TraceSearchFilters) => void;
|
|
48
|
+
onCompare?: (traceIds: [string, string]) => void;
|
|
49
|
+
// Sort
|
|
50
|
+
sort?: string;
|
|
51
|
+
onSortChange?: (sort: string) => void;
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
// ---------------------------------------------------------------------------
|
|
41
|
-
//
|
|
55
|
+
// Sort helpers
|
|
42
56
|
// ---------------------------------------------------------------------------
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
function sortTraces(traces: TraceSummary[], sort: string): TraceSummary[] {
|
|
59
|
+
const sorted = [...traces];
|
|
60
|
+
switch (sort) {
|
|
61
|
+
case "longest":
|
|
62
|
+
return sorted.sort((a, b) => b.durationMs - a.durationMs);
|
|
63
|
+
case "shortest":
|
|
64
|
+
return sorted.sort((a, b) => a.durationMs - b.durationMs);
|
|
65
|
+
case "mostSpans":
|
|
66
|
+
return sorted.sort((a, b) => b.spanCount - a.spanCount);
|
|
67
|
+
case "leastSpans":
|
|
68
|
+
return sorted.sort((a, b) => a.spanCount - b.spanCount);
|
|
69
|
+
case "recent":
|
|
70
|
+
default:
|
|
71
|
+
return sorted.sort((a, b) => b.timestampMs - a.timestampMs);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
54
74
|
|
|
55
75
|
// ---------------------------------------------------------------------------
|
|
56
76
|
// Component
|
|
57
77
|
// ---------------------------------------------------------------------------
|
|
58
78
|
|
|
59
79
|
export function TraceSearch({
|
|
80
|
+
services = [],
|
|
60
81
|
service,
|
|
61
|
-
traces,
|
|
62
82
|
operations = [],
|
|
83
|
+
traces,
|
|
63
84
|
isLoading,
|
|
64
85
|
error,
|
|
65
86
|
onSelectTrace,
|
|
66
|
-
onBack,
|
|
67
87
|
onSearch,
|
|
88
|
+
onCompare,
|
|
89
|
+
sort: controlledSort,
|
|
90
|
+
onSortChange,
|
|
68
91
|
}: TraceSearchProps) {
|
|
69
|
-
|
|
70
|
-
const [
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
// Sort state (internal fallback if not controlled)
|
|
93
|
+
const [internalSort, setInternalSort] = useState("recent");
|
|
94
|
+
const currentSort = controlledSort ?? internalSort;
|
|
95
|
+
const handleSortChange = (s: string) => {
|
|
96
|
+
if (onSortChange) onSortChange(s);
|
|
97
|
+
else setInternalSort(s);
|
|
98
|
+
};
|
|
75
99
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
// Comparison state
|
|
101
|
+
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
102
|
+
|
|
103
|
+
const toggleSelected = (traceId: string) => {
|
|
104
|
+
setSelected((prev) => {
|
|
105
|
+
const next = new Set(prev);
|
|
106
|
+
if (next.has(traceId)) {
|
|
107
|
+
next.delete(traceId);
|
|
108
|
+
} else {
|
|
109
|
+
// Max 2 selected
|
|
110
|
+
if (next.size >= 2) return prev;
|
|
111
|
+
next.add(traceId);
|
|
112
|
+
}
|
|
113
|
+
return next;
|
|
84
114
|
});
|
|
85
115
|
};
|
|
86
116
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<span className="text-foreground">{service}</span>
|
|
99
|
-
</div>
|
|
117
|
+
const handleFormSubmit = (values: SearchFormValues) => {
|
|
118
|
+
onSearch?.({
|
|
119
|
+
service: values.service || undefined,
|
|
120
|
+
operation: values.operation || undefined,
|
|
121
|
+
tags: values.tags || undefined,
|
|
122
|
+
lookback: values.lookback || undefined,
|
|
123
|
+
minDuration: values.minDuration || undefined,
|
|
124
|
+
maxDuration: values.maxDuration || undefined,
|
|
125
|
+
limit: values.limit,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
100
128
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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>
|
|
129
|
+
const sortedTraces = useMemo(
|
|
130
|
+
() => sortTraces(traces, currentSort),
|
|
131
|
+
[traces, currentSort]
|
|
132
|
+
);
|
|
134
133
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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>
|
|
134
|
+
const maxDurationMs = useMemo(
|
|
135
|
+
() => Math.max(...traces.map((t) => t.durationMs), 0),
|
|
136
|
+
[traces]
|
|
137
|
+
);
|
|
153
138
|
|
|
154
|
-
|
|
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>
|
|
139
|
+
const selectedArr = Array.from(selected);
|
|
171
140
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
141
|
+
return (
|
|
142
|
+
<div className="flex gap-6 min-h-0">
|
|
143
|
+
{/* Left sidebar */}
|
|
144
|
+
{onSearch && (
|
|
145
|
+
<div className="w-72 shrink-0 border border-border rounded-lg p-4 self-start">
|
|
146
|
+
<SearchForm
|
|
147
|
+
services={services}
|
|
148
|
+
operations={operations}
|
|
149
|
+
initialValues={{ service }}
|
|
150
|
+
onSubmit={handleFormSubmit}
|
|
151
|
+
isLoading={isLoading}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
185
155
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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>
|
|
156
|
+
{/* Right content */}
|
|
157
|
+
<div className="flex-1 min-w-0 space-y-4">
|
|
158
|
+
{/* Scatter plot */}
|
|
159
|
+
{traces.length > 0 && (
|
|
160
|
+
<ScatterPlot traces={traces} onSelectTrace={onSelectTrace} />
|
|
161
|
+
)}
|
|
200
162
|
|
|
163
|
+
{/* Sort bar + result count */}
|
|
164
|
+
<div className="flex items-center justify-between gap-2">
|
|
165
|
+
<span className="text-sm text-muted-foreground">
|
|
166
|
+
{traces.length} Trace{traces.length !== 1 ? "s" : ""}
|
|
167
|
+
</span>
|
|
168
|
+
<div className="flex items-center gap-2">
|
|
169
|
+
{onCompare && selected.size === 2 && (
|
|
201
170
|
<button
|
|
202
|
-
onClick={
|
|
203
|
-
className="px-
|
|
171
|
+
onClick={() => onCompare(selectedArr as [string, string])}
|
|
172
|
+
className="px-3 py-1.5 text-xs font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors"
|
|
204
173
|
>
|
|
205
|
-
|
|
174
|
+
Compare
|
|
206
175
|
</button>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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...
|
|
176
|
+
)}
|
|
177
|
+
<SortDropdown value={currentSort} onChange={handleSortChange} />
|
|
178
|
+
</div>
|
|
217
179
|
</div>
|
|
218
|
-
)}
|
|
219
180
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
181
|
+
{/* Loading */}
|
|
182
|
+
{isLoading && (
|
|
183
|
+
<div className="flex items-center gap-2 text-muted-foreground py-8">
|
|
184
|
+
<div className="w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" />
|
|
185
|
+
Loading traces...
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
225
188
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
189
|
+
{/* Error */}
|
|
190
|
+
{error && (
|
|
191
|
+
<div className="text-red-400 py-4">
|
|
192
|
+
Error loading traces: {error.message}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
231
195
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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>
|
|
196
|
+
{/* Empty */}
|
|
197
|
+
{!isLoading && !error && traces.length === 0 && (
|
|
198
|
+
<div className="text-muted-foreground py-8">No traces found</div>
|
|
199
|
+
)}
|
|
254
200
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
{t.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
201
|
+
{/* Result cards */}
|
|
202
|
+
{sortedTraces.length > 0 && (
|
|
203
|
+
<div className="space-y-2">
|
|
204
|
+
{sortedTraces.map((t) => (
|
|
205
|
+
<div
|
|
206
|
+
key={t.traceId}
|
|
207
|
+
className="border border-border rounded-lg px-4 py-3 hover:border-foreground/30 hover:bg-muted/30 cursor-pointer transition-colors"
|
|
208
|
+
>
|
|
209
|
+
{/* Title line */}
|
|
210
|
+
<div className="flex items-center gap-2">
|
|
211
|
+
{onCompare && (
|
|
212
|
+
<input
|
|
213
|
+
type="checkbox"
|
|
214
|
+
checked={selected.has(t.traceId)}
|
|
215
|
+
onChange={() => toggleSelected(t.traceId)}
|
|
216
|
+
onClick={(e) => e.stopPropagation()}
|
|
217
|
+
className="shrink-0"
|
|
218
|
+
disabled={!selected.has(t.traceId) && selected.size >= 2}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
<div
|
|
222
|
+
className="flex-1 min-w-0"
|
|
223
|
+
onClick={() => onSelectTrace(t.traceId)}
|
|
273
224
|
>
|
|
274
|
-
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
225
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
226
|
+
<div className="flex items-baseline gap-1.5 min-w-0">
|
|
227
|
+
<span className="font-medium text-foreground truncate">
|
|
228
|
+
{t.serviceName}: {t.rootSpanName}
|
|
229
|
+
</span>
|
|
230
|
+
<span className="text-xs font-mono text-muted-foreground shrink-0">
|
|
231
|
+
{t.traceId.slice(0, 7)}
|
|
232
|
+
</span>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Duration bar */}
|
|
237
|
+
<div className="mt-1.5">
|
|
238
|
+
<DurationBar
|
|
239
|
+
durationMs={t.durationMs}
|
|
240
|
+
maxDurationMs={maxDurationMs}
|
|
241
|
+
color={getServiceColor(t.serviceName)}
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
281
244
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
245
|
+
{/* Tags line */}
|
|
246
|
+
<div className="flex items-center flex-wrap gap-1.5 mt-1.5">
|
|
247
|
+
<span className="text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
|
|
248
|
+
{t.spanCount} Span{t.spanCount !== 1 ? "s" : ""}
|
|
249
|
+
</span>
|
|
250
|
+
{t.errorCount > 0 && (
|
|
251
|
+
<span className="text-xs px-1.5 py-0.5 rounded bg-red-500/20 text-red-400">
|
|
252
|
+
{t.errorCount} Error{t.errorCount !== 1 ? "s" : ""}
|
|
253
|
+
</span>
|
|
254
|
+
)}
|
|
255
|
+
{t.services.map((svc) => (
|
|
256
|
+
<span
|
|
257
|
+
key={svc.name}
|
|
258
|
+
className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded"
|
|
259
|
+
style={{
|
|
260
|
+
backgroundColor: `${getServiceColor(svc.name)}20`,
|
|
261
|
+
color: getServiceColor(svc.name),
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
{svc.hasError && (
|
|
265
|
+
<span className="w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" />
|
|
266
|
+
)}
|
|
267
|
+
{svc.name} ({svc.count})
|
|
268
|
+
</span>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Timestamp */}
|
|
273
|
+
<div className="text-xs text-muted-foreground mt-1 text-right">
|
|
274
|
+
{formatTimestamp(t.timestampMs)}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
285
278
|
</div>
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
290
283
|
</div>
|
|
291
284
|
);
|
|
292
285
|
}
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { SpanNode } from "../../types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { formatRelativeTime } from "../../utils/time.js";
|
|
4
4
|
import { formatAttributeValue } from "../../utils/attributes.js";
|
|
5
5
|
|
|
6
6
|
export interface EventsTabProps {
|
|
7
7
|
span: SpanNode;
|
|
8
8
|
}
|
|
9
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
10
|
export function EventsTab({ span }: EventsTabProps) {
|
|
17
11
|
const [expandedEvents, setExpandedEvents] = useState<Set<number>>(new Set());
|
|
18
12
|
|