lopata 0.0.1
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 +15 -0
- package/package.json +51 -0
- package/runtime/bindings/ai.ts +132 -0
- package/runtime/bindings/analytics-engine.ts +96 -0
- package/runtime/bindings/browser.ts +64 -0
- package/runtime/bindings/cache.ts +179 -0
- package/runtime/bindings/cf-streams.ts +56 -0
- package/runtime/bindings/container-docker.ts +225 -0
- package/runtime/bindings/container.ts +662 -0
- package/runtime/bindings/crypto-extras.ts +89 -0
- package/runtime/bindings/d1.ts +315 -0
- package/runtime/bindings/do-executor-inprocess.ts +140 -0
- package/runtime/bindings/do-executor-worker.ts +368 -0
- package/runtime/bindings/do-executor.ts +45 -0
- package/runtime/bindings/do-websocket-bridge.ts +70 -0
- package/runtime/bindings/do-worker-entry.ts +220 -0
- package/runtime/bindings/do-worker-env.ts +74 -0
- package/runtime/bindings/durable-object.ts +992 -0
- package/runtime/bindings/email.ts +180 -0
- package/runtime/bindings/html-rewriter.ts +84 -0
- package/runtime/bindings/hyperdrive.ts +130 -0
- package/runtime/bindings/images.ts +381 -0
- package/runtime/bindings/kv.ts +359 -0
- package/runtime/bindings/queue.ts +507 -0
- package/runtime/bindings/r2.ts +759 -0
- package/runtime/bindings/rpc-stub.ts +267 -0
- package/runtime/bindings/scheduled.ts +172 -0
- package/runtime/bindings/service-binding.ts +217 -0
- package/runtime/bindings/static-assets.ts +481 -0
- package/runtime/bindings/websocket-pair.ts +182 -0
- package/runtime/bindings/workflow.ts +858 -0
- package/runtime/bunflare-config.ts +56 -0
- package/runtime/cli/cache.ts +39 -0
- package/runtime/cli/context.ts +105 -0
- package/runtime/cli/d1.ts +163 -0
- package/runtime/cli/dev.ts +392 -0
- package/runtime/cli/kv.ts +84 -0
- package/runtime/cli/queues.ts +109 -0
- package/runtime/cli/r2.ts +140 -0
- package/runtime/cli/traces.ts +251 -0
- package/runtime/cli.ts +102 -0
- package/runtime/config.ts +148 -0
- package/runtime/d1-migrate.ts +37 -0
- package/runtime/dashboard/api.ts +174 -0
- package/runtime/dashboard/app.tsx +220 -0
- package/runtime/dashboard/components/breadcrumb.tsx +16 -0
- package/runtime/dashboard/components/buttons.tsx +13 -0
- package/runtime/dashboard/components/code-block.tsx +5 -0
- package/runtime/dashboard/components/detail-field.tsx +8 -0
- package/runtime/dashboard/components/empty-state.tsx +8 -0
- package/runtime/dashboard/components/filter-input.tsx +11 -0
- package/runtime/dashboard/components/index.ts +16 -0
- package/runtime/dashboard/components/key-value-table.tsx +23 -0
- package/runtime/dashboard/components/modal.tsx +23 -0
- package/runtime/dashboard/components/page-header.tsx +11 -0
- package/runtime/dashboard/components/pill-button.tsx +14 -0
- package/runtime/dashboard/components/refresh-button.tsx +7 -0
- package/runtime/dashboard/components/service-info.tsx +45 -0
- package/runtime/dashboard/components/status-badge.tsx +7 -0
- package/runtime/dashboard/components/table-link.tsx +5 -0
- package/runtime/dashboard/components/table.tsx +26 -0
- package/runtime/dashboard/components.tsx +19 -0
- package/runtime/dashboard/index.html +23 -0
- package/runtime/dashboard/lib.ts +45 -0
- package/runtime/dashboard/rpc/client.ts +20 -0
- package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
- package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
- package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
- package/runtime/dashboard/rpc/handlers/config.ts +137 -0
- package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
- package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
- package/runtime/dashboard/rpc/handlers/do.ts +117 -0
- package/runtime/dashboard/rpc/handlers/email.ts +82 -0
- package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
- package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
- package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
- package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
- package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
- package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
- package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
- package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
- package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
- package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
- package/runtime/dashboard/rpc/hooks.ts +132 -0
- package/runtime/dashboard/rpc/server.ts +70 -0
- package/runtime/dashboard/rpc/types.ts +396 -0
- package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
- package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
- package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
- package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
- package/runtime/dashboard/sql-browser/hooks.ts +137 -0
- package/runtime/dashboard/sql-browser/index.ts +4 -0
- package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
- package/runtime/dashboard/sql-browser/modals.tsx +116 -0
- package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
- package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
- package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
- package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
- package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
- package/runtime/dashboard/sql-browser/types.ts +61 -0
- package/runtime/dashboard/sql-browser/utils.ts +167 -0
- package/runtime/dashboard/style.css +177 -0
- package/runtime/dashboard/views/ai.tsx +152 -0
- package/runtime/dashboard/views/analytics-engine.tsx +169 -0
- package/runtime/dashboard/views/cache.tsx +93 -0
- package/runtime/dashboard/views/containers.tsx +197 -0
- package/runtime/dashboard/views/d1.tsx +81 -0
- package/runtime/dashboard/views/do.tsx +168 -0
- package/runtime/dashboard/views/email.tsx +235 -0
- package/runtime/dashboard/views/errors.tsx +558 -0
- package/runtime/dashboard/views/home.tsx +287 -0
- package/runtime/dashboard/views/kv.tsx +273 -0
- package/runtime/dashboard/views/queue.tsx +193 -0
- package/runtime/dashboard/views/r2.tsx +202 -0
- package/runtime/dashboard/views/scheduled.tsx +89 -0
- package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
- package/runtime/dashboard/views/traces.tsx +768 -0
- package/runtime/dashboard/views/workers.tsx +55 -0
- package/runtime/dashboard/views/workflows.tsx +473 -0
- package/runtime/db.ts +258 -0
- package/runtime/env.ts +362 -0
- package/runtime/error-page/app.tsx +394 -0
- package/runtime/error-page/build.ts +269 -0
- package/runtime/error-page/index.html +16 -0
- package/runtime/error-page/style.css +31 -0
- package/runtime/execution-context.ts +18 -0
- package/runtime/file-watcher.ts +57 -0
- package/runtime/generation-manager.ts +230 -0
- package/runtime/generation.ts +411 -0
- package/runtime/plugin.ts +292 -0
- package/runtime/request-cf.ts +28 -0
- package/runtime/rpc-validate.ts +154 -0
- package/runtime/tracing/context.ts +40 -0
- package/runtime/tracing/db.ts +73 -0
- package/runtime/tracing/frames.ts +75 -0
- package/runtime/tracing/instrument.ts +186 -0
- package/runtime/tracing/span.ts +138 -0
- package/runtime/tracing/store.ts +499 -0
- package/runtime/tracing/types.ts +47 -0
- package/runtime/vite-plugin/config-plugin.ts +68 -0
- package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
- package/runtime/vite-plugin/dist/index.mjs +52333 -0
- package/runtime/vite-plugin/globals-plugin.ts +94 -0
- package/runtime/vite-plugin/index.ts +43 -0
- package/runtime/vite-plugin/modules-plugin.ts +88 -0
- package/runtime/vite-plugin/react-router-plugin.ts +95 -0
- package/runtime/worker-registry.ts +52 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { useState, useEffect } from "preact/hooks";
|
|
2
|
+
import type { SpanData, SpanEventData } from "../rpc/types";
|
|
3
|
+
|
|
4
|
+
// ─── Types ───────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface TraceWaterfallProps {
|
|
7
|
+
spans: SpanData[];
|
|
8
|
+
events: SpanEventData[];
|
|
9
|
+
highlightSpanId?: string | null;
|
|
10
|
+
onAddAttributeFilter?: (key: string, value: string, type: "include" | "exclude") => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ─── Component ───────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export function TraceWaterfall({ spans, events, highlightSpanId, onAddAttributeFilter }: TraceWaterfallProps) {
|
|
16
|
+
const [expandedSpan, setExpandedSpan] = useState<string | null>(null);
|
|
17
|
+
const [collapsedSpans, setCollapsedSpans] = useState<Set<string>>(new Set());
|
|
18
|
+
|
|
19
|
+
// Compute waterfall layout
|
|
20
|
+
const traceStart = spans.length > 0 ? Math.min(...spans.map(s => s.startTime)) : 0;
|
|
21
|
+
const traceEnd = spans.length > 0 ? Math.max(...spans.map(s => (s.endTime ?? Date.now()))) : 0;
|
|
22
|
+
const traceDuration = traceEnd - traceStart || 1;
|
|
23
|
+
|
|
24
|
+
// Build tree structure
|
|
25
|
+
const spanMap = new Map(spans.map(s => [s.spanId, s]));
|
|
26
|
+
const childMap = new Map<string | null, SpanData[]>();
|
|
27
|
+
for (const s of spans) {
|
|
28
|
+
const key = s.parentSpanId;
|
|
29
|
+
if (!childMap.has(key)) childMap.set(key, []);
|
|
30
|
+
childMap.get(key)!.push(s);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Auto-expand: first 2 levels OR spans with >10% duration
|
|
34
|
+
const getAutoExpanded = (): Set<string> => {
|
|
35
|
+
const autoCollapsed = new Set<string>();
|
|
36
|
+
function walk(parentId: string | null, depth: number) {
|
|
37
|
+
const children = childMap.get(parentId) ?? [];
|
|
38
|
+
for (const child of children) {
|
|
39
|
+
const hasChildren = (childMap.get(child.spanId) ?? []).length > 0;
|
|
40
|
+
if (hasChildren) {
|
|
41
|
+
const spanDur = child.durationMs ?? 0;
|
|
42
|
+
const significantDuration = spanDur > traceDuration * 0.1;
|
|
43
|
+
if (depth >= 2 && !significantDuration) {
|
|
44
|
+
autoCollapsed.add(child.spanId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
walk(child.spanId, depth + 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
walk(null, 0);
|
|
51
|
+
return autoCollapsed;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Initialize collapsed state when spans change
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (spans.length > 0) {
|
|
57
|
+
setCollapsedSpans(getAutoExpanded());
|
|
58
|
+
setExpandedSpan(null);
|
|
59
|
+
}
|
|
60
|
+
}, [spans]);
|
|
61
|
+
|
|
62
|
+
const toggleCollapse = (spanId: string) => {
|
|
63
|
+
setCollapsedSpans(prev => {
|
|
64
|
+
const next = new Set(prev);
|
|
65
|
+
if (next.has(spanId)) {
|
|
66
|
+
next.delete(spanId);
|
|
67
|
+
} else {
|
|
68
|
+
next.add(spanId);
|
|
69
|
+
}
|
|
70
|
+
return next;
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function flattenTree(parentId: string | null, depth: number): Array<{ span: SpanData; depth: number }> {
|
|
75
|
+
const children = childMap.get(parentId) ?? [];
|
|
76
|
+
const result: Array<{ span: SpanData; depth: number }> = [];
|
|
77
|
+
for (const child of children) {
|
|
78
|
+
result.push({ span: child, depth });
|
|
79
|
+
if (!collapsedSpans.has(child.spanId)) {
|
|
80
|
+
result.push(...flattenTree(child.spanId, depth + 1));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const flatSpans = flattenTree(null, 0);
|
|
87
|
+
|
|
88
|
+
// Get parent span attributes for filtering inherited attrs
|
|
89
|
+
const getParentAttributes = (span: SpanData): Record<string, unknown> => {
|
|
90
|
+
if (!span.parentSpanId) return {};
|
|
91
|
+
const parent = spanMap.get(span.parentSpanId);
|
|
92
|
+
return parent?.attributes ?? {};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div>
|
|
97
|
+
{/* Timeline header */}
|
|
98
|
+
<div class="flex items-center justify-between mb-3">
|
|
99
|
+
<span class="text-xs text-text-muted font-mono">0ms</span>
|
|
100
|
+
<span class="text-xs text-text-muted font-mono">{formatDuration(traceDuration)}</span>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Waterfall */}
|
|
104
|
+
<div class="space-y-0.5">
|
|
105
|
+
{flatSpans.map(({ span, depth }) => {
|
|
106
|
+
const offset = ((span.startTime - traceStart) / traceDuration) * 100;
|
|
107
|
+
const width = (((span.endTime ?? Date.now()) - span.startTime) / traceDuration) * 100;
|
|
108
|
+
const spanEvents = events.filter(e => e.spanId === span.spanId);
|
|
109
|
+
const isExpanded = expandedSpan === span.spanId;
|
|
110
|
+
const hasChildren = (childMap.get(span.spanId) ?? []).length > 0;
|
|
111
|
+
const isCollapsed = collapsedSpans.has(span.spanId);
|
|
112
|
+
const parentAttrs = getParentAttributes(span);
|
|
113
|
+
const isHighlighted = highlightSpanId === span.spanId;
|
|
114
|
+
|
|
115
|
+
// Key attributes to show in the bar
|
|
116
|
+
const keyAttrs = width > 5 ? getKeyAttributes(span.attributes, 2) : [];
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div key={span.spanId}>
|
|
120
|
+
<div
|
|
121
|
+
class={`flex items-center cursor-pointer hover:bg-panel-hover rounded-md py-1 px-1 transition-colors`}
|
|
122
|
+
style={isHighlighted ? { background: "var(--color-error-highlight)", boxShadow: `inset 0 0 0 2px var(--color-span-error)` } : undefined}
|
|
123
|
+
onClick={() => setExpandedSpan(isExpanded ? null : span.spanId)}
|
|
124
|
+
>
|
|
125
|
+
{/* Span name with collapse toggle */}
|
|
126
|
+
<div class="w-[200px] flex-shrink-0 truncate text-xs text-ink flex items-center" style={{ paddingLeft: `${depth * 16}px` }}>
|
|
127
|
+
{hasChildren && (
|
|
128
|
+
<span
|
|
129
|
+
class="inline-block w-4 text-text-muted cursor-pointer select-none flex-shrink-0"
|
|
130
|
+
onClick={(e) => { e.stopPropagation(); toggleCollapse(span.spanId); }}
|
|
131
|
+
>
|
|
132
|
+
{isCollapsed ? "\u25B6" : "\u25BC"}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
{!hasChildren && <span class="inline-block w-4 flex-shrink-0" />}
|
|
136
|
+
<span class="truncate">{span.name}</span>
|
|
137
|
+
</div>
|
|
138
|
+
{/* Bar area */}
|
|
139
|
+
<div class="flex-1 h-6 relative bg-panel-secondary rounded">
|
|
140
|
+
<div
|
|
141
|
+
class={`absolute top-0.5 bottom-0.5 rounded flex items-center overflow-hidden ${
|
|
142
|
+
span.status !== "error" && span.status !== "ok" ? "animate-pulse" : ""
|
|
143
|
+
}`}
|
|
144
|
+
style={{
|
|
145
|
+
left: `${offset}%`, width: `${Math.max(width, 0.5)}%`,
|
|
146
|
+
background: span.status === "error" ? "var(--color-span-error)" : span.status === "ok" ? "var(--color-span-ok)" : "#d1d5db",
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{/* Key attributes inside bar */}
|
|
150
|
+
{keyAttrs.length > 0 && (
|
|
151
|
+
<span class="text-[9px] text-white px-1 truncate whitespace-nowrap">
|
|
152
|
+
{keyAttrs.map(([k, v]) => `${k}=${String(v)}`).join(" ")}
|
|
153
|
+
</span>
|
|
154
|
+
)}
|
|
155
|
+
{/* Event markers */}
|
|
156
|
+
{spanEvents.map(ev => {
|
|
157
|
+
const evOffset = ((ev.timestamp - span.startTime) / ((span.endTime ?? Date.now()) - span.startTime || 1)) * 100;
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
key={ev.id}
|
|
161
|
+
class={`absolute top-0 w-1.5 h-full rounded-full ${ev.name === "exception" ? "bg-red-600" : "bg-panel-secondary0"}`}
|
|
162
|
+
style={{ left: `${Math.min(evOffset, 100)}%` }}
|
|
163
|
+
title={ev.message ?? ev.name}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</div>
|
|
168
|
+
{/* Duration label */}
|
|
169
|
+
<span
|
|
170
|
+
class="absolute top-0.5 text-[10px] text-text-secondary whitespace-nowrap"
|
|
171
|
+
style={{ left: `${offset + width + 1}%` }}
|
|
172
|
+
>
|
|
173
|
+
{span.durationMs !== null ? formatDuration(span.durationMs) : "..."}
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Expanded detail */}
|
|
179
|
+
{isExpanded && (
|
|
180
|
+
<div class="bg-panel-secondary border border-border-subtle rounded-lg p-4 mt-1 mb-2 ml-4">
|
|
181
|
+
<div class="text-xs space-y-3">
|
|
182
|
+
{/* Timing section */}
|
|
183
|
+
<div class="grid grid-cols-[auto_1fr_auto_1fr] gap-x-3 gap-y-1.5 items-baseline">
|
|
184
|
+
<span class="text-text-muted">Kind:</span> <span class="text-ink">{span.kind}</span>
|
|
185
|
+
<span class="text-text-muted">Status:</span> <TraceStatusBadge status={span.status} />
|
|
186
|
+
<span class="text-text-muted">Start:</span> <span class="text-ink font-mono">{formatTimestamp(span.startTime)}</span>
|
|
187
|
+
<span class="text-text-muted">End:</span> <span class="text-ink font-mono">{span.endTime ? formatTimestamp(span.endTime) : "..."}</span>
|
|
188
|
+
<span class="text-text-muted">Duration:</span> <span class="text-ink font-mono">{span.durationMs !== null ? formatDuration(span.durationMs) : "..."}</span>
|
|
189
|
+
<span class="text-text-muted">Trace ID:</span> <span class="text-ink font-mono">{span.traceId.slice(0, 16)}...</span>
|
|
190
|
+
{span.parentSpanId && (
|
|
191
|
+
<>
|
|
192
|
+
<span class="text-text-muted">Parent:</span> <span class="text-ink font-mono">{span.parentSpanId.slice(0, 16)}...</span>
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
{span.statusMessage && (
|
|
197
|
+
<div>
|
|
198
|
+
<span class="text-text-muted">Error:</span>
|
|
199
|
+
<span class="ml-2 text-red-500">{span.statusMessage}</span>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
{/* Attributes (filtered: inherited removed) */}
|
|
203
|
+
{Object.keys(span.attributes).length > 0 && (() => {
|
|
204
|
+
const filteredAttrs = Object.entries(span.attributes).filter(([k, v]) => {
|
|
205
|
+
const parentVal = parentAttrs[k];
|
|
206
|
+
return parentVal === undefined || JSON.stringify(parentVal) !== JSON.stringify(v);
|
|
207
|
+
});
|
|
208
|
+
if (filteredAttrs.length === 0) return null;
|
|
209
|
+
return (
|
|
210
|
+
<div>
|
|
211
|
+
<div class="text-text-muted mb-1.5 font-medium">Attributes:</div>
|
|
212
|
+
<div class="space-y-1.5">
|
|
213
|
+
{filteredAttrs.map(([k, v]) => {
|
|
214
|
+
const isComplex = typeof v === "object" || (typeof v === "string" && (v.includes("\n") || (v.startsWith("{") || v.startsWith("[")) && v.length > 2));
|
|
215
|
+
return (
|
|
216
|
+
<div key={k} class="group">
|
|
217
|
+
<div class="flex items-center gap-1.5">
|
|
218
|
+
<span class="text-text-secondary font-mono text-[11px]">{k}</span>
|
|
219
|
+
{onAddAttributeFilter && (
|
|
220
|
+
<span class="invisible group-hover:visible flex gap-0.5">
|
|
221
|
+
<button
|
|
222
|
+
onClick={(e) => { e.stopPropagation(); onAddAttributeFilter(k, String(v), "include"); }}
|
|
223
|
+
class="text-emerald-500 hover:text-emerald-700 text-[10px] leading-none"
|
|
224
|
+
title="Include filter"
|
|
225
|
+
>+</button>
|
|
226
|
+
<button
|
|
227
|
+
onClick={(e) => { e.stopPropagation(); onAddAttributeFilter(k, String(v), "exclude"); }}
|
|
228
|
+
class="text-red-500 hover:text-red-700 text-[10px] leading-none"
|
|
229
|
+
title="Exclude filter"
|
|
230
|
+
>{"\u2212"}</button>
|
|
231
|
+
</span>
|
|
232
|
+
)}
|
|
233
|
+
{!isComplex && (
|
|
234
|
+
<span class="font-mono text-[11px] ml-auto"><AttributeValue value={v} /></span>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
{isComplex && (
|
|
238
|
+
<div class="mt-0.5 font-mono text-[11px]"><AttributeValue value={v} /></div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
})}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
})()}
|
|
247
|
+
{/* Events */}
|
|
248
|
+
{spanEvents.length > 0 && (
|
|
249
|
+
<div>
|
|
250
|
+
<div class="text-text-muted mb-1">Events:</div>
|
|
251
|
+
{spanEvents.map(ev => (
|
|
252
|
+
<div key={ev.id} class={`py-1 px-2 rounded-md mb-1 ${ev.name !== "exception" ? "bg-panel border border-border-subtle" : ""}`} style={ev.name === "exception" ? { background: "var(--color-error-highlight)" } : undefined}>
|
|
253
|
+
<div class="flex items-center gap-2">
|
|
254
|
+
<span class="font-medium">{ev.name}</span>
|
|
255
|
+
{ev.level && <EventLevelBadge level={ev.level} />}
|
|
256
|
+
<span class="text-text-muted font-mono ml-auto">
|
|
257
|
+
+{Math.round(ev.timestamp - span.startTime)}ms
|
|
258
|
+
</span>
|
|
259
|
+
</div>
|
|
260
|
+
{ev.message && <div class="text-text-data mt-0.5 font-mono break-all">{ev.message}</div>}
|
|
261
|
+
</div>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
})}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Smart Attribute Rendering ───────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
const URL_REGEX = /^https?:\/\/[^\s]+$/;
|
|
279
|
+
|
|
280
|
+
function CollapsibleBlock({ content, lines }: { content: string; lines: number }) {
|
|
281
|
+
const [expanded, setExpanded] = useState(false);
|
|
282
|
+
const isLong = lines > 8;
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div class="relative">
|
|
286
|
+
<pre
|
|
287
|
+
class={`text-ink bg-panel border border-border p-2 rounded-md overflow-x-auto text-[11px] whitespace-pre-wrap break-all ${
|
|
288
|
+
isLong && !expanded ? "max-h-[160px] overflow-hidden" : ""
|
|
289
|
+
}`}
|
|
290
|
+
>{content}</pre>
|
|
291
|
+
{isLong && (
|
|
292
|
+
<button
|
|
293
|
+
onClick={(e) => { e.stopPropagation(); setExpanded(!expanded); }}
|
|
294
|
+
class={`text-[10px] text-blue-500 hover:text-blue-700 font-medium mt-0.5 ${
|
|
295
|
+
!expanded ? "absolute bottom-0 left-0 right-0 pt-6 pb-1 text-center bg-gradient-to-t from-panel via-panel/90 to-transparent rounded-b-md" : ""
|
|
296
|
+
}`}
|
|
297
|
+
>
|
|
298
|
+
{expanded ? "Show less" : `Show all (${lines} lines)`}
|
|
299
|
+
</button>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function AttributeValue({ value }: { value: unknown }) {
|
|
306
|
+
if (value === null || value === undefined) {
|
|
307
|
+
return <span class="text-text-muted italic">null</span>;
|
|
308
|
+
}
|
|
309
|
+
if (typeof value === "boolean") {
|
|
310
|
+
return <span class="text-orange-600">{String(value)}</span>;
|
|
311
|
+
}
|
|
312
|
+
if (typeof value === "number") {
|
|
313
|
+
return <span class="text-purple-600">{value}</span>;
|
|
314
|
+
}
|
|
315
|
+
if (typeof value === "object") {
|
|
316
|
+
const formatted = JSON.stringify(value, null, 2);
|
|
317
|
+
const lines = formatted.split("\n").length;
|
|
318
|
+
return <CollapsibleBlock content={formatted} lines={lines} />;
|
|
319
|
+
}
|
|
320
|
+
const str = String(value);
|
|
321
|
+
if (URL_REGEX.test(str)) {
|
|
322
|
+
return <a href={str} target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:underline break-all">{str}</a>;
|
|
323
|
+
}
|
|
324
|
+
// Try JSON
|
|
325
|
+
if ((str.startsWith("{") || str.startsWith("[")) && str.length > 2) {
|
|
326
|
+
try {
|
|
327
|
+
const parsed = JSON.parse(str);
|
|
328
|
+
const formatted = JSON.stringify(parsed, null, 2);
|
|
329
|
+
const lines = formatted.split("\n").length;
|
|
330
|
+
return <CollapsibleBlock content={formatted} lines={lines} />;
|
|
331
|
+
} catch {}
|
|
332
|
+
}
|
|
333
|
+
// Multiline
|
|
334
|
+
if (str.includes("\n")) {
|
|
335
|
+
const lines = str.split("\n").length;
|
|
336
|
+
return <CollapsibleBlock content={str} lines={lines} />;
|
|
337
|
+
}
|
|
338
|
+
return <span class="text-ink">{str}</span>;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─── Shared Helpers ──────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
export function EventLevelBadge({ level }: { level: string }) {
|
|
344
|
+
const upper = level.toUpperCase();
|
|
345
|
+
const styles: Record<string, { bg: string; color: string } | null> = {
|
|
346
|
+
ERROR: { bg: "var(--color-badge-red-bg)", color: "var(--color-badge-red-text)" },
|
|
347
|
+
WARN: { bg: "var(--color-badge-orange-bg)", color: "var(--color-badge-orange-text)" },
|
|
348
|
+
WARNING: { bg: "var(--color-badge-orange-bg)", color: "var(--color-badge-orange-text)" },
|
|
349
|
+
INFO: { bg: "var(--color-badge-blue-bg)", color: "var(--color-badge-blue-text)" },
|
|
350
|
+
LOG: null,
|
|
351
|
+
DEBUG: null,
|
|
352
|
+
};
|
|
353
|
+
const s = styles[upper];
|
|
354
|
+
return (
|
|
355
|
+
<span
|
|
356
|
+
class={`inline-flex px-1.5 py-0.5 rounded-md text-[10px] font-medium ${!s ? "bg-panel-secondary text-text-secondary" : ""}`}
|
|
357
|
+
style={s ? { background: s.bg, color: s.color } : undefined}
|
|
358
|
+
>
|
|
359
|
+
{upper}
|
|
360
|
+
</span>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function TraceStatusBadge({ status }: { status: string }) {
|
|
365
|
+
const styles: Record<string, { bg: string; color: string } | null> = {
|
|
366
|
+
ok: { bg: "var(--color-badge-emerald-bg)", color: "var(--color-badge-emerald-text)" },
|
|
367
|
+
error: { bg: "var(--color-badge-red-bg)", color: "var(--color-badge-red-text)" },
|
|
368
|
+
unset: null,
|
|
369
|
+
};
|
|
370
|
+
const s = styles[status];
|
|
371
|
+
return (
|
|
372
|
+
<span
|
|
373
|
+
class={`inline-flex px-2 py-0.5 rounded-md text-xs font-medium ${!s ? "bg-panel-hover text-text-secondary" : ""} ${status === "unset" ? "animate-pulse" : ""}`}
|
|
374
|
+
style={s ? { background: s.bg, color: s.color } : undefined}
|
|
375
|
+
>
|
|
376
|
+
{status === "unset" ? "running" : status}
|
|
377
|
+
</span>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function getKeyAttributes(attrs: Record<string, unknown>, max: number): [string, unknown][] {
|
|
382
|
+
const priorityKeys = ["http.method", "http.status_code", "http.url", "http.route", "db.system", "db.operation", "rpc.method"];
|
|
383
|
+
const entries = Object.entries(attrs);
|
|
384
|
+
const result: [string, unknown][] = [];
|
|
385
|
+
for (const key of priorityKeys) {
|
|
386
|
+
if (result.length >= max) break;
|
|
387
|
+
const entry = entries.find(([k]) => k === key);
|
|
388
|
+
if (entry) result.push(entry);
|
|
389
|
+
}
|
|
390
|
+
if (result.length < max) {
|
|
391
|
+
for (const entry of entries) {
|
|
392
|
+
if (result.length >= max) break;
|
|
393
|
+
if (!result.some(([k]) => k === entry[0]) && typeof entry[1] !== "object") {
|
|
394
|
+
result.push(entry);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function formatDuration(ms: number): string {
|
|
402
|
+
if (ms < 1) return "<1ms";
|
|
403
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
404
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function formatTimestamp(ts: number): string {
|
|
408
|
+
const d = new Date(ts);
|
|
409
|
+
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}.${d.getMilliseconds().toString().padStart(3, "0")}`;
|
|
410
|
+
}
|