@workflow/web-shared 5.0.0-beta.15 → 5.0.0-beta.17
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/components/event-list-view.d.ts +1 -1
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +20 -3
- package/dist/components/new-trace-viewer/components/timeline.d.ts +1 -1
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +40 -4
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +8 -9
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +26 -32
- package/dist/components/sidebar/attributes-block.d.ts +28 -0
- package/dist/components/sidebar/attributes-block.d.ts.map +1 -0
- package/dist/components/sidebar/attributes-block.js +83 -0
- package/dist/components/sidebar/entity-detail-panel.js +2 -2
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +8 -1
- package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/node.js +2 -1
- package/dist/components/ui/context-card.d.ts +47 -0
- package/dist/components/ui/context-card.d.ts.map +1 -0
- package/dist/components/ui/context-card.js +471 -0
- package/dist/components/ui/timestamp-tooltip.d.ts +34 -1
- package/dist/components/ui/timestamp-tooltip.d.ts.map +1 -1
- package/dist/components/ui/timestamp-tooltip.js +80 -132
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +8 -10
- package/dist/components/workflow-traces/event-colors.d.ts +1 -0
- package/dist/components/workflow-traces/event-colors.d.ts.map +1 -1
- package/dist/components/workflow-traces/event-colors.js +12 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +2 -1
- package/dist/lib/trace-builder.d.ts +15 -0
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +27 -2
- package/dist/styles.css +9 -0
- package/package.json +7 -5
- package/src/components/event-list-view.tsx +25 -2
- package/src/components/new-trace-viewer/components/timeline.tsx +82 -2
- package/src/components/new-trace-viewer/trace-viewer.tsx +17 -14
- package/src/components/sidebar/attribute-panel.tsx +117 -119
- package/src/components/sidebar/attributes-block.tsx +182 -0
- package/src/components/sidebar/entity-detail-panel.tsx +1 -1
- package/src/components/sidebar/events-list.tsx +8 -0
- package/src/components/trace-viewer/components/node.tsx +3 -2
- package/src/components/ui/context-card.tsx +784 -0
- package/src/components/ui/timestamp-tooltip.tsx +132 -194
- package/src/components/workflow-trace-view.tsx +18 -10
- package/src/components/workflow-traces/event-colors.ts +12 -0
- package/src/components/workflow-traces/trace-span-construction.ts +1 -0
- package/src/lib/trace-builder.ts +32 -1
- package/src/styles.css +9 -0
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
|
-
import { useEffect,
|
|
5
|
-
import {
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
ContextCardProvider,
|
|
7
|
+
ContextCardTrigger,
|
|
8
|
+
type ContextCardTriggerProps,
|
|
9
|
+
useHasContextCardProvider,
|
|
10
|
+
} from './context-card';
|
|
6
11
|
|
|
7
12
|
// ---------------------------------------------------------------------------
|
|
8
13
|
// Time formatting helpers
|
|
9
14
|
// ---------------------------------------------------------------------------
|
|
10
15
|
|
|
11
16
|
interface TimeUnit {
|
|
12
|
-
unit:
|
|
17
|
+
unit: Intl.RelativeTimeFormatUnit;
|
|
13
18
|
ms: number;
|
|
14
19
|
}
|
|
15
20
|
|
|
@@ -38,25 +43,69 @@ function formatTimeDifference(diff: number): string {
|
|
|
38
43
|
return result.join(', ');
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Detailed relative time string that auto-updates every second
|
|
48
|
+
* (e.g. "2 hours, 15 minutes, 30 seconds ago"). Used inside the hover card.
|
|
49
|
+
*/
|
|
50
|
+
export function useTimeAgo(date: number): string {
|
|
42
51
|
const [timeAgo, setTimeAgo] = useState<string>('');
|
|
43
52
|
|
|
44
53
|
useEffect(() => {
|
|
45
|
-
const
|
|
54
|
+
const updateTimeAgo = (): void => {
|
|
46
55
|
const diff = Date.now() - date;
|
|
47
|
-
const
|
|
48
|
-
setTimeAgo(
|
|
56
|
+
const formattedDiff = formatTimeDifference(diff);
|
|
57
|
+
setTimeAgo(formattedDiff ? `${formattedDiff} ago` : 'Just now');
|
|
49
58
|
};
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
|
|
60
|
+
updateTimeAgo();
|
|
61
|
+
const timer = setInterval(updateTimeAgo, 1000);
|
|
52
62
|
return () => clearInterval(timer);
|
|
53
63
|
}, [date]);
|
|
54
64
|
|
|
55
65
|
return timeAgo;
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Short relative time string that auto-updates every minute
|
|
70
|
+
* (e.g. "3 days ago", "5 hours ago"). Returns an empty string if `date` is
|
|
71
|
+
* nullish.
|
|
72
|
+
*/
|
|
73
|
+
export function useShortTimeAgo(date: number | null | undefined): string {
|
|
74
|
+
const [shortTimeAgo, setShortTimeAgo] = useState<string>('');
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!date) {
|
|
78
|
+
setShortTimeAgo('');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const updateShortTimeAgo = (): void => {
|
|
83
|
+
const diff = Date.now() - date;
|
|
84
|
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
85
|
+
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
86
|
+
const minutes = Math.floor(diff / (1000 * 60));
|
|
87
|
+
|
|
88
|
+
if (days > 0) {
|
|
89
|
+
setShortTimeAgo(`${days} day${days > 1 ? 's' : ''} ago`);
|
|
90
|
+
} else if (hours > 0) {
|
|
91
|
+
setShortTimeAgo(`${hours} hour${hours > 1 ? 's' : ''} ago`);
|
|
92
|
+
} else if (minutes > 0) {
|
|
93
|
+
setShortTimeAgo(`${minutes} minute${minutes > 1 ? 's' : ''} ago`);
|
|
94
|
+
} else {
|
|
95
|
+
setShortTimeAgo('Just now');
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
updateShortTimeAgo();
|
|
100
|
+
const timer = setInterval(updateShortTimeAgo, 60000);
|
|
101
|
+
return () => clearInterval(timer);
|
|
102
|
+
}, [date]);
|
|
103
|
+
|
|
104
|
+
return shortTimeAgo;
|
|
105
|
+
}
|
|
106
|
+
|
|
58
107
|
// ---------------------------------------------------------------------------
|
|
59
|
-
//
|
|
108
|
+
// Hover card content
|
|
60
109
|
// ---------------------------------------------------------------------------
|
|
61
110
|
|
|
62
111
|
function ZoneDateTimeRow({
|
|
@@ -91,86 +140,34 @@ function ZoneDateTimeRow({
|
|
|
91
140
|
});
|
|
92
141
|
|
|
93
142
|
return (
|
|
94
|
-
<div
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}}
|
|
101
|
-
>
|
|
102
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
103
|
-
<div
|
|
104
|
-
style={{
|
|
105
|
-
display: 'inline-flex',
|
|
106
|
-
alignItems: 'center',
|
|
107
|
-
justifyContent: 'center',
|
|
108
|
-
height: 16,
|
|
109
|
-
padding: '0 6px',
|
|
110
|
-
backgroundColor: 'var(--ds-gray-200)',
|
|
111
|
-
borderRadius: 3,
|
|
112
|
-
fontSize: 11,
|
|
113
|
-
fontFamily: 'var(--font-mono, monospace)',
|
|
114
|
-
fontWeight: 500,
|
|
115
|
-
color: 'var(--ds-gray-900)',
|
|
116
|
-
whiteSpace: 'nowrap',
|
|
117
|
-
}}
|
|
118
|
-
>
|
|
119
|
-
{formattedZone}
|
|
143
|
+
<div className="flex items-center justify-between gap-3">
|
|
144
|
+
<div className="flex items-center gap-1.5">
|
|
145
|
+
<div className="flex items-center justify-center h-4 px-1.5 bg-gray-200 rounded-xs">
|
|
146
|
+
<span className="text-[12px] font-mono text-gray-900">
|
|
147
|
+
{formattedZone}
|
|
148
|
+
</span>
|
|
120
149
|
</div>
|
|
121
|
-
<span
|
|
122
|
-
style={{
|
|
123
|
-
fontSize: 13,
|
|
124
|
-
color: 'var(--ds-gray-1000)',
|
|
125
|
-
whiteSpace: 'nowrap',
|
|
126
|
-
}}
|
|
127
|
-
>
|
|
128
|
-
{formattedDate}
|
|
129
|
-
</span>
|
|
150
|
+
<span className="text-[13px] text-gray-1000">{formattedDate}</span>
|
|
130
151
|
</div>
|
|
131
|
-
<span
|
|
132
|
-
style={{
|
|
133
|
-
fontSize: 11,
|
|
134
|
-
fontFamily: 'var(--font-mono, monospace)',
|
|
135
|
-
fontVariantNumeric: 'tabular-nums',
|
|
136
|
-
color: 'var(--ds-gray-900)',
|
|
137
|
-
whiteSpace: 'nowrap',
|
|
138
|
-
}}
|
|
139
|
-
>
|
|
152
|
+
<span className="tabular-nums text-[12px] font-mono text-gray-900">
|
|
140
153
|
{formattedTime}
|
|
141
154
|
</span>
|
|
142
155
|
</div>
|
|
143
156
|
);
|
|
144
157
|
}
|
|
145
158
|
|
|
146
|
-
|
|
147
|
-
// Tooltip card content
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
function TimestampTooltipContent({ date }: { date: number }): ReactNode {
|
|
159
|
+
function RelativeTimeContextCardContent({ date }: { date: number }): ReactNode {
|
|
151
160
|
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
152
161
|
const timeAgo = useTimeAgo(date);
|
|
153
162
|
|
|
154
163
|
return (
|
|
155
|
-
<div
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}}
|
|
163
|
-
>
|
|
164
|
-
<span
|
|
165
|
-
style={{
|
|
166
|
-
fontSize: 13,
|
|
167
|
-
fontVariantNumeric: 'tabular-nums',
|
|
168
|
-
color: 'var(--ds-gray-900)',
|
|
169
|
-
}}
|
|
170
|
-
>
|
|
171
|
-
{timeAgo}
|
|
172
|
-
</span>
|
|
173
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
164
|
+
<div className="flex flex-col gap-3 min-w-[300px]">
|
|
165
|
+
<div className="flex flex-col gap-3">
|
|
166
|
+
<span className="tabular-nums text-[13px] text-gray-900">
|
|
167
|
+
{timeAgo}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="flex flex-col gap-2">
|
|
174
171
|
<ZoneDateTimeRow date={date} zone="UTC" />
|
|
175
172
|
<ZoneDateTimeRow date={date} zone={localTimezone} />
|
|
176
173
|
</div>
|
|
@@ -178,102 +175,75 @@ function TimestampTooltipContent({ date }: { date: number }): ReactNode {
|
|
|
178
175
|
);
|
|
179
176
|
}
|
|
180
177
|
|
|
178
|
+
function DefaultTimeText({
|
|
179
|
+
date,
|
|
180
|
+
}: {
|
|
181
|
+
date: number | null | undefined;
|
|
182
|
+
}): ReactNode {
|
|
183
|
+
const shortTimeAgo = useShortTimeAgo(date);
|
|
184
|
+
return <span className="text-label-14 text-gray-900">{shortTimeAgo}</span>;
|
|
185
|
+
}
|
|
186
|
+
|
|
181
187
|
// ---------------------------------------------------------------------------
|
|
182
|
-
//
|
|
188
|
+
// RelativeTimeCard
|
|
183
189
|
// ---------------------------------------------------------------------------
|
|
184
190
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
type RelativeTimeCardProps = Omit<
|
|
192
|
+
ContextCardTriggerProps,
|
|
193
|
+
'content' | 'children'
|
|
194
|
+
> & {
|
|
195
|
+
/** Timestamp in milliseconds to display as a relative time. */
|
|
196
|
+
date?: number | null;
|
|
197
|
+
/** Custom content to render instead of the default relative time text. */
|
|
198
|
+
children?: ReactNode;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Relative time label that reveals a context card with detailed UTC and local
|
|
203
|
+
* timestamps on hover. Renders a default short relative time label (e.g.
|
|
204
|
+
* "3 days ago") when `children` is omitted; renders just the children without
|
|
205
|
+
* the hover card when `date` is nullish.
|
|
206
|
+
*/
|
|
207
|
+
export function RelativeTimeCard({
|
|
192
208
|
date,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}): ReactNode {
|
|
199
|
-
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
200
|
-
const [style, setStyle] = useState<React.CSSProperties>({
|
|
201
|
-
position: 'fixed',
|
|
202
|
-
zIndex: 9999,
|
|
203
|
-
visibility: 'hidden',
|
|
204
|
-
});
|
|
209
|
+
children: _children,
|
|
210
|
+
...props
|
|
211
|
+
}: RelativeTimeCardProps): ReactNode {
|
|
212
|
+
const children =
|
|
213
|
+
_children === undefined ? <DefaultTimeText date={date} /> : _children;
|
|
205
214
|
|
|
206
|
-
|
|
207
|
-
const placement = triggerRect.top > 240 ? 'above' : 'below';
|
|
208
|
-
const centerX = triggerRect.left + triggerRect.width / 2;
|
|
209
|
-
|
|
210
|
-
const el = tooltipRef.current;
|
|
211
|
-
const w = el ? el.offsetWidth : TOOLTIP_WIDTH;
|
|
212
|
-
const h = el ? el.offsetHeight : 100;
|
|
213
|
-
|
|
214
|
-
let left = centerX - w / 2;
|
|
215
|
-
left = Math.max(
|
|
216
|
-
VIEWPORT_PAD,
|
|
217
|
-
Math.min(left, window.innerWidth - w - VIEWPORT_PAD)
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
let top: number;
|
|
221
|
-
if (placement === 'above') {
|
|
222
|
-
top = triggerRect.top - h - 6;
|
|
223
|
-
if (top < VIEWPORT_PAD) {
|
|
224
|
-
top = triggerRect.bottom + 6;
|
|
225
|
-
}
|
|
226
|
-
} else {
|
|
227
|
-
top = triggerRect.bottom + 6;
|
|
228
|
-
if (top + h > window.innerHeight - VIEWPORT_PAD) {
|
|
229
|
-
top = triggerRect.top - h - 6;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
215
|
+
if (!date) return children;
|
|
232
216
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
zIndex: 9999,
|
|
238
|
-
borderRadius: 10,
|
|
239
|
-
border: '1px solid var(--ds-gray-alpha-200)',
|
|
240
|
-
backgroundColor: 'var(--ds-background-100)',
|
|
241
|
-
boxShadow: '0 4px 12px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.06)',
|
|
242
|
-
visibility: 'visible',
|
|
243
|
-
});
|
|
244
|
-
}, [triggerRect]);
|
|
245
|
-
|
|
246
|
-
return createPortal(
|
|
247
|
-
// biome-ignore lint/a11y/noStaticElementInteractions: tooltip hover zone
|
|
248
|
-
<div
|
|
249
|
-
ref={tooltipRef}
|
|
250
|
-
onMouseEnter={onMouseEnter}
|
|
251
|
-
onMouseLeave={onMouseLeave}
|
|
252
|
-
style={style}
|
|
217
|
+
return (
|
|
218
|
+
<ContextCardTrigger
|
|
219
|
+
content={<RelativeTimeContextCardContent date={date} />}
|
|
220
|
+
{...props}
|
|
253
221
|
>
|
|
254
|
-
|
|
255
|
-
</
|
|
256
|
-
document.body
|
|
222
|
+
{children}
|
|
223
|
+
</ContextCardTrigger>
|
|
257
224
|
);
|
|
258
225
|
}
|
|
259
226
|
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// TimestampTooltip — convenience wrapper used across the observability UI
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Wraps an already-formatted timestamp display with a relative-time hover
|
|
233
|
+
* card. Self-mounts a {@link ContextCardProvider} when one isn't already
|
|
234
|
+
* present so it works anywhere, but shares a provider (enabling the animated
|
|
235
|
+
* card morph between adjacent timestamps) when rendered inside one.
|
|
236
|
+
*/
|
|
260
237
|
export function TimestampTooltip({
|
|
261
238
|
date,
|
|
262
239
|
children,
|
|
240
|
+
side = 'top',
|
|
263
241
|
}: {
|
|
264
242
|
date: number | Date | string | null | undefined;
|
|
265
243
|
children: ReactNode;
|
|
244
|
+
side?: ContextCardTriggerProps['side'];
|
|
266
245
|
}): ReactNode {
|
|
267
|
-
const
|
|
268
|
-
const [triggerRect, setTriggerRect] = useState<DOMRect | null>(null);
|
|
269
|
-
const triggerRef = useRef<HTMLSpanElement>(null);
|
|
270
|
-
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
271
|
-
|
|
272
|
-
useEffect(() => {
|
|
273
|
-
return () => {
|
|
274
|
-
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
275
|
-
};
|
|
276
|
-
}, []);
|
|
246
|
+
const hasProvider = useHasContextCardProvider();
|
|
277
247
|
|
|
278
248
|
const ts =
|
|
279
249
|
date == null
|
|
@@ -284,43 +254,11 @@ export function TimestampTooltip({
|
|
|
284
254
|
|
|
285
255
|
if (ts == null || Number.isNaN(ts)) return <>{children}</>;
|
|
286
256
|
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
clearTimeout(closeTimer.current);
|
|
290
|
-
closeTimer.current = null;
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const scheduleClose = () => {
|
|
295
|
-
cancelClose();
|
|
296
|
-
closeTimer.current = setTimeout(() => setOpen(false), 120);
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const handleOpen = () => {
|
|
300
|
-
cancelClose();
|
|
301
|
-
if (triggerRef.current) {
|
|
302
|
-
setTriggerRect(triggerRef.current.getBoundingClientRect());
|
|
303
|
-
}
|
|
304
|
-
setOpen(true);
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
return (
|
|
308
|
-
// biome-ignore lint/a11y/noStaticElementInteractions: tooltip trigger
|
|
309
|
-
<span
|
|
310
|
-
ref={triggerRef}
|
|
311
|
-
onMouseEnter={handleOpen}
|
|
312
|
-
onMouseLeave={scheduleClose}
|
|
313
|
-
style={{ display: 'inline-flex' }}
|
|
314
|
-
>
|
|
257
|
+
const card = (
|
|
258
|
+
<RelativeTimeCard date={ts} side={side} asChild>
|
|
315
259
|
{children}
|
|
316
|
-
|
|
317
|
-
<TooltipPortal
|
|
318
|
-
triggerRect={triggerRect}
|
|
319
|
-
onMouseEnter={cancelClose}
|
|
320
|
-
onMouseLeave={scheduleClose}
|
|
321
|
-
date={ts}
|
|
322
|
-
/>
|
|
323
|
-
)}
|
|
324
|
-
</span>
|
|
260
|
+
</RelativeTimeCard>
|
|
325
261
|
);
|
|
262
|
+
|
|
263
|
+
return hasProvider ? card : <ContextCardProvider>{card}</ContextCardProvider>;
|
|
326
264
|
}
|
|
@@ -17,7 +17,11 @@ import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
|
|
17
17
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
18
18
|
import { createPortal } from 'react-dom';
|
|
19
19
|
import { useToast } from '../lib/toast';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
buildTrace,
|
|
22
|
+
filterSpanRawEvents,
|
|
23
|
+
type TraceWithMeta,
|
|
24
|
+
} from '../lib/trace-builder';
|
|
21
25
|
import { ErrorBoundary } from './error-boundary';
|
|
22
26
|
import {
|
|
23
27
|
EntityDetailPanel,
|
|
@@ -862,12 +866,14 @@ export const WorkflowTraceViewer = ({
|
|
|
862
866
|
const handleSelectionChange = useCallback(
|
|
863
867
|
(info: SelectedSpanInfo | null) => {
|
|
864
868
|
if (info) {
|
|
865
|
-
// Filter raw events
|
|
866
|
-
//
|
|
867
|
-
|
|
868
|
-
const rawEvents =
|
|
869
|
-
|
|
870
|
-
|
|
869
|
+
// Filter raw events for the selected span: child spans match on
|
|
870
|
+
// correlationId (stepId/hookId), the run root span gets run-level
|
|
871
|
+
// events. This bypasses the trace worker pipeline entirely.
|
|
872
|
+
const rawEvents = filterSpanRawEvents(
|
|
873
|
+
events,
|
|
874
|
+
info.resource,
|
|
875
|
+
info.spanId
|
|
876
|
+
);
|
|
871
877
|
setSelectedSpan({ ...info, rawEvents });
|
|
872
878
|
} else {
|
|
873
879
|
setSelectedSpan(null);
|
|
@@ -881,8 +887,10 @@ export const WorkflowTraceViewer = ({
|
|
|
881
887
|
const correlationId = selectedSpan?.spanId;
|
|
882
888
|
if (!correlationId) return;
|
|
883
889
|
|
|
884
|
-
const nextRawEvents =
|
|
885
|
-
|
|
890
|
+
const nextRawEvents = filterSpanRawEvents(
|
|
891
|
+
events,
|
|
892
|
+
selectedSpan?.resource,
|
|
893
|
+
correlationId
|
|
886
894
|
);
|
|
887
895
|
setSelectedSpan((prev) => {
|
|
888
896
|
if (!prev || prev.spanId !== correlationId) {
|
|
@@ -904,7 +912,7 @@ export const WorkflowTraceViewer = ({
|
|
|
904
912
|
rawEvents: nextRawEvents,
|
|
905
913
|
};
|
|
906
914
|
});
|
|
907
|
-
}, [events, selectedSpan?.spanId]);
|
|
915
|
+
}, [events, selectedSpan?.spanId, selectedSpan?.resource]);
|
|
908
916
|
|
|
909
917
|
// Reset selected span when navigating to a different run
|
|
910
918
|
useEffect(() => {
|
|
@@ -22,6 +22,7 @@ export interface EventColorPalette {
|
|
|
22
22
|
* - Red for failures (step_failed, run_failed)
|
|
23
23
|
* - Orange/yellow for retries (step_retrying)
|
|
24
24
|
* - Purple for webhook-related events
|
|
25
|
+
* - Teal for attribute changes (attr_set)
|
|
25
26
|
* - Blue otherwise (default)
|
|
26
27
|
*/
|
|
27
28
|
export function getEventColor(
|
|
@@ -64,6 +65,17 @@ export function getEventColor(
|
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// Attribute changes - Teal
|
|
69
|
+
if (eventType === 'attr_set') {
|
|
70
|
+
return {
|
|
71
|
+
color: 'var(--ds-teal-900)',
|
|
72
|
+
background: 'var(--ds-teal-200)',
|
|
73
|
+
border: 'var(--ds-teal-500)',
|
|
74
|
+
text: 'var(--ds-teal-900)',
|
|
75
|
+
secondary: 'var(--ds-gray-900)',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
// Default - Blue
|
|
68
80
|
return {
|
|
69
81
|
color: 'var(--ds-blue-600)',
|
package/src/lib/trace-builder.ts
CHANGED
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
hookToSpan,
|
|
13
13
|
runToSpan,
|
|
14
14
|
stepToSpan,
|
|
15
|
-
waitToSpan,
|
|
16
15
|
WORKFLOW_LIBRARY,
|
|
16
|
+
waitToSpan,
|
|
17
17
|
} from '../components/workflow-traces/trace-span-construction';
|
|
18
18
|
import { otelTimeToMs } from '../components/workflow-traces/trace-time-utils';
|
|
19
19
|
|
|
@@ -31,6 +31,37 @@ export const isHookLifecycleEvent = (eventType: string) =>
|
|
|
31
31
|
eventType === 'hook_created' ||
|
|
32
32
|
eventType === 'hook_disposed';
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Events that belong to the run root span rather than a child entity span.
|
|
36
|
+
* Mirrors the fallthrough logic in {@link groupEventsByCorrelation}: events
|
|
37
|
+
* without a correlationId (run lifecycle) plus correlated events that aren't
|
|
38
|
+
* step/timer/hook events (e.g. `attr_set`, whose correlationId is a dedup
|
|
39
|
+
* token rather than a child entity ID).
|
|
40
|
+
*/
|
|
41
|
+
export const isRunLevelEvent = (event: Event): boolean =>
|
|
42
|
+
!event.correlationId ||
|
|
43
|
+
(!isTimerEvent(event.eventType) &&
|
|
44
|
+
!isHookLifecycleEvent(event.eventType) &&
|
|
45
|
+
!isStepEvent(event.eventType));
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Filter the raw event list down to the events for a selected span.
|
|
49
|
+
* For the run root span this returns run-level events (see
|
|
50
|
+
* {@link isRunLevelEvent}); for child spans it returns the events
|
|
51
|
+
* correlated to that span's ID.
|
|
52
|
+
*/
|
|
53
|
+
export function filterSpanRawEvents(
|
|
54
|
+
events: Event[],
|
|
55
|
+
resource: string | undefined,
|
|
56
|
+
spanId: string | undefined
|
|
57
|
+
): Event[] {
|
|
58
|
+
if (resource === 'run') {
|
|
59
|
+
return events.filter(isRunLevelEvent);
|
|
60
|
+
}
|
|
61
|
+
if (!spanId) return [];
|
|
62
|
+
return events.filter((e) => e.correlationId === spanId);
|
|
63
|
+
}
|
|
64
|
+
|
|
34
65
|
// ---------------------------------------------------------------------------
|
|
35
66
|
// Event grouping
|
|
36
67
|
// ---------------------------------------------------------------------------
|
package/src/styles.css
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* import '@workflow/web-shared/styles.css';
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
@custom-variant dark-theme (&:where(.dark-theme, .dark-theme *, .dark, .dark *, [data-theme="dark"], [data-theme="dark"] *));
|
|
16
|
+
@custom-variant light-theme (&:where(.light-theme, .light-theme *, .light, .light *));
|
|
17
|
+
|
|
15
18
|
:root {
|
|
16
19
|
/* Background */
|
|
17
20
|
--ds-background-100: hsl(0, 0%, 100%);
|
|
@@ -103,6 +106,9 @@
|
|
|
103
106
|
|
|
104
107
|
/* Shadows */
|
|
105
108
|
--ds-shadow-small: 0px 1px 2px rgba(0, 0, 0, 0.04);
|
|
109
|
+
--ds-shadow-tooltip:
|
|
110
|
+
0 0 0 1px rgba(0, 0, 0, 0.08), 0px 1px 1px rgba(0, 0, 0, 0.02),
|
|
111
|
+
0px 4px 8px rgba(0, 0, 0, 0.04), 0 0 0 1px var(--ds-background-200);
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
/* Dark theme defaults */
|
|
@@ -187,6 +193,9 @@
|
|
|
187
193
|
--ds-pink-900: hsl(336, 80%, 70%);
|
|
188
194
|
|
|
189
195
|
--ds-shadow-small: 0px 1px 2px rgba(0, 0, 0, 0.3);
|
|
196
|
+
--ds-shadow-tooltip:
|
|
197
|
+
0 0 0 1px rgba(255, 255, 255, 0.145), 0px 1px 1px rgba(0, 0, 0, 0.02),
|
|
198
|
+
0px 4px 8px rgba(0, 0, 0, 0.04), 0 0 0 1px var(--ds-background-200);
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
@theme inline {
|