@workflow/web-shared 4.1.11 → 4.1.12
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 +6 -2
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +29 -33
- package/dist/components/trace-viewer/components/map.d.ts +2 -2
- package/dist/components/trace-viewer/components/map.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/node.d.ts +7 -7
- package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.module.css +4 -4
- package/dist/components/ui/alert.js +2 -2
- package/dist/components/ui/card.js +2 -2
- 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/error-card.js +2 -2
- package/dist/components/ui/skeleton.js +2 -2
- 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-traces/trace-span-construction.d.ts +4 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +30 -9
- package/dist/hooks/use-reduced-motion.d.ts +10 -0
- package/dist/hooks/use-reduced-motion.d.ts.map +1 -0
- package/dist/hooks/use-reduced-motion.js +29 -0
- package/dist/lib/cn.d.ts +10 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +83 -0
- package/dist/lib/event-materialization.d.ts +3 -0
- package/dist/lib/event-materialization.d.ts.map +1 -1
- package/dist/lib/event-materialization.js +13 -7
- package/dist/lib/utils.d.ts +0 -2
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +1 -6
- package/dist/styles.css +9 -0
- package/package.json +6 -4
- package/src/components/event-list-view.tsx +10 -1
- package/src/components/sidebar/attribute-panel.tsx +122 -121
- package/src/components/trace-viewer/trace-viewer.module.css +4 -4
- package/src/components/ui/alert.tsx +1 -1
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/context-card.tsx +784 -0
- package/src/components/ui/error-card.tsx +1 -1
- package/src/components/ui/skeleton.tsx +1 -1
- package/src/components/ui/timestamp-tooltip.tsx +132 -194
- package/src/components/workflow-traces/trace-span-construction.ts +72 -30
- package/src/hooks/use-reduced-motion.ts +34 -0
- package/src/lib/cn.ts +93 -0
- package/src/lib/event-materialization.ts +68 -40
- package/src/lib/utils.ts +0 -6
- 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
|
}
|
|
@@ -28,6 +28,27 @@ const MARKER_EVENT_TYPES: Set<Event['eventType']> = new Set([
|
|
|
28
28
|
'wait_completed',
|
|
29
29
|
]);
|
|
30
30
|
|
|
31
|
+
const findEventByType = (
|
|
32
|
+
events: Event[],
|
|
33
|
+
eventTypes: Event['eventType'][]
|
|
34
|
+
): Event | undefined => {
|
|
35
|
+
for (const eventType of eventTypes) {
|
|
36
|
+
const event = events.find((e) => e.eventType === eventType);
|
|
37
|
+
if (event) return event;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const withOccurredAt = <T extends object>(
|
|
43
|
+
entity: T,
|
|
44
|
+
occurredAt: Event['occurredAt'] | undefined
|
|
45
|
+
): T & { occurredAt?: Date } => {
|
|
46
|
+
if (!occurredAt || (entity as { occurredAt?: unknown }).occurredAt != null) {
|
|
47
|
+
return entity;
|
|
48
|
+
}
|
|
49
|
+
return { ...entity, occurredAt };
|
|
50
|
+
};
|
|
51
|
+
|
|
31
52
|
/**
|
|
32
53
|
* Convert workflow events to span events
|
|
33
54
|
* Only includes events that should be displayed as markers
|
|
@@ -59,7 +80,8 @@ export const waitEventsToWaitEntity = (
|
|
|
59
80
|
waitId: string;
|
|
60
81
|
runId: string;
|
|
61
82
|
createdAt: Date;
|
|
62
|
-
|
|
83
|
+
occurredAt?: Date;
|
|
84
|
+
resumeAt?: Date;
|
|
63
85
|
completedAt?: Date;
|
|
64
86
|
} | null => {
|
|
65
87
|
const startEvent = events.find((event) => event.eventType === 'wait_created');
|
|
@@ -69,13 +91,18 @@ export const waitEventsToWaitEntity = (
|
|
|
69
91
|
const completedEvent = events.find(
|
|
70
92
|
(event) => event.eventType === 'wait_completed'
|
|
71
93
|
);
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
return withOccurredAt(
|
|
95
|
+
{
|
|
96
|
+
waitId: startEvent.correlationId,
|
|
97
|
+
runId: startEvent.runId,
|
|
98
|
+
createdAt: startEvent.createdAt,
|
|
99
|
+
resumeAt: startEvent.eventData?.resumeAt
|
|
100
|
+
? new Date(startEvent.eventData.resumeAt)
|
|
101
|
+
: undefined,
|
|
102
|
+
completedAt: completedEvent?.createdAt,
|
|
103
|
+
},
|
|
104
|
+
startEvent.occurredAt
|
|
105
|
+
);
|
|
79
106
|
};
|
|
80
107
|
|
|
81
108
|
/**
|
|
@@ -122,6 +149,7 @@ export const stepEventsToStepEntity = (
|
|
|
122
149
|
attempt: number;
|
|
123
150
|
createdAt: Date;
|
|
124
151
|
updatedAt: Date;
|
|
152
|
+
occurredAt?: Date;
|
|
125
153
|
startedAt?: Date;
|
|
126
154
|
completedAt?: Date;
|
|
127
155
|
specVersion?: number;
|
|
@@ -172,18 +200,24 @@ export const stepEventsToStepEntity = (
|
|
|
172
200
|
if (attempt === 0) attempt = 1;
|
|
173
201
|
|
|
174
202
|
const lastEvent = events[events.length - 1];
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
203
|
+
const occurrenceEvent =
|
|
204
|
+
findEventByType(events, ['step_created', 'step_started']) ?? anchorEvent;
|
|
205
|
+
|
|
206
|
+
return withOccurredAt(
|
|
207
|
+
{
|
|
208
|
+
stepId: anchorEvent.correlationId ?? '',
|
|
209
|
+
runId: anchorEvent.runId,
|
|
210
|
+
stepName: createdEvent?.eventData?.stepName ?? '',
|
|
211
|
+
status,
|
|
212
|
+
attempt,
|
|
213
|
+
createdAt: anchorEvent.createdAt,
|
|
214
|
+
updatedAt: lastEvent?.createdAt ?? anchorEvent.createdAt,
|
|
215
|
+
startedAt,
|
|
216
|
+
completedAt,
|
|
217
|
+
specVersion: anchorEvent.specVersion,
|
|
218
|
+
},
|
|
219
|
+
occurrenceEvent.occurredAt
|
|
220
|
+
);
|
|
187
221
|
};
|
|
188
222
|
|
|
189
223
|
/**
|
|
@@ -252,6 +286,7 @@ export const hookEventsToHookEntity = (
|
|
|
252
286
|
runId: string;
|
|
253
287
|
token?: string;
|
|
254
288
|
createdAt: Date;
|
|
289
|
+
occurredAt?: Date;
|
|
255
290
|
receivedCount: number;
|
|
256
291
|
lastReceivedAt?: Date;
|
|
257
292
|
disposedAt?: Date;
|
|
@@ -269,15 +304,18 @@ export const hookEventsToHookEntity = (
|
|
|
269
304
|
(event) => event.eventType === 'hook_disposed'
|
|
270
305
|
);
|
|
271
306
|
const lastReceivedEvent = receivedEvents.at(-1);
|
|
272
|
-
return
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
307
|
+
return withOccurredAt(
|
|
308
|
+
{
|
|
309
|
+
hookId: createdEvent.correlationId,
|
|
310
|
+
runId: createdEvent.runId,
|
|
311
|
+
token: createdEvent.eventData?.token,
|
|
312
|
+
createdAt: createdEvent.createdAt,
|
|
313
|
+
receivedCount: receivedEvents.length,
|
|
314
|
+
lastReceivedAt: lastReceivedEvent?.createdAt || undefined,
|
|
315
|
+
disposedAt: disposedEvents.at(-1)?.createdAt || undefined,
|
|
316
|
+
},
|
|
317
|
+
createdEvent.occurredAt
|
|
318
|
+
);
|
|
281
319
|
};
|
|
282
320
|
|
|
283
321
|
/**
|
|
@@ -327,9 +365,13 @@ export function runToSpan(
|
|
|
327
365
|
// Only embed identification fields — not the full object with
|
|
328
366
|
// input/output/error which may contain non-cloneable types.
|
|
329
367
|
const { input: _i, output: _o, error: _e, ...runIdentity } = run;
|
|
368
|
+
const occurrenceEvent = findEventByType(runEvents, [
|
|
369
|
+
'run_created',
|
|
370
|
+
'run_started',
|
|
371
|
+
]);
|
|
330
372
|
const attributes = {
|
|
331
373
|
resource: 'run' as const,
|
|
332
|
-
data: runIdentity,
|
|
374
|
+
data: withOccurredAt(runIdentity, occurrenceEvent?.occurredAt),
|
|
333
375
|
};
|
|
334
376
|
|
|
335
377
|
// Use createdAt as span start time, with activeStartTime for when execution began
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
const QUERY = '(prefers-reduced-motion: reduce)';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook that detects whether the user has requested reduced motion and reacts
|
|
9
|
+
* to changes. Mirrors the `prefers-reduced-motion: reduce` media query so
|
|
10
|
+
* components can skip or shorten animations for users with motion
|
|
11
|
+
* sensitivities (e.g. vestibular disorders).
|
|
12
|
+
*
|
|
13
|
+
* @returns `true` if the user prefers reduced motion, `false` otherwise
|
|
14
|
+
*/
|
|
15
|
+
export const useReducedMotion = (): boolean => {
|
|
16
|
+
const [reduced, setReduced] = useState(() => {
|
|
17
|
+
if (typeof window === 'undefined' || !window.matchMedia) return false;
|
|
18
|
+
return window.matchMedia(QUERY).matches;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (typeof window === 'undefined' || !window.matchMedia) return;
|
|
23
|
+
|
|
24
|
+
const media = window.matchMedia(QUERY);
|
|
25
|
+
const onChange = (): void => setReduced(media.matches);
|
|
26
|
+
|
|
27
|
+
media.addEventListener('change', onChange);
|
|
28
|
+
onChange();
|
|
29
|
+
|
|
30
|
+
return () => media.removeEventListener('change', onChange);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return reduced;
|
|
34
|
+
};
|