@workflow/web-shared 5.0.0-beta.42 → 5.0.0-beta.44
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 +4 -4
- package/dist/components/event-list-view.d.ts +12 -0
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +41 -20
- package/dist/components/sidebar/attribute-panel.d.ts +6 -2
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +10 -8
- package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
- package/dist/components/sidebar/copyable-data-block.js +2 -2
- package/dist/components/sidebar/entity-detail-panel.d.ts +6 -2
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +13 -5
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +12 -5
- package/dist/components/sidebar/resolve-hook-modal.js +1 -1
- package/dist/components/sidebar/sidebar-data-context.d.ts +2 -0
- package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -1
- package/dist/components/sidebar/sidebar-data-context.js +1 -1
- package/dist/components/stream-viewer.js +2 -2
- package/dist/components/trace-viewer/components/detail-panel-width.d.ts +4 -4
- package/dist/components/trace-viewer/components/detail-panel-width.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/detail-panel-width.js +5 -5
- package/dist/components/trace-viewer/components/detail-panel.d.ts +1 -1
- package/dist/components/trace-viewer/components/detail-panel.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/detail-panel.js +3 -3
- package/dist/components/trace-viewer/components/draggable-border.d.ts +1 -1
- package/dist/components/trace-viewer/components/draggable-border.js +2 -2
- package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.d.ts +9 -0
- package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.d.ts.map +1 -0
- package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.js +29 -0
- package/dist/components/trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/middle-truncate/middle-truncate.js +16 -8
- package/dist/components/trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/middle-truncate/use-middle-truncate.js +83 -19
- package/dist/components/trace-viewer/components/minimap.js +4 -4
- package/dist/components/trace-viewer/components/span-markers.d.ts +8 -9
- package/dist/components/trace-viewer/components/span-markers.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/span-markers.js +13 -10
- package/dist/components/trace-viewer/components/timeline.d.ts +1 -1
- package/dist/components/trace-viewer/components/timeline.js +4 -4
- package/dist/components/trace-viewer/components/use-row-window.d.ts +1 -1
- package/dist/components/trace-viewer/components/use-row-window.js +2 -2
- package/dist/components/trace-viewer/trace-viewer.js +3 -3
- package/dist/components/trace-viewer/utils.d.ts +3 -1
- package/dist/components/trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/trace-viewer/utils.js +6 -5
- package/dist/components/ui/data-inspector.d.ts +7 -1
- package/dist/components/ui/data-inspector.d.ts.map +1 -1
- package/dist/components/ui/data-inspector.js +10 -5
- package/dist/components/ui/decrypt-button.d.ts +5 -1
- package/dist/components/ui/decrypt-button.d.ts.map +1 -1
- package/dist/components/ui/decrypt-button.js +4 -4
- package/dist/components/ui/duplicate-event-tooltip.d.ts +15 -5
- package/dist/components/ui/duplicate-event-tooltip.d.ts.map +1 -1
- package/dist/components/ui/duplicate-event-tooltip.js +18 -9
- package/dist/components/ui/error-stack-block.js +4 -4
- package/dist/components/ui/timestamp-tooltip.d.ts +5 -3
- package/dist/components/ui/timestamp-tooltip.d.ts.map +1 -1
- package/dist/components/ui/timestamp-tooltip.js +9 -9
- package/dist/components/workflow-traces/event-colors.d.ts.map +1 -1
- package/dist/components/workflow-traces/event-colors.js +11 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/duplicate-events.d.ts +1 -1
- package/dist/lib/duplicate-events.js +2 -2
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +8 -13
- package/dist/lib/sealed-events.d.ts +28 -0
- package/dist/lib/sealed-events.d.ts.map +1 -0
- package/dist/lib/sealed-events.js +30 -0
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +8 -5
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.js +2 -2
- package/dist/lib/zstd-browser-decoder.js +2 -2
- package/package.json +5 -5
- package/src/components/event-list-view.tsx +57 -22
- package/src/components/sidebar/attribute-panel.tsx +15 -7
- package/src/components/sidebar/copyable-data-block.tsx +8 -1
- package/src/components/sidebar/entity-detail-panel.tsx +18 -2
- package/src/components/sidebar/events-list.tsx +16 -6
- package/src/components/sidebar/resolve-hook-modal.tsx +2 -2
- package/src/components/sidebar/sidebar-data-context.tsx +2 -0
- package/src/components/stream-viewer.tsx +1 -1
- package/src/components/trace-viewer/components/detail-panel-width.ts +4 -4
- package/src/components/trace-viewer/components/detail-panel.tsx +3 -1
- package/src/components/trace-viewer/components/draggable-border.tsx +1 -1
- package/src/components/trace-viewer/components/middle-truncate/measurement-batch.ts +47 -0
- package/src/components/trace-viewer/components/middle-truncate/middle-truncate.tsx +15 -7
- package/src/components/trace-viewer/components/middle-truncate/use-middle-truncate.ts +132 -38
- package/src/components/trace-viewer/components/minimap.tsx +3 -3
- package/src/components/trace-viewer/components/span-markers.tsx +18 -13
- package/src/components/trace-viewer/components/timeline.tsx +4 -4
- package/src/components/trace-viewer/components/use-row-window.ts +1 -1
- package/src/components/trace-viewer/trace-viewer.tsx +2 -2
- package/src/components/trace-viewer/utils.test.ts +5 -3
- package/src/components/trace-viewer/utils.ts +12 -8
- package/src/components/ui/data-inspector.tsx +19 -3
- package/src/components/ui/decrypt-button.tsx +9 -2
- package/src/components/ui/duplicate-event-tooltip.tsx +32 -10
- package/src/components/ui/error-stack-block.tsx +3 -3
- package/src/components/ui/timestamp-tooltip.tsx +16 -6
- package/src/components/workflow-traces/event-colors.ts +11 -0
- package/src/components/workflow-traces/trace-span-construction.test.ts +27 -1
- package/src/components/workflow-traces/trace-span-construction.ts +5 -3
- package/src/index.ts +4 -0
- package/src/lib/duplicate-events.ts +1 -1
- package/src/lib/hydration.ts +7 -12
- package/src/lib/sealed-events.ts +32 -0
- package/src/lib/trace-builder.test.ts +29 -1
- package/src/lib/trace-builder.ts +10 -5
- package/src/lib/utils.ts +1 -1
- package/src/lib/zstd-browser-decoder.ts +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface MeasurementTask<T> {
|
|
2
|
+
measure: (measurement: T) => void;
|
|
3
|
+
read: () => T | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type PendingMeasurement = () => (() => void) | null;
|
|
7
|
+
|
|
8
|
+
const pendingMeasurements = new Map<object, PendingMeasurement>();
|
|
9
|
+
let measurementFrame = 0;
|
|
10
|
+
|
|
11
|
+
function flushMeasurements(): void {
|
|
12
|
+
measurementFrame = 0;
|
|
13
|
+
|
|
14
|
+
const reads = [...pendingMeasurements.values()];
|
|
15
|
+
pendingMeasurements.clear();
|
|
16
|
+
const measures = reads.map((read) => read());
|
|
17
|
+
|
|
18
|
+
for (const measure of measures) {
|
|
19
|
+
measure?.();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function scheduleMeasurement<T>(
|
|
24
|
+
key: object,
|
|
25
|
+
{ read, measure }: MeasurementTask<T>
|
|
26
|
+
): void {
|
|
27
|
+
pendingMeasurements.set(key, () => {
|
|
28
|
+
const measurement = read();
|
|
29
|
+
return measurement === null ? null : () => measure(measurement);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (measurementFrame === 0) {
|
|
33
|
+
measurementFrame = requestAnimationFrame(flushMeasurements);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cancelMeasurement(key: object): void {
|
|
38
|
+
pendingMeasurements.delete(key);
|
|
39
|
+
|
|
40
|
+
if (pendingMeasurements.size === 0 && measurementFrame !== 0) {
|
|
41
|
+
cancelAnimationFrame(measurementFrame);
|
|
42
|
+
measurementFrame = 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { cancelMeasurement, scheduleMeasurement };
|
|
47
|
+
export type { MeasurementTask };
|
|
@@ -28,13 +28,20 @@ function getRangeOffsets(
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
let startRange: Range;
|
|
32
|
+
let endRange: Range;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
startRange = document.createRange();
|
|
36
|
+
startRange.selectNodeContents(container);
|
|
37
|
+
startRange.setEnd(range.startContainer, range.startOffset);
|
|
38
|
+
|
|
39
|
+
endRange = document.createRange();
|
|
40
|
+
endRange.selectNodeContents(container);
|
|
41
|
+
endRange.setEnd(range.endContainer, range.endOffset);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
38
45
|
|
|
39
46
|
return {
|
|
40
47
|
end: endRange.toString().length,
|
|
@@ -80,6 +87,7 @@ function MiddleTruncate({
|
|
|
80
87
|
let copyText: string | null = null;
|
|
81
88
|
|
|
82
89
|
if (
|
|
90
|
+
selectionText.includes(ELLIPSIS) &&
|
|
83
91
|
e.currentTarget.contains(range.startContainer) &&
|
|
84
92
|
e.currentTarget.contains(range.endContainer) &&
|
|
85
93
|
visibleEl
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useRef,
|
|
9
9
|
useState,
|
|
10
10
|
} from 'react';
|
|
11
|
+
import { cancelMeasurement, scheduleMeasurement } from './measurement-batch';
|
|
11
12
|
import { middleTruncate, toGraphemes } from './truncate';
|
|
12
13
|
|
|
13
14
|
const useIsomorphicLayoutEffect =
|
|
@@ -22,6 +23,17 @@ interface MiddleTruncateState {
|
|
|
22
23
|
suffixText: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
interface MiddleTruncateMeasurement {
|
|
27
|
+
availableWidth: number;
|
|
28
|
+
typography: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TextMeasurementCache {
|
|
32
|
+
typography: string;
|
|
33
|
+
value: string;
|
|
34
|
+
widths: Map<string, number>;
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
function createFullState(
|
|
26
38
|
value: string,
|
|
27
39
|
graphemes: string[]
|
|
@@ -62,7 +74,15 @@ function useMiddleTruncate(value: string): {
|
|
|
62
74
|
const ref = useRef<HTMLSpanElement | null>(null);
|
|
63
75
|
const measureRef = useRef<HTMLSpanElement | null>(null);
|
|
64
76
|
const [state, setState] = useState<MiddleTruncateState>(() => fullState);
|
|
65
|
-
const
|
|
77
|
+
const measurementKeyRef = useRef<object>({});
|
|
78
|
+
const textMeasurementCacheRef = useRef<TextMeasurementCache>({
|
|
79
|
+
typography: '',
|
|
80
|
+
value,
|
|
81
|
+
widths: new Map(),
|
|
82
|
+
});
|
|
83
|
+
const lastMeasurementRef = useRef<
|
|
84
|
+
(MiddleTruncateMeasurement & { value: string }) | null
|
|
85
|
+
>(null);
|
|
66
86
|
|
|
67
87
|
const updateState = useCallback((nextState: MiddleTruncateState) => {
|
|
68
88
|
setState((currentState) => {
|
|
@@ -81,76 +101,150 @@ function useMiddleTruncate(value: string): {
|
|
|
81
101
|
});
|
|
82
102
|
}, []);
|
|
83
103
|
|
|
84
|
-
const
|
|
104
|
+
const readMeasurement = useCallback((): MiddleTruncateMeasurement | null => {
|
|
85
105
|
const el = ref.current;
|
|
86
106
|
const measureEl = measureRef.current;
|
|
87
|
-
if (!el || !measureEl) return;
|
|
107
|
+
if (!el || !measureEl) return null;
|
|
88
108
|
|
|
89
|
-
const
|
|
109
|
+
const availableWidth = el.clientWidth;
|
|
110
|
+
const style = getComputedStyle(measureEl);
|
|
90
111
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
return {
|
|
113
|
+
availableWidth,
|
|
114
|
+
typography: [
|
|
115
|
+
style.fontFamily,
|
|
116
|
+
style.fontFeatureSettings,
|
|
117
|
+
style.fontKerning,
|
|
118
|
+
style.fontSize,
|
|
119
|
+
style.fontStretch,
|
|
120
|
+
style.fontStyle,
|
|
121
|
+
style.fontVariationSettings,
|
|
122
|
+
style.fontWeight,
|
|
123
|
+
style.letterSpacing,
|
|
124
|
+
style.textTransform,
|
|
125
|
+
].join('\0'),
|
|
99
126
|
};
|
|
127
|
+
}, []);
|
|
100
128
|
|
|
101
|
-
|
|
129
|
+
const recalculate = useCallback(
|
|
130
|
+
({ availableWidth, typography }: MiddleTruncateMeasurement) => {
|
|
131
|
+
const measureEl = measureRef.current;
|
|
132
|
+
if (!measureEl) return;
|
|
102
133
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
const lastMeasurement = lastMeasurementRef.current;
|
|
135
|
+
if (
|
|
136
|
+
lastMeasurement?.availableWidth === availableWidth &&
|
|
137
|
+
lastMeasurement.typography === typography &&
|
|
138
|
+
lastMeasurement.value === value
|
|
139
|
+
) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
107
142
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
143
|
+
lastMeasurementRef.current = {
|
|
144
|
+
availableWidth,
|
|
145
|
+
typography,
|
|
146
|
+
value,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (availableWidth <= 0) {
|
|
150
|
+
updateState(fullState);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const textMeasurementCache = textMeasurementCacheRef.current;
|
|
155
|
+
if (
|
|
156
|
+
textMeasurementCache.typography !== typography ||
|
|
157
|
+
textMeasurementCache.value !== value
|
|
158
|
+
) {
|
|
159
|
+
textMeasurementCache.typography = typography;
|
|
160
|
+
textMeasurementCache.value = value;
|
|
161
|
+
textMeasurementCache.widths.clear();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const measure = (text: string): number => {
|
|
165
|
+
const cachedWidth = textMeasurementCache.widths.get(text);
|
|
166
|
+
if (cachedWidth !== undefined) {
|
|
167
|
+
return cachedWidth;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
measureEl.textContent = text;
|
|
171
|
+
const width = measureEl.scrollWidth;
|
|
172
|
+
textMeasurementCache.widths.set(text, width);
|
|
173
|
+
return width;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const fullWidth = measure(value);
|
|
177
|
+
|
|
178
|
+
if (fullWidth <= availableWidth) {
|
|
179
|
+
updateState(fullState);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const result = middleTruncate(
|
|
184
|
+
graphemes,
|
|
185
|
+
availableWidth,
|
|
186
|
+
measure,
|
|
187
|
+
fullWidth
|
|
188
|
+
);
|
|
189
|
+
updateState({
|
|
190
|
+
displayText: result.text,
|
|
191
|
+
isTruncated: result.truncated,
|
|
192
|
+
prefixGraphemeCount: result.prefixGraphemeCount,
|
|
193
|
+
prefixText: result.prefixText,
|
|
194
|
+
suffixGraphemeCount: result.suffixGraphemeCount,
|
|
195
|
+
suffixText: result.suffixText,
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
[fullState, graphemes, updateState, value]
|
|
199
|
+
);
|
|
118
200
|
|
|
119
201
|
// Measure on mount and when value changes - before paint
|
|
120
202
|
useIsomorphicLayoutEffect(() => {
|
|
121
|
-
|
|
122
|
-
|
|
203
|
+
const measurement = readMeasurement();
|
|
204
|
+
if (measurement) {
|
|
205
|
+
recalculate(measurement);
|
|
206
|
+
}
|
|
207
|
+
}, [readMeasurement, recalculate]);
|
|
123
208
|
|
|
124
209
|
// ResizeObserver + font loading for ongoing responsiveness
|
|
125
210
|
useEffect(() => {
|
|
126
211
|
const el = ref.current;
|
|
127
212
|
if (!el) return;
|
|
128
213
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
214
|
+
const measurementKey = measurementKeyRef.current;
|
|
215
|
+
const scheduleRecalculation = (): void => {
|
|
216
|
+
scheduleMeasurement(measurementKey, {
|
|
217
|
+
read: readMeasurement,
|
|
218
|
+
measure: recalculate,
|
|
219
|
+
});
|
|
132
220
|
};
|
|
133
221
|
|
|
134
222
|
const ro =
|
|
135
223
|
typeof ResizeObserver !== 'undefined'
|
|
136
|
-
? new ResizeObserver(
|
|
224
|
+
? new ResizeObserver(scheduleRecalculation)
|
|
137
225
|
: null;
|
|
138
226
|
ro?.observe(el);
|
|
139
|
-
|
|
227
|
+
if (!ro) {
|
|
228
|
+
window.addEventListener('resize', scheduleRecalculation);
|
|
229
|
+
}
|
|
140
230
|
|
|
141
231
|
const onFontsLoaded = (): void => {
|
|
142
|
-
|
|
232
|
+
textMeasurementCacheRef.current.widths.clear();
|
|
233
|
+
lastMeasurementRef.current = null;
|
|
234
|
+
scheduleRecalculation();
|
|
143
235
|
};
|
|
144
236
|
const fontSet = 'fonts' in document ? document.fonts : null;
|
|
145
237
|
fontSet?.addEventListener?.('loadingdone', onFontsLoaded);
|
|
146
238
|
|
|
147
239
|
return () => {
|
|
148
240
|
ro?.disconnect();
|
|
149
|
-
|
|
150
|
-
|
|
241
|
+
if (!ro) {
|
|
242
|
+
window.removeEventListener('resize', scheduleRecalculation);
|
|
243
|
+
}
|
|
244
|
+
cancelMeasurement(measurementKey);
|
|
151
245
|
fontSet?.removeEventListener?.('loadingdone', onFontsLoaded);
|
|
152
246
|
};
|
|
153
|
-
}, [recalculate]);
|
|
247
|
+
}, [readMeasurement, recalculate]);
|
|
154
248
|
|
|
155
249
|
return {
|
|
156
250
|
ref,
|
|
@@ -200,9 +200,9 @@ export const Minimap = memo(function Minimap({
|
|
|
200
200
|
|
|
201
201
|
// Density canvas: one thin line per span across the full run. Sizing is
|
|
202
202
|
// read live and redraws run straight from the ResizeObserver (which fires
|
|
203
|
-
// before paint), so layout changes
|
|
203
|
+
// before paint), so layout changes (e.g. the detail panel opening) never
|
|
204
204
|
// paint a frame with a stale, stretched bitmap.
|
|
205
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: themeVersion is a redraw trigger
|
|
205
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: themeVersion is a redraw trigger; the canvas re-resolves its token colors when the theme flips
|
|
206
206
|
useLayoutEffect(() => {
|
|
207
207
|
const canvas = canvasRef.current;
|
|
208
208
|
const container = containerRef.current;
|
|
@@ -276,7 +276,7 @@ export const Minimap = memo(function Minimap({
|
|
|
276
276
|
return { mode: 'resize-right' };
|
|
277
277
|
}
|
|
278
278
|
// Panning a full-extent window is a no-op, so let a drag on the brush
|
|
279
|
-
// start a selection instead
|
|
279
|
+
// start a selection instead, the natural first gesture on the map.
|
|
280
280
|
const { start, end } = viewportRef.current;
|
|
281
281
|
const isFullExtent =
|
|
282
282
|
start - rootStartMs < rootDurationMs * 0.001 &&
|
|
@@ -4,7 +4,12 @@ import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
|
4
4
|
import type { ReactNode } from 'react';
|
|
5
5
|
import { cn } from '../../../lib/cn';
|
|
6
6
|
import { TimestampTooltip } from '../../ui/timestamp-tooltip';
|
|
7
|
-
import type { SpanMarker } from '../utils';
|
|
7
|
+
import type { SpanMarker, SpanMarkerKind } from '../utils';
|
|
8
|
+
|
|
9
|
+
const MARKER_KIND_PREFIX: Record<SpanMarkerKind, string> = {
|
|
10
|
+
hook_received: 'Hook received',
|
|
11
|
+
attr_set: 'Attribute set',
|
|
12
|
+
};
|
|
8
13
|
|
|
9
14
|
// ---------------------------------------------------------------------------
|
|
10
15
|
// Marker projection
|
|
@@ -12,8 +17,9 @@ import type { SpanMarker } from '../utils';
|
|
|
12
17
|
|
|
13
18
|
export interface VisibleMarker {
|
|
14
19
|
leftPct: number;
|
|
15
|
-
/** Absolute (epoch) timestamp in ms
|
|
20
|
+
/** Absolute (epoch) timestamp in ms, for the tooltip. */
|
|
16
21
|
timeMs: number;
|
|
22
|
+
kind: SpanMarkerKind;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
/**
|
|
@@ -37,6 +43,7 @@ export function projectMarkers(
|
|
|
37
43
|
{
|
|
38
44
|
leftPct: ((m.timeMs - visibleStartMs) / visibleDurationMs) * 100,
|
|
39
45
|
timeMs: m.timeMs,
|
|
46
|
+
kind: m.kind,
|
|
40
47
|
},
|
|
41
48
|
];
|
|
42
49
|
});
|
|
@@ -48,7 +55,7 @@ const MARKER_MIN_GAP_PX = 16;
|
|
|
48
55
|
/**
|
|
49
56
|
* Thin out ticks that would visually collide: walk left-to-right and keep each
|
|
50
57
|
* one unless it sits within MARKER_MIN_GAP_PX of the last kept tick. Well-spaced
|
|
51
|
-
* markers survive even when a tight cluster elsewhere on the bar gets thinned
|
|
58
|
+
* markers survive even when a tight cluster elsewhere on the bar gets thinned;
|
|
52
59
|
* zoom in to resolve a cluster.
|
|
53
60
|
*/
|
|
54
61
|
export function cullCollidingMarkers(
|
|
@@ -80,13 +87,11 @@ function MarkerTick({ className }: { className?: string }): ReactNode {
|
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
/**
|
|
83
|
-
* Point-in-time markers overlaid on a bar
|
|
90
|
+
* Point-in-time markers overlaid on a bar: one vertical tick per event (hook
|
|
84
91
|
* resumptions and attribute writes), centered on the bar. Each tick sits inside
|
|
85
|
-
* a larger hit target and, on hover, shows the shared
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* of the product. The position is clamped a hair inside the bar so an edge
|
|
89
|
-
* marker never jams the rounded corner.
|
|
92
|
+
* a larger hit target and, on hover, shows the shared relative-time context
|
|
93
|
+
* card, prefixed with the event kind. The position is clamped a hair inside
|
|
94
|
+
* the bar so an edge marker never jams the rounded corner.
|
|
90
95
|
*/
|
|
91
96
|
export function MarkerLayer({
|
|
92
97
|
markers,
|
|
@@ -95,13 +100,13 @@ export function MarkerLayer({
|
|
|
95
100
|
}): ReactNode {
|
|
96
101
|
return (
|
|
97
102
|
<>
|
|
98
|
-
{markers.map((m) => (
|
|
103
|
+
{markers.map((m, index) => (
|
|
99
104
|
<span
|
|
100
|
-
key={m.timeMs}
|
|
105
|
+
key={`${m.timeMs}-${index}`}
|
|
101
106
|
className="pointer-events-auto absolute top-0 bottom-0 z-10 flex w-8 -translate-x-1/2 items-center justify-center"
|
|
102
107
|
style={{ left: `clamp(8px, ${m.leftPct}%, calc(100% - 8px))` }}
|
|
103
108
|
>
|
|
104
|
-
<TimestampTooltip date={m.timeMs}>
|
|
109
|
+
<TimestampTooltip date={m.timeMs} prefix={MARKER_KIND_PREFIX[m.kind]}>
|
|
105
110
|
<span className="flex h-6 w-8 items-center justify-center">
|
|
106
111
|
<MarkerTick className="h-3" />
|
|
107
112
|
</span>
|
|
@@ -147,7 +152,7 @@ export function OffscreenMarkerIndicator({
|
|
|
147
152
|
aria-label={label}
|
|
148
153
|
title={label}
|
|
149
154
|
onClick={(e) => {
|
|
150
|
-
// Don't let the row's onClick fire
|
|
155
|
+
// Don't let the row's onClick fire, since revealing shouldn't also
|
|
151
156
|
// change the span selection.
|
|
152
157
|
e.stopPropagation();
|
|
153
158
|
onReveal?.(targetMs);
|
|
@@ -432,7 +432,7 @@ const TimelineBar = memo(function TimelineBar({
|
|
|
432
432
|
);
|
|
433
433
|
|
|
434
434
|
// Markers that fall outside the visible window (scrolled off while zoomed in)
|
|
435
|
-
//
|
|
435
|
+
// are surfaced as edge indicators so they aren't silently lost.
|
|
436
436
|
const offscreen = useMemo<OffscreenMarkers>(
|
|
437
437
|
() =>
|
|
438
438
|
geometry.mode.kind === 'full'
|
|
@@ -543,7 +543,7 @@ export { TimelineBar };
|
|
|
543
543
|
// ---------------------------------------------------------------------------
|
|
544
544
|
|
|
545
545
|
// Horizontal distance between the anchor bar's measured edge and the vertical
|
|
546
|
-
// guide
|
|
546
|
+
// guide; also the width of the connector stub bridging the two.
|
|
547
547
|
const MEASURE_GUIDE_OUTSET_PX = 4;
|
|
548
548
|
|
|
549
549
|
const DeltaMeasureLine = memo(function DeltaMeasureLine({
|
|
@@ -567,7 +567,7 @@ const DeltaMeasureLine = memo(function DeltaMeasureLine({
|
|
|
567
567
|
// It sits just outside the anchor bar's measured edge (so it doesn't blend
|
|
568
568
|
// into the bar's border), joined to the bar by a short horizontal stub. The
|
|
569
569
|
// line runs from the elbow corner (the guide's x) to the hovered span's
|
|
570
|
-
// measured edge
|
|
570
|
+
// measured edge, pulled short of the edge arrow when the hovered span is
|
|
571
571
|
// fully off-screen.
|
|
572
572
|
const guideTop = Math.min(anchorCenterY, lineY);
|
|
573
573
|
const guideBottom = Math.max(anchorCenterY, lineY);
|
|
@@ -675,7 +675,7 @@ export function TimelineHeader({
|
|
|
675
675
|
export interface TimelineHover {
|
|
676
676
|
/** Pointer x as a fraction of the timeline's content width, in [0, 1]. */
|
|
677
677
|
fraction: number;
|
|
678
|
-
/** Row index under the pointer; may be past the last row
|
|
678
|
+
/** Row index under the pointer; may be past the last row (not validated). */
|
|
679
679
|
rowIndex: number;
|
|
680
680
|
}
|
|
681
681
|
|
|
@@ -96,7 +96,7 @@ export const ROW_HEIGHT_PX = 40;
|
|
|
96
96
|
* Scroll a windowed row into view by index.
|
|
97
97
|
*
|
|
98
98
|
* The list is virtualized (fixed `rowHeight` rows), so an off-screen row has no
|
|
99
|
-
* DOM node to `scrollIntoView
|
|
99
|
+
* DOM node to `scrollIntoView`; its target offset is computed from its index
|
|
100
100
|
* instead. Walks up from `listEl` to the shared scrollable ancestor (the same
|
|
101
101
|
* one `useRowWindow` measures against), only scrolls when the row sits outside
|
|
102
102
|
* the visible area, leaves a one-row `margin` of breathing room past it, and
|
|
@@ -231,7 +231,7 @@ function TraceViewerContent({
|
|
|
231
231
|
[root.startTime, root.endTime]
|
|
232
232
|
);
|
|
233
233
|
|
|
234
|
-
// Pan (keeping the current zoom) so `timeMs` is centered in view
|
|
234
|
+
// Pan (keeping the current zoom) so `timeMs` is centered in view; used by the
|
|
235
235
|
// off-screen marker indicators to scroll their marker into view.
|
|
236
236
|
const handleRevealTime = useCallback(
|
|
237
237
|
(timeMs: number) => {
|
|
@@ -307,7 +307,7 @@ function TraceViewerContent({
|
|
|
307
307
|
|
|
308
308
|
// Bring a row into view when keyboard/button navigation lands on a span that
|
|
309
309
|
// sits outside the shared scroll container's visible area. The list is
|
|
310
|
-
// windowed, so an off-screen row has no DOM node to `scrollIntoView
|
|
310
|
+
// windowed, so an off-screen row has no DOM node to `scrollIntoView`;
|
|
311
311
|
// `scrollRowIntoView` computes the target offset from the span's index.
|
|
312
312
|
const scrollSpanIntoView = useCallback(
|
|
313
313
|
(spanId: string) => {
|
|
@@ -153,8 +153,10 @@ describe('computeSpanMarkers', () => {
|
|
|
153
153
|
attrSetMs: [10_000, 70_000],
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
-
expect(computeSpanMarkers(span)
|
|
157
|
-
10_000,
|
|
156
|
+
expect(computeSpanMarkers(span)).toEqual([
|
|
157
|
+
{ timeMs: 10_000, kind: 'attr_set' },
|
|
158
|
+
{ timeMs: 50_000, kind: 'hook_received' },
|
|
159
|
+
{ timeMs: 70_000, kind: 'attr_set' },
|
|
158
160
|
]);
|
|
159
161
|
});
|
|
160
162
|
|
|
@@ -165,7 +167,7 @@ describe('computeSpanMarkers', () => {
|
|
|
165
167
|
});
|
|
166
168
|
|
|
167
169
|
describe('computeOffscreenMarkers', () => {
|
|
168
|
-
const mk = (timeMs: number) => ({ timeMs });
|
|
170
|
+
const mk = (timeMs: number) => ({ timeMs, kind: 'hook_received' as const });
|
|
169
171
|
|
|
170
172
|
it('partitions markers by side with the nearest one per side', () => {
|
|
171
173
|
const markers = [mk(5), mk(8), mk(50), mk(92), mk(99)];
|
|
@@ -78,7 +78,7 @@ export function clampViewportToRoot(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// ---------------------------------------------------------------------------
|
|
81
|
-
// Wheel gestures
|
|
81
|
+
// Wheel gestures: shared between the timeline and the minimap
|
|
82
82
|
// ---------------------------------------------------------------------------
|
|
83
83
|
|
|
84
84
|
/** Convert a wheel delta to pixel units (line-mode deltas arrive in lines). */
|
|
@@ -164,7 +164,7 @@ export function computeTimeMarkers(
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// ---------------------------------------------------------------------------
|
|
167
|
-
// Span gaps
|
|
167
|
+
// Span gaps: time deltas between consecutive spans (Alt-key overlay)
|
|
168
168
|
// ---------------------------------------------------------------------------
|
|
169
169
|
|
|
170
170
|
export interface SpanGap {
|
|
@@ -254,8 +254,8 @@ export function computeSpanDelta(
|
|
|
254
254
|
const deltaMs = laterStart - originMs;
|
|
255
255
|
|
|
256
256
|
// Entirely outside the viewport. Compared in time-space (not clamped
|
|
257
|
-
// fractions) so a zero-delta point sitting exactly on a viewport edge
|
|
258
|
-
// e.g. the root span selected at default zoom
|
|
257
|
+
// fractions) so a zero-delta point sitting exactly on a viewport edge
|
|
258
|
+
// (e.g. the root span selected at default zoom) still renders.
|
|
259
259
|
if (laterStart < viewStart || originMs > viewEnd) {
|
|
260
260
|
return null;
|
|
261
261
|
}
|
|
@@ -294,7 +294,7 @@ export const RESOURCE_CLASS_NAMES: Record<
|
|
|
294
294
|
className: 'border-green-500 bg-green-200',
|
|
295
295
|
errorClassName: 'border-red-500 bg-red-200',
|
|
296
296
|
},
|
|
297
|
-
// Passive spans (hooks) stay gray
|
|
297
|
+
// Passive spans (hooks) stay gray; matches event-list icons and the minimap.
|
|
298
298
|
hook: {
|
|
299
299
|
className: 'border-gray-500 bg-gray-200',
|
|
300
300
|
errorClassName: 'border-red-500 bg-red-200',
|
|
@@ -317,7 +317,7 @@ export function getResourceClassNames(resource: string): {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
// ---------------------------------------------------------------------------
|
|
320
|
-
// Span segments
|
|
320
|
+
// Span segments: split a timeline bar into colored sections by event state
|
|
321
321
|
// ---------------------------------------------------------------------------
|
|
322
322
|
|
|
323
323
|
export type SegmentStatus =
|
|
@@ -603,11 +603,14 @@ export function computeSpanSegments(span: Span): Segment[] {
|
|
|
603
603
|
}
|
|
604
604
|
|
|
605
605
|
// ---------------------------------------------------------------------------
|
|
606
|
-
// Span markers
|
|
606
|
+
// Span markers: point-in-time events rendered as ticks on top of a bar
|
|
607
607
|
// ---------------------------------------------------------------------------
|
|
608
608
|
|
|
609
|
+
export type SpanMarkerKind = 'hook_received' | 'attr_set';
|
|
610
|
+
|
|
609
611
|
export interface SpanMarker {
|
|
610
612
|
timeMs: number;
|
|
613
|
+
kind: SpanMarkerKind;
|
|
611
614
|
}
|
|
612
615
|
|
|
613
616
|
// `hook_received` = a resumption; `attr_set` = attributes written mid-span.
|
|
@@ -616,12 +619,13 @@ const MARKER_EVENT_NAMES = ['hook_received', 'attr_set'];
|
|
|
616
619
|
export function computeSpanMarkers(span: Span): SpanMarker[] {
|
|
617
620
|
return sortedEventMarks(span.events, MARKER_EVENT_NAMES).map((mark) => ({
|
|
618
621
|
timeMs: mark.time,
|
|
622
|
+
kind: mark.type as SpanMarkerKind,
|
|
619
623
|
}));
|
|
620
624
|
}
|
|
621
625
|
|
|
622
626
|
export interface OffscreenSide {
|
|
623
627
|
count: number;
|
|
624
|
-
/** Nearest off-screen marker
|
|
628
|
+
/** Nearest off-screen marker: the one a reveal jumps to. */
|
|
625
629
|
nearestMs: number;
|
|
626
630
|
}
|
|
627
631
|
|
|
@@ -145,6 +145,8 @@ export const StreamClickContext = createContext<
|
|
|
145
145
|
export type DecryptClickContextValue = {
|
|
146
146
|
onDecrypt: () => void;
|
|
147
147
|
isDecrypting: boolean;
|
|
148
|
+
isDecryptDisabled?: boolean;
|
|
149
|
+
decryptDisabledReason?: string;
|
|
148
150
|
hasEncryptedData?: boolean;
|
|
149
151
|
};
|
|
150
152
|
|
|
@@ -167,7 +169,8 @@ function EncryptedInlineLabel() {
|
|
|
167
169
|
<Button
|
|
168
170
|
size="xs"
|
|
169
171
|
className="align-baseline gap-x-1"
|
|
170
|
-
disabled={ctx.isDecrypting}
|
|
172
|
+
disabled={ctx.isDecrypting || ctx.isDecryptDisabled}
|
|
173
|
+
title={ctx.isDecryptDisabled ? ctx.decryptDisabledReason : undefined}
|
|
171
174
|
onClick={(e) => {
|
|
172
175
|
e.stopPropagation();
|
|
173
176
|
ctx.onDecrypt();
|
|
@@ -842,7 +845,7 @@ export function collapseRefs(data: unknown): unknown {
|
|
|
842
845
|
if (data instanceof Set) {
|
|
843
846
|
return new Set(Array.from(data.values(), collapseRefs));
|
|
844
847
|
}
|
|
845
|
-
// Only recurse into plain objects
|
|
848
|
+
// Only recurse into plain objects; leave class instances untouched
|
|
846
849
|
const proto = Object.getPrototypeOf(data);
|
|
847
850
|
if (proto !== Object.prototype && proto !== null) return data;
|
|
848
851
|
const result: Record<string, unknown> = {};
|
|
@@ -871,6 +874,10 @@ export interface DataInspectorProps {
|
|
|
871
874
|
onDecrypt?: () => void;
|
|
872
875
|
/** Whether decryption is currently in progress */
|
|
873
876
|
isDecrypting?: boolean;
|
|
877
|
+
/** Whether decryption is unavailable */
|
|
878
|
+
isDecryptDisabled?: boolean;
|
|
879
|
+
/** Explains why decryption is unavailable */
|
|
880
|
+
decryptDisabledReason?: string;
|
|
874
881
|
}
|
|
875
882
|
|
|
876
883
|
export function DataInspector({
|
|
@@ -881,6 +888,8 @@ export function DataInspector({
|
|
|
881
888
|
onRunClick,
|
|
882
889
|
onDecrypt,
|
|
883
890
|
isDecrypting = false,
|
|
891
|
+
isDecryptDisabled = false,
|
|
892
|
+
decryptDisabledReason,
|
|
884
893
|
}: DataInspectorProps) {
|
|
885
894
|
const collapsedData = useMemo(() => collapseRefs(data), [data]);
|
|
886
895
|
const stableData = useStableInspectorData(collapsedData);
|
|
@@ -912,7 +921,14 @@ export function DataInspector({
|
|
|
912
921
|
}
|
|
913
922
|
if (onDecrypt) {
|
|
914
923
|
content = (
|
|
915
|
-
<DecryptClickContext.Provider
|
|
924
|
+
<DecryptClickContext.Provider
|
|
925
|
+
value={{
|
|
926
|
+
onDecrypt,
|
|
927
|
+
isDecrypting,
|
|
928
|
+
isDecryptDisabled,
|
|
929
|
+
decryptDisabledReason,
|
|
930
|
+
}}
|
|
931
|
+
>
|
|
916
932
|
{content}
|
|
917
933
|
</DecryptClickContext.Provider>
|
|
918
934
|
);
|