@workflow/web-shared 5.0.0-beta.35 → 5.0.0-beta.37
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/new-trace-viewer/components/event-list.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/event-list.js +10 -7
- package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/split-pane.js +5 -5
- package/dist/components/new-trace-viewer/components/timeline.d.ts +8 -2
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +95 -26
- package/dist/components/new-trace-viewer/components/trace-shortcut-helper.js +2 -2
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +24 -22
- package/dist/components/new-trace-viewer/utils.d.ts +29 -0
- package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/utils.js +46 -3
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +13 -5
- package/package.json +5 -5
- package/src/components/new-trace-viewer/components/event-list.tsx +21 -11
- package/src/components/new-trace-viewer/components/split-pane.tsx +14 -4
- package/src/components/new-trace-viewer/components/timeline.tsx +158 -47
- package/src/components/new-trace-viewer/components/trace-shortcut-helper.tsx +1 -1
- package/src/components/new-trace-viewer/trace-viewer.tsx +54 -50
- package/src/components/new-trace-viewer/utils.test.ts +199 -0
- package/src/components/new-trace-viewer/utils.ts +79 -2
- package/src/components/sidebar/attribute-panel.tsx +6 -0
- package/src/index.ts +1 -4
- package/src/lib/hydration.ts +12 -6
|
@@ -3,6 +3,7 @@ import type { Span, SpanEvent } from './types';
|
|
|
3
3
|
import {
|
|
4
4
|
clampViewportToRoot,
|
|
5
5
|
computeOffscreenMarkers,
|
|
6
|
+
computeSpanDelta,
|
|
6
7
|
computeSpanMarkers,
|
|
7
8
|
computeSpanSegments,
|
|
8
9
|
computeTimeMarkers,
|
|
@@ -257,3 +258,201 @@ describe('getResourceColor', () => {
|
|
|
257
258
|
});
|
|
258
259
|
});
|
|
259
260
|
});
|
|
261
|
+
|
|
262
|
+
describe('computeSpanDelta', () => {
|
|
263
|
+
function span(id: string, startMs: number, endMs: number): Span {
|
|
264
|
+
return {
|
|
265
|
+
name: id,
|
|
266
|
+
kind: 0,
|
|
267
|
+
resource: 'step',
|
|
268
|
+
library: { name: 'workflow' },
|
|
269
|
+
spanId: id,
|
|
270
|
+
status: { code: 1 },
|
|
271
|
+
traceFlags: 0,
|
|
272
|
+
attributes: {},
|
|
273
|
+
links: [],
|
|
274
|
+
events: [],
|
|
275
|
+
startTime: ts(startMs),
|
|
276
|
+
endTime: ts(endMs),
|
|
277
|
+
duration: ts(endMs - startMs),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
it('measures the gap between disjoint spans (anchor earlier)', () => {
|
|
282
|
+
const delta = computeSpanDelta(
|
|
283
|
+
span('a', 0, 100),
|
|
284
|
+
span('b', 300, 400),
|
|
285
|
+
0,
|
|
286
|
+
1000
|
|
287
|
+
);
|
|
288
|
+
expect(delta).toEqual({
|
|
289
|
+
deltaMs: 200,
|
|
290
|
+
anchorFrac: 0.1,
|
|
291
|
+
hoveredFrac: 0.3,
|
|
292
|
+
anchorEdge: 'end',
|
|
293
|
+
hoveredOffscreen: null,
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('is direction-agnostic: anchor later gives the same delta with edges swapped', () => {
|
|
298
|
+
const delta = computeSpanDelta(
|
|
299
|
+
span('b', 300, 400),
|
|
300
|
+
span('a', 0, 100),
|
|
301
|
+
0,
|
|
302
|
+
1000
|
|
303
|
+
);
|
|
304
|
+
expect(delta).toEqual({
|
|
305
|
+
deltaMs: 200,
|
|
306
|
+
anchorFrac: 0.3,
|
|
307
|
+
hoveredFrac: 0.1,
|
|
308
|
+
anchorEdge: 'start',
|
|
309
|
+
hoveredOffscreen: null,
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('measures start-to-start offset for overlapping spans', () => {
|
|
314
|
+
const delta = computeSpanDelta(
|
|
315
|
+
span('a', 0, 500),
|
|
316
|
+
span('b', 200, 800),
|
|
317
|
+
0,
|
|
318
|
+
1000
|
|
319
|
+
);
|
|
320
|
+
expect(delta).toEqual({
|
|
321
|
+
deltaMs: 200,
|
|
322
|
+
anchorFrac: 0,
|
|
323
|
+
hoveredFrac: 0.2,
|
|
324
|
+
anchorEdge: 'start',
|
|
325
|
+
hoveredOffscreen: null,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('measures start-to-start offset for contained spans (step inside run)', () => {
|
|
330
|
+
const delta = computeSpanDelta(
|
|
331
|
+
span('run', 0, 1000),
|
|
332
|
+
span('step', 400, 600),
|
|
333
|
+
0,
|
|
334
|
+
1000
|
|
335
|
+
);
|
|
336
|
+
expect(delta).toEqual({
|
|
337
|
+
deltaMs: 400,
|
|
338
|
+
anchorFrac: 0,
|
|
339
|
+
hoveredFrac: 0.4,
|
|
340
|
+
anchorEdge: 'start',
|
|
341
|
+
hoveredOffscreen: null,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('returns a zero delta for identical starts', () => {
|
|
346
|
+
const delta = computeSpanDelta(
|
|
347
|
+
span('a', 200, 500),
|
|
348
|
+
span('b', 200, 300),
|
|
349
|
+
0,
|
|
350
|
+
1000
|
|
351
|
+
);
|
|
352
|
+
expect(delta).toEqual({
|
|
353
|
+
deltaMs: 0,
|
|
354
|
+
anchorFrac: 0.2,
|
|
355
|
+
hoveredFrac: 0.2,
|
|
356
|
+
anchorEdge: 'start',
|
|
357
|
+
hoveredOffscreen: null,
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('keeps a zero delta sitting exactly on the viewport edge (root span anchor at default zoom)', () => {
|
|
362
|
+
const delta = computeSpanDelta(
|
|
363
|
+
span('run', 0, 1000),
|
|
364
|
+
span('step', 0, 400),
|
|
365
|
+
0,
|
|
366
|
+
1000
|
|
367
|
+
);
|
|
368
|
+
expect(delta).toEqual({
|
|
369
|
+
deltaMs: 0,
|
|
370
|
+
anchorFrac: 0,
|
|
371
|
+
hoveredFrac: 0,
|
|
372
|
+
anchorEdge: 'start',
|
|
373
|
+
hoveredOffscreen: null,
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('treats touching spans as a zero gap, not an overlap', () => {
|
|
378
|
+
const delta = computeSpanDelta(
|
|
379
|
+
span('a', 0, 300),
|
|
380
|
+
span('b', 300, 400),
|
|
381
|
+
0,
|
|
382
|
+
1000
|
|
383
|
+
);
|
|
384
|
+
expect(delta).toEqual({
|
|
385
|
+
deltaMs: 0,
|
|
386
|
+
anchorFrac: 0.3,
|
|
387
|
+
hoveredFrac: 0.3,
|
|
388
|
+
anchorEdge: 'end',
|
|
389
|
+
hoveredOffscreen: null,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('handles zero-duration spans as points', () => {
|
|
394
|
+
const delta = computeSpanDelta(
|
|
395
|
+
span('a', 100, 100),
|
|
396
|
+
span('b', 400, 400),
|
|
397
|
+
0,
|
|
398
|
+
1000
|
|
399
|
+
);
|
|
400
|
+
expect(delta).toEqual({
|
|
401
|
+
deltaMs: 300,
|
|
402
|
+
anchorFrac: 0.1,
|
|
403
|
+
hoveredFrac: 0.4,
|
|
404
|
+
anchorEdge: 'end',
|
|
405
|
+
hoveredOffscreen: null,
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('clamps edges to the viewport but keeps the true delta', () => {
|
|
410
|
+
const delta = computeSpanDelta(
|
|
411
|
+
span('a', 0, 100),
|
|
412
|
+
span('b', 900, 950),
|
|
413
|
+
500,
|
|
414
|
+
800
|
|
415
|
+
);
|
|
416
|
+
expect(delta).toEqual({
|
|
417
|
+
deltaMs: 800,
|
|
418
|
+
anchorFrac: 0,
|
|
419
|
+
hoveredFrac: 1,
|
|
420
|
+
anchorEdge: 'end',
|
|
421
|
+
hoveredOffscreen: 'right',
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('flags a hovered span lying fully left of the viewport', () => {
|
|
426
|
+
const delta = computeSpanDelta(
|
|
427
|
+
span('anchor', 600, 700),
|
|
428
|
+
span('hovered', 0, 100),
|
|
429
|
+
200,
|
|
430
|
+
1200
|
|
431
|
+
);
|
|
432
|
+
expect(delta).toEqual({
|
|
433
|
+
deltaMs: 500,
|
|
434
|
+
anchorFrac: 0.4,
|
|
435
|
+
hoveredFrac: 0,
|
|
436
|
+
anchorEdge: 'start',
|
|
437
|
+
hoveredOffscreen: 'left',
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('returns null when the measurement is entirely left of the viewport', () => {
|
|
442
|
+
expect(
|
|
443
|
+
computeSpanDelta(span('a', 0, 100), span('b', 200, 300), 500, 1000)
|
|
444
|
+
).toBeNull();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('returns null when the measurement is entirely right of the viewport', () => {
|
|
448
|
+
expect(
|
|
449
|
+
computeSpanDelta(span('a', 600, 700), span('b', 900, 950), 0, 500)
|
|
450
|
+
).toBeNull();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('returns null for an empty viewport range', () => {
|
|
454
|
+
expect(
|
|
455
|
+
computeSpanDelta(span('a', 0, 100), span('b', 200, 300), 500, 500)
|
|
456
|
+
).toBeNull();
|
|
457
|
+
});
|
|
458
|
+
});
|
|
@@ -174,6 +174,11 @@ export interface SpanGap {
|
|
|
174
174
|
rowIndex: number;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/** Project an absolute time onto the viewport as a clamped [0, 1] fraction. */
|
|
178
|
+
function toViewportFrac(ms: number, viewStart: number, range: number): number {
|
|
179
|
+
return Math.min(Math.max((ms - viewStart) / range, 0), 1);
|
|
180
|
+
}
|
|
181
|
+
|
|
177
182
|
export function computeSpanGaps(
|
|
178
183
|
spans: Span[],
|
|
179
184
|
viewStart: number,
|
|
@@ -189,8 +194,8 @@ export function computeSpanGaps(
|
|
|
189
194
|
const gapMs = startTime - endTime;
|
|
190
195
|
if (gapMs <= 0) continue;
|
|
191
196
|
|
|
192
|
-
const leftFrac =
|
|
193
|
-
const rightFrac =
|
|
197
|
+
const leftFrac = toViewportFrac(endTime, viewStart, range);
|
|
198
|
+
const rightFrac = toViewportFrac(startTime, viewStart, range);
|
|
194
199
|
if (rightFrac - leftFrac < 0.001) continue;
|
|
195
200
|
|
|
196
201
|
gaps.push({ gapMs, leftFrac, rightFrac, rowIndex: i });
|
|
@@ -198,6 +203,78 @@ export function computeSpanGaps(
|
|
|
198
203
|
return gaps;
|
|
199
204
|
}
|
|
200
205
|
|
|
206
|
+
export interface SpanDelta {
|
|
207
|
+
/** True measured time between the two spans, unaffected by clamping. */
|
|
208
|
+
deltaMs: number;
|
|
209
|
+
/** Viewport-clamped fraction of the anchor span's measured edge. */
|
|
210
|
+
anchorFrac: number;
|
|
211
|
+
/** Viewport-clamped fraction of the hovered span's measured edge. */
|
|
212
|
+
hoveredFrac: number;
|
|
213
|
+
/**
|
|
214
|
+
* Which edge of the anchor span the measurement runs from: its end when the
|
|
215
|
+
* spans are disjoint and the anchor comes first, otherwise its start.
|
|
216
|
+
*/
|
|
217
|
+
anchorEdge: 'start' | 'end';
|
|
218
|
+
/**
|
|
219
|
+
* Set when the hovered span lies entirely outside the viewport on one side,
|
|
220
|
+
* i.e. its row renders an off-screen arrow the line should stop short of.
|
|
221
|
+
*/
|
|
222
|
+
hoveredOffscreen: 'left' | 'right' | null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Measure the temporal delta between an anchor (selected) span and a hovered
|
|
227
|
+
* span for the Alt+hover measurement overlay. Ordering the two spans by start
|
|
228
|
+
* time (E = earlier-starting, L = later-starting), the measurement runs to
|
|
229
|
+
* L's start from E's end when the spans are disjoint ("how long after E ended
|
|
230
|
+
* did L start"), or from E's start when they overlap ("how long after E
|
|
231
|
+
* started did L start"). Fractions are clamped to the viewport like
|
|
232
|
+
* `computeSpanGaps`; `deltaMs` is always the true duration. Returns null when
|
|
233
|
+
* the measurement lies entirely outside the viewport.
|
|
234
|
+
*/
|
|
235
|
+
export function computeSpanDelta(
|
|
236
|
+
anchor: Span,
|
|
237
|
+
hovered: Span,
|
|
238
|
+
viewStart: number,
|
|
239
|
+
viewEnd: number
|
|
240
|
+
): SpanDelta | null {
|
|
241
|
+
const range = viewEnd - viewStart;
|
|
242
|
+
if (range <= 0) return null;
|
|
243
|
+
|
|
244
|
+
const anchorStart = getHighResInMs(anchor.startTime);
|
|
245
|
+
const hoveredStart = getHighResInMs(hovered.startTime);
|
|
246
|
+
const anchorIsEarlier = anchorStart <= hoveredStart;
|
|
247
|
+
const earlierStart = anchorIsEarlier ? anchorStart : hoveredStart;
|
|
248
|
+
const earlierEnd = getHighResInMs(
|
|
249
|
+
anchorIsEarlier ? anchor.endTime : hovered.endTime
|
|
250
|
+
);
|
|
251
|
+
const laterStart = anchorIsEarlier ? hoveredStart : anchorStart;
|
|
252
|
+
|
|
253
|
+
const originMs = earlierEnd <= laterStart ? earlierEnd : earlierStart;
|
|
254
|
+
const deltaMs = laterStart - originMs;
|
|
255
|
+
|
|
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 — still renders.
|
|
259
|
+
if (laterStart < viewStart || originMs > viewEnd) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const originFrac = toViewportFrac(originMs, viewStart, range);
|
|
264
|
+
const laterFrac = toViewportFrac(laterStart, viewStart, range);
|
|
265
|
+
|
|
266
|
+
const hoveredEnd = getHighResInMs(hovered.endTime);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
deltaMs,
|
|
270
|
+
anchorFrac: anchorIsEarlier ? originFrac : laterFrac,
|
|
271
|
+
hoveredFrac: anchorIsEarlier ? laterFrac : originFrac,
|
|
272
|
+
anchorEdge: anchorIsEarlier && earlierEnd <= laterStart ? 'end' : 'start',
|
|
273
|
+
hoveredOffscreen:
|
|
274
|
+
hoveredStart > viewEnd ? 'right' : hoveredEnd < viewStart ? 'left' : null,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
201
278
|
// ---------------------------------------------------------------------------
|
|
202
279
|
// Resource colors
|
|
203
280
|
// ---------------------------------------------------------------------------
|
|
@@ -417,6 +417,8 @@ const attributeToDisplayFn: Record<
|
|
|
417
417
|
receivedCount: (value: unknown) => String(value),
|
|
418
418
|
lastReceivedAt: localMillisecondTimeOrNull,
|
|
419
419
|
disposedAt: localMillisecondTimeOrNull,
|
|
420
|
+
// Internal resume plumbing — not surfaced in the UI
|
|
421
|
+
resumeContext: (_value: unknown) => null,
|
|
420
422
|
// Event details
|
|
421
423
|
eventType: (value: unknown) => String(value),
|
|
422
424
|
correlationId: (value: unknown) => String(value),
|
|
@@ -638,6 +640,10 @@ const attributeToDisplayFn: Record<
|
|
|
638
640
|
if (typeof value !== 'string' || value.length === 0) return null;
|
|
639
641
|
return String(value);
|
|
640
642
|
},
|
|
643
|
+
// Internal encryption plumbing: the run's X25519 public key, used by
|
|
644
|
+
// cross-run writers to seal payloads to this run. Not actionable for users
|
|
645
|
+
// and not secret — hidden rather than rendered as 44 opaque base64 chars.
|
|
646
|
+
encryptionPublicKey: (_value: unknown) => null,
|
|
641
647
|
};
|
|
642
648
|
|
|
643
649
|
const resolvableAttributes = [
|
package/src/index.ts
CHANGED
package/src/lib/hydration.ts
CHANGED
|
@@ -487,7 +487,7 @@ export async function hydrateResourceIOAsync<T>(
|
|
|
487
487
|
resource: T,
|
|
488
488
|
key?: Uint8Array
|
|
489
489
|
): Promise<T> {
|
|
490
|
-
const { hydrateDataWithKey } = await import(
|
|
490
|
+
const { hydrateDataWithKey, deriveRunPayloadKeys } = await import(
|
|
491
491
|
'@workflow/core/serialization-format'
|
|
492
492
|
);
|
|
493
493
|
// Payloads may be zstd-compressed (the Web DecompressionStream has no zstd);
|
|
@@ -497,11 +497,17 @@ export async function hydrateResourceIOAsync<T>(
|
|
|
497
497
|
'./zstd-browser-decoder.js'
|
|
498
498
|
);
|
|
499
499
|
ensureZstdDecoderRegistered();
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
500
|
+
// Resolve the *full* key capability, not just the symmetric key: a run's
|
|
501
|
+
// event log can contain sealed ('encp') payloads that another run wrote to
|
|
502
|
+
// it (a cross-deployment hook resumption, say), and opening those needs the
|
|
503
|
+
// run's X25519 scalar in addition to its AES key. Both are derived from the
|
|
504
|
+
// same 32 bytes the key-retrieval endpoint returns.
|
|
505
|
+
// Resolve the *full* key capability, not just the symmetric key: a run's
|
|
506
|
+
// event log can contain sealed ('encp') payloads that another run wrote to
|
|
507
|
+
// it (a cross-deployment hook resumption, say), and opening those needs the
|
|
508
|
+
// run's X25519 scalar in addition to its AES key. Both derive from the same
|
|
509
|
+
// 32 bytes the key-retrieval endpoint returns.
|
|
510
|
+
const cryptoKey = key ? await deriveRunPayloadKeys(key) : undefined;
|
|
505
511
|
const revivers = getRevivers();
|
|
506
512
|
|
|
507
513
|
async function hydrateField(value: unknown): Promise<unknown> {
|