@kaizenreport/kensho-viewer 0.1.0

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.
@@ -0,0 +1,1156 @@
1
+ /* Auto-generated from charts.jsx by packages/viewer/scripts/build.js. Edit the .jsx — DO NOT edit this file. */
2
+ /* global React */
3
+
4
+ // ============== TREND CHART V2 ==============
5
+ // Stacked area chart: four status bands stacked over time.
6
+ // Designed to read calm at a glance — green dominance = healthy run history.
7
+ // One axis (count). Pass-rate is implicit in the green band's share.
8
+ // Hover reveals a vertical guide + tooltip with the per-status breakdown.
9
+ function TrendChartV2({
10
+ runs
11
+ }) {
12
+ const [hoverIdx, setHover] = React.useState(null);
13
+ const W = 760,
14
+ H = 240,
15
+ padL = 44,
16
+ padR = 24,
17
+ padT = 24,
18
+ padB = 40;
19
+ const innerW = W - padL - padR,
20
+ innerH = H - padT - padB;
21
+ const totals = runs.map(r => r.passed + r.failed + r.broken + r.skipped);
22
+ const maxY = Math.ceil(Math.max(...totals, 8) / 4) * 4;
23
+ const xStep = runs.length > 1 ? innerW / (runs.length - 1) : 0;
24
+ const xAt = i => padL + i * xStep;
25
+ const yAt = v => padT + innerH - v / maxY * innerH;
26
+
27
+ // Stacking order — passed at the bottom (so it dominates visually when healthy),
28
+ // then skipped, broken, failed at the top. Cumulative band paths.
29
+ const ORDER = [{
30
+ key: 'passed',
31
+ color: 'var(--status-passed)'
32
+ }, {
33
+ key: 'skipped',
34
+ color: 'var(--status-skipped)'
35
+ }, {
36
+ key: 'broken',
37
+ color: 'var(--status-broken)'
38
+ }, {
39
+ key: 'failed',
40
+ color: 'var(--status-failed)'
41
+ }];
42
+
43
+ // Build cumulative top-of-band y values per run.
44
+ const stack = runs.map(r => {
45
+ const acc = [];
46
+ let c = 0;
47
+ ORDER.forEach(o => {
48
+ c += r[o.key] || 0;
49
+ acc.push(c);
50
+ });
51
+ return acc; // [yPassed, yPassed+skipped, …]
52
+ });
53
+ const bandPath = bandIdx => {
54
+ // bottom = previous band's cumulative (or 0 for first), top = this band's cumulative
55
+ const top = runs.map((_, i) => yAt(stack[i][bandIdx]));
56
+ const bot = runs.map((_, i) => yAt(bandIdx === 0 ? 0 : stack[i][bandIdx - 1]));
57
+ const fwd = runs.map((_, i) => `${i === 0 ? 'M' : 'L'}${xAt(i)},${top[i]}`).join(' ');
58
+ const back = runs.map((_, i) => `L${xAt(runs.length - 1 - i)},${bot[runs.length - 1 - i]}`).join(' ');
59
+ return `${fwd} ${back} Z`;
60
+ };
61
+
62
+ // y-axis ticks — 5 nice round values
63
+ const yTicks = [0, maxY / 4, maxY / 2, 3 * maxY / 4, maxY];
64
+
65
+ // Run-level summary for tooltip and "current vs previous" indicator
66
+ const hovered = hoverIdx != null ? runs[hoverIdx] : runs[runs.length - 1];
67
+ const hoveredTotal = hovered ? hovered.passed + hovered.failed + hovered.broken + hovered.skipped : 0;
68
+ const hoveredPassRate = hoveredTotal ? hovered.passed / hoveredTotal : 0;
69
+ const prevIdx = (hoverIdx ?? runs.length - 1) - 1;
70
+ const prev = prevIdx >= 0 ? runs[prevIdx] : null;
71
+ const prevPassRate = prev ? prev.passed / Math.max(1, prev.passed + prev.failed + prev.broken + prev.skipped) : 0;
72
+ const passRateDelta = prev ? (hoveredPassRate - prevPassRate) * 100 : 0;
73
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
74
+ style: {
75
+ display: 'grid',
76
+ gridTemplateColumns: '1fr auto auto auto auto',
77
+ gap: 18,
78
+ alignItems: 'baseline',
79
+ padding: '4px 4px 14px',
80
+ borderBottom: '1px solid var(--line)',
81
+ marginBottom: 14
82
+ }
83
+ }, /*#__PURE__*/React.createElement("div", {
84
+ style: {
85
+ minWidth: 0
86
+ }
87
+ }, /*#__PURE__*/React.createElement("div", {
88
+ style: {
89
+ fontFamily: 'var(--font-mono)',
90
+ fontSize: 10.5,
91
+ color: 'var(--fg3)',
92
+ letterSpacing: 1.2,
93
+ textTransform: 'uppercase'
94
+ }
95
+ }, hoverIdx == null ? 'Current run' : `Run #${hovered.short}`), /*#__PURE__*/React.createElement("div", {
96
+ style: {
97
+ display: 'flex',
98
+ alignItems: 'baseline',
99
+ gap: 8,
100
+ marginTop: 4
101
+ }
102
+ }, /*#__PURE__*/React.createElement("span", {
103
+ style: {
104
+ fontFamily: 'var(--font-display)',
105
+ fontSize: 26,
106
+ fontWeight: 700,
107
+ color: 'var(--fg1)',
108
+ letterSpacing: -0.5,
109
+ lineHeight: 1,
110
+ fontVariantNumeric: 'tabular-nums'
111
+ }
112
+ }, Math.round(hoveredPassRate * 100), /*#__PURE__*/React.createElement("span", {
113
+ style: {
114
+ fontSize: 14,
115
+ color: 'var(--fg3)',
116
+ marginLeft: 2
117
+ }
118
+ }, "%")), /*#__PURE__*/React.createElement("span", {
119
+ style: {
120
+ fontFamily: 'var(--font-mono)',
121
+ fontSize: 11,
122
+ color: 'var(--fg3)'
123
+ }
124
+ }, "pass rate"), prev && /*#__PURE__*/React.createElement("span", {
125
+ style: {
126
+ fontFamily: 'var(--font-mono)',
127
+ fontSize: 11,
128
+ fontWeight: 600,
129
+ color: passRateDelta >= 0 ? 'var(--status-passed)' : 'var(--status-failed)',
130
+ fontVariantNumeric: 'tabular-nums'
131
+ }
132
+ }, passRateDelta >= 0 ? '↑' : '↓', " ", Math.abs(passRateDelta).toFixed(1), "pp"))), ORDER.map(o => /*#__PURE__*/React.createElement("div", {
133
+ key: o.key,
134
+ style: {
135
+ textAlign: 'right',
136
+ minWidth: 60
137
+ }
138
+ }, /*#__PURE__*/React.createElement("div", {
139
+ style: {
140
+ display: 'flex',
141
+ alignItems: 'center',
142
+ gap: 5,
143
+ justifyContent: 'flex-end',
144
+ fontFamily: 'var(--font-mono)',
145
+ fontSize: 10,
146
+ color: 'var(--fg3)',
147
+ textTransform: 'uppercase',
148
+ letterSpacing: 0.5
149
+ }
150
+ }, /*#__PURE__*/React.createElement("span", {
151
+ style: {
152
+ width: 8,
153
+ height: 8,
154
+ borderRadius: 2,
155
+ background: o.color
156
+ }
157
+ }), o.key), /*#__PURE__*/React.createElement("div", {
158
+ style: {
159
+ fontFamily: 'var(--font-mono)',
160
+ fontSize: 15,
161
+ fontWeight: 600,
162
+ color: 'var(--fg1)',
163
+ fontVariantNumeric: 'tabular-nums',
164
+ marginTop: 2
165
+ }
166
+ }, hovered[o.key] || 0)))), /*#__PURE__*/React.createElement("svg", {
167
+ viewBox: `0 0 ${W} ${H}`,
168
+ width: "100%",
169
+ style: {
170
+ display: 'block',
171
+ overflow: 'visible'
172
+ },
173
+ onMouseLeave: () => setHover(null)
174
+ }, yTicks.map((t, i) => /*#__PURE__*/React.createElement("g", {
175
+ key: i
176
+ }, /*#__PURE__*/React.createElement("line", {
177
+ x1: padL,
178
+ x2: W - padR,
179
+ y1: yAt(t),
180
+ y2: yAt(t),
181
+ stroke: "var(--line)",
182
+ strokeWidth: "1",
183
+ strokeDasharray: i === 0 ? '0' : '2 4'
184
+ }), /*#__PURE__*/React.createElement("text", {
185
+ x: padL - 10,
186
+ y: yAt(t) + 4,
187
+ textAnchor: "end",
188
+ fontFamily: "var(--font-mono)",
189
+ fontSize: "10.5",
190
+ fill: "var(--fg3)",
191
+ fontVariantNumeric: "tabular-nums"
192
+ }, t))), /*#__PURE__*/React.createElement("text", {
193
+ x: padL - 30,
194
+ y: padT - 8,
195
+ fontFamily: "var(--font-mono)",
196
+ fontSize: "10",
197
+ fill: "var(--fg3)",
198
+ letterSpacing: "1.2",
199
+ textAnchor: "start"
200
+ }, "TESTS"), ORDER.map((o, i) => /*#__PURE__*/React.createElement("path", {
201
+ key: o.key,
202
+ d: bandPath(i),
203
+ fill: o.color,
204
+ fillOpacity: "0.85"
205
+ })), /*#__PURE__*/React.createElement("path", {
206
+ d: runs.map((_, i) => `${i === 0 ? 'M' : 'L'}${xAt(i)},${yAt(stack[i][ORDER.length - 1])}`).join(' '),
207
+ fill: "none",
208
+ stroke: "var(--fg1)",
209
+ strokeOpacity: "0.18",
210
+ strokeWidth: "1"
211
+ }), hoverIdx != null && /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
212
+ x1: xAt(hoverIdx),
213
+ x2: xAt(hoverIdx),
214
+ y1: padT,
215
+ y2: padT + innerH,
216
+ stroke: "var(--fg1)",
217
+ strokeOpacity: "0.35",
218
+ strokeWidth: "1",
219
+ strokeDasharray: "3 3"
220
+ }), /*#__PURE__*/React.createElement("circle", {
221
+ cx: xAt(hoverIdx),
222
+ cy: yAt(stack[hoverIdx][ORDER.length - 1]),
223
+ r: "4",
224
+ fill: "var(--bg-elev)",
225
+ stroke: "var(--fg1)",
226
+ strokeWidth: "1.5"
227
+ })), runs.map((r, i) => /*#__PURE__*/React.createElement("rect", {
228
+ key: i,
229
+ x: xAt(i) - xStep / 2,
230
+ y: padT,
231
+ width: xStep || innerW,
232
+ height: innerH,
233
+ fill: "transparent",
234
+ onMouseEnter: () => setHover(i)
235
+ })), runs.map((r, i) => {
236
+ const isHover = hoverIdx === i;
237
+ const isLast = i === runs.length - 1;
238
+ // sparse labels: first, last, every 2nd otherwise
239
+ const show = i === 0 || isLast || isHover || i % 2 === 0;
240
+ if (!show) return null;
241
+ return /*#__PURE__*/React.createElement("text", {
242
+ key: i,
243
+ x: xAt(i),
244
+ y: H - padB + 16,
245
+ textAnchor: "middle",
246
+ fontFamily: "var(--font-mono)",
247
+ fontSize: "10.5",
248
+ fill: isHover || isLast ? 'var(--fg1)' : 'var(--fg3)',
249
+ fontWeight: isHover || isLast ? 600 : 400
250
+ }, "#", r.short);
251
+ }), /*#__PURE__*/React.createElement("text", {
252
+ x: xAt(runs.length - 1),
253
+ y: H - padB + 30,
254
+ textAnchor: "middle",
255
+ fontFamily: "var(--font-mono)",
256
+ fontSize: "9.5",
257
+ fill: "var(--fg3)",
258
+ letterSpacing: "0.5",
259
+ textTransform: "uppercase"
260
+ }, "now"), /*#__PURE__*/React.createElement("line", {
261
+ x1: padL,
262
+ x2: W - padR,
263
+ y1: padT + innerH,
264
+ y2: padT + innerH,
265
+ stroke: "var(--line-strong)",
266
+ strokeWidth: "1"
267
+ })));
268
+ }
269
+
270
+ // ============== TIMELINE (Gantt by suite) ==============
271
+ //
272
+ // Rows = suites (each suite groups its tests on its own row, time-axis horizontal).
273
+ // X axis = wall-clock ms since run start.
274
+ // Bars = individual tests; gaps between bars = idle time within suite.
275
+ // Hover any bar → details fill the right sidebar (no floating tooltips that overflow).
276
+ function TimelineGantt({
277
+ tests,
278
+ totalMs,
279
+ onOpen
280
+ }) {
281
+ const [hover, setHover] = React.useState(null);
282
+
283
+ // group tests by suite, preserve order, map to rows
284
+ const bySuite = {};
285
+ tests.forEach(t => {
286
+ if (!bySuite[t.suite]) bySuite[t.suite] = [];
287
+ bySuite[t.suite].push(t);
288
+ });
289
+ const suiteRows = Object.entries(bySuite); // [[suiteName, [tests]], ...]
290
+
291
+ const rowH = 44;
292
+ const padL = 220,
293
+ padR = 16,
294
+ padT = 22,
295
+ padB = 28;
296
+ const W = 1000;
297
+ const H = padT + suiteRows.length * rowH + padB;
298
+ const innerW = W - padL - padR;
299
+ const x = ms => padL + ms / totalMs * innerW;
300
+
301
+ // tick spacing — aim for 6-8 evenly-spaced ticks regardless of scale, so
302
+ // the label row stays legible whether the run is 200ms or 5min long.
303
+ const niceStep = max => {
304
+ const target = max / 7;
305
+ const exp = Math.floor(Math.log10(target));
306
+ const f = target / Math.pow(10, exp);
307
+ const round = f < 1.5 ? 1 : f < 3.5 ? 2 : f < 7.5 ? 5 : 10;
308
+ return round * Math.pow(10, exp);
309
+ };
310
+ const tickStep = niceStep(totalMs);
311
+ const ticks = [];
312
+ for (let t = 0; t <= totalMs; t += tickStep) ticks.push(t);
313
+ if (ticks[ticks.length - 1] < totalMs - tickStep / 2) ticks.push(totalMs);
314
+
315
+ // Format ticks per scale: ms below 2s, integer seconds below 10s,
316
+ // round seconds at scale, minutes-and-seconds for very long runs.
317
+ const fmtTick = ms => {
318
+ if (totalMs < 2000) return ms + 'ms';
319
+ if (totalMs < 10000) return (ms / 1000).toFixed(1) + 's';
320
+ if (totalMs < 120000) return Math.round(ms / 1000) + 's';
321
+ const m = Math.floor(ms / 60000);
322
+ const s = Math.round(ms % 60000 / 1000);
323
+ return s ? `${m}m ${s}s` : `${m}m`;
324
+ };
325
+ return /*#__PURE__*/React.createElement("div", {
326
+ style: {
327
+ display: 'grid',
328
+ gridTemplateColumns: '1fr 280px',
329
+ gap: 16,
330
+ alignItems: 'start'
331
+ }
332
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
333
+ style: {
334
+ fontFamily: 'var(--font-mono)',
335
+ fontSize: 10,
336
+ color: 'var(--fg3)',
337
+ letterSpacing: 1.5,
338
+ textTransform: 'uppercase',
339
+ paddingLeft: padL,
340
+ marginBottom: 4
341
+ }
342
+ }, "Wall-clock time (s) \u2192"), /*#__PURE__*/React.createElement("svg", {
343
+ viewBox: `0 0 ${W} ${H}`,
344
+ width: "100%",
345
+ style: {
346
+ display: 'block'
347
+ },
348
+ onMouseLeave: () => setHover(null)
349
+ }, suiteRows.map(([suite, _], i) => /*#__PURE__*/React.createElement("rect", {
350
+ key: i,
351
+ x: padL,
352
+ y: padT + i * rowH,
353
+ width: innerW,
354
+ height: rowH,
355
+ fill: i % 2 ? 'transparent' : 'var(--bg-2)',
356
+ fillOpacity: "0.5"
357
+ })), ticks.map((t, i) => /*#__PURE__*/React.createElement("line", {
358
+ key: i,
359
+ x1: x(t),
360
+ x2: x(t),
361
+ y1: padT,
362
+ y2: padT + suiteRows.length * rowH,
363
+ stroke: "var(--line)",
364
+ strokeWidth: "1",
365
+ opacity: t === 0 ? 0.8 : 0.5
366
+ })), ticks.map((t, i) => /*#__PURE__*/React.createElement("text", {
367
+ key: i,
368
+ x: x(t),
369
+ y: padT - 6,
370
+ textAnchor: i === 0 ? 'start' : i === ticks.length - 1 ? 'end' : 'middle',
371
+ fontFamily: "var(--font-mono)",
372
+ fontSize: "11",
373
+ fill: "var(--fg3)",
374
+ fontVariantNumeric: "tabular-nums"
375
+ }, fmtTick(t))), suiteRows.map(([suite, suiteTests], i) => /*#__PURE__*/React.createElement("g", {
376
+ key: i
377
+ }, /*#__PURE__*/React.createElement("text", {
378
+ x: padL - 12,
379
+ y: padT + i * rowH + rowH / 2 + 4,
380
+ textAnchor: "end",
381
+ fontFamily: "var(--font-mono)",
382
+ fontSize: "12",
383
+ fill: "var(--fg1)",
384
+ fontWeight: "600"
385
+ }, suite), /*#__PURE__*/React.createElement("text", {
386
+ x: padL - 12,
387
+ y: padT + i * rowH + rowH / 2 + 18,
388
+ textAnchor: "end",
389
+ fontFamily: "var(--font-mono)",
390
+ fontSize: "10",
391
+ fill: "var(--fg3)"
392
+ }, suiteTests.length, " test", suiteTests.length !== 1 ? 's' : ''))), suiteRows.map(([suite, suiteTests], rowIdx) => suiteTests.map((t, i) => {
393
+ if (t.durMs === 0) return null;
394
+ // Min bar width 10px so every bar is clickable; 3px slivers from
395
+ // sub-100ms tests in a 60s run window are unusable on hover.
396
+ const bx = x(t.start),
397
+ bw = Math.max(10, x(t.start + t.durMs) - bx);
398
+ const by = padT + rowIdx * rowH + 6;
399
+ const bh = rowH - 12;
400
+ const c = `var(--status-${t.status})`;
401
+ const isHover = hover && hover.id === t.id;
402
+ return /*#__PURE__*/React.createElement("g", {
403
+ key: i,
404
+ onMouseEnter: () => setHover(t),
405
+ onClick: () => onOpen?.(t),
406
+ style: {
407
+ cursor: 'pointer'
408
+ }
409
+ }, /*#__PURE__*/React.createElement("title", null, `${t.name} · ${t.dur} · ${t.status}`), /*#__PURE__*/React.createElement("rect", {
410
+ x: bx,
411
+ y: by,
412
+ width: bw,
413
+ height: bh,
414
+ rx: "3",
415
+ fill: c,
416
+ fillOpacity: isHover ? 1 : 0.85,
417
+ stroke: isHover ? c : 'transparent',
418
+ strokeWidth: "2"
419
+ }), bw >= 44 && /*#__PURE__*/React.createElement("text", {
420
+ x: bx + bw / 2,
421
+ y: by + bh / 2 + 4,
422
+ textAnchor: "middle",
423
+ fontFamily: "var(--font-mono)",
424
+ fontSize: "10.5",
425
+ fill: "#fff",
426
+ fontWeight: "700",
427
+ style: {
428
+ pointerEvents: 'none'
429
+ },
430
+ clipPath: `inset(0 0 0 0)`
431
+ }, t.dur));
432
+ })), /*#__PURE__*/React.createElement("line", {
433
+ x1: x(totalMs),
434
+ x2: x(totalMs),
435
+ y1: padT,
436
+ y2: padT + suiteRows.length * rowH,
437
+ stroke: "var(--accent)",
438
+ strokeWidth: "2",
439
+ strokeDasharray: "2 2"
440
+ }), /*#__PURE__*/React.createElement("text", {
441
+ x: x(totalMs),
442
+ y: padT + suiteRows.length * rowH + 18,
443
+ textAnchor: "end",
444
+ fontFamily: "var(--font-mono)",
445
+ fontSize: "10",
446
+ fill: "var(--accent)",
447
+ fontWeight: "700"
448
+ }, "RUN END"))), /*#__PURE__*/React.createElement(TimelineDetails, {
449
+ test: hover,
450
+ totalMs: totalMs
451
+ }));
452
+ }
453
+ function TimelineDetails({
454
+ test,
455
+ totalMs
456
+ }) {
457
+ if (!test) {
458
+ return /*#__PURE__*/React.createElement("div", {
459
+ style: {
460
+ border: '1px solid var(--line)',
461
+ borderRadius: 6,
462
+ padding: 14,
463
+ background: 'var(--bg-2)'
464
+ }
465
+ }, /*#__PURE__*/React.createElement("div", {
466
+ className: "k-overline",
467
+ style: {
468
+ marginBottom: 6
469
+ }
470
+ }, "How to read this"), /*#__PURE__*/React.createElement("div", {
471
+ style: {
472
+ fontSize: 12,
473
+ color: 'var(--fg2)',
474
+ lineHeight: 1.5
475
+ }
476
+ }, "Each row is a ", /*#__PURE__*/React.createElement("b", null, "suite"), ". Bars are the ", /*#__PURE__*/React.createElement("b", null, "tests"), " in that suite, positioned by when they started. Hover a bar for details, click to open."), /*#__PURE__*/React.createElement("div", {
477
+ style: {
478
+ marginTop: 12,
479
+ display: 'flex',
480
+ flexDirection: 'column',
481
+ gap: 6
482
+ }
483
+ }, /*#__PURE__*/React.createElement(LegendDot, {
484
+ color: "var(--status-passed)",
485
+ label: "Passed"
486
+ }), /*#__PURE__*/React.createElement(LegendDot, {
487
+ color: "var(--status-failed)",
488
+ label: "Failed"
489
+ }), /*#__PURE__*/React.createElement(LegendDot, {
490
+ color: "var(--status-broken)",
491
+ label: "Broken"
492
+ }), /*#__PURE__*/React.createElement(LegendDot, {
493
+ color: "var(--status-skipped)",
494
+ label: "Skipped"
495
+ })));
496
+ }
497
+ const c = `var(--status-${test.status})`;
498
+ const startPct = test.start / totalMs * 100;
499
+ return /*#__PURE__*/React.createElement("div", {
500
+ style: {
501
+ border: '1px solid var(--line)',
502
+ borderRadius: 6,
503
+ padding: 14,
504
+ background: 'var(--bg-elev)'
505
+ }
506
+ }, /*#__PURE__*/React.createElement("div", {
507
+ style: {
508
+ display: 'flex',
509
+ alignItems: 'center',
510
+ gap: 8,
511
+ marginBottom: 10
512
+ }
513
+ }, /*#__PURE__*/React.createElement("span", {
514
+ style: {
515
+ width: 10,
516
+ height: 10,
517
+ borderRadius: 2,
518
+ background: c
519
+ }
520
+ }), /*#__PURE__*/React.createElement("span", {
521
+ style: {
522
+ fontFamily: 'var(--font-mono)',
523
+ fontSize: 10,
524
+ color: 'var(--fg3)',
525
+ textTransform: 'uppercase',
526
+ letterSpacing: 1
527
+ }
528
+ }, test.status)), /*#__PURE__*/React.createElement("div", {
529
+ style: {
530
+ fontFamily: 'var(--font-mono)',
531
+ fontWeight: 600,
532
+ fontSize: 13,
533
+ color: 'var(--fg1)',
534
+ marginBottom: 4,
535
+ wordBreak: 'break-word'
536
+ }
537
+ }, test.name), /*#__PURE__*/React.createElement("div", {
538
+ style: {
539
+ fontFamily: 'var(--font-mono)',
540
+ fontSize: 11,
541
+ color: 'var(--fg3)',
542
+ marginBottom: 10
543
+ }
544
+ }, test.suite), /*#__PURE__*/React.createElement("div", {
545
+ style: {
546
+ display: 'grid',
547
+ gridTemplateColumns: 'auto 1fr',
548
+ gap: '6px 10px',
549
+ fontFamily: 'var(--font-mono)',
550
+ fontSize: 11.5
551
+ }
552
+ }, /*#__PURE__*/React.createElement("span", {
553
+ style: {
554
+ color: 'var(--fg3)'
555
+ }
556
+ }, "started"), /*#__PURE__*/React.createElement("span", {
557
+ style: {
558
+ color: 'var(--fg1)'
559
+ }
560
+ }, (test.start / 1000).toFixed(2), "s (", startPct.toFixed(0), "% in)"), /*#__PURE__*/React.createElement("span", {
561
+ style: {
562
+ color: 'var(--fg3)'
563
+ }
564
+ }, "duration"), /*#__PURE__*/React.createElement("span", {
565
+ style: {
566
+ color: 'var(--fg1)',
567
+ fontWeight: 600
568
+ }
569
+ }, test.dur), /*#__PURE__*/React.createElement("span", {
570
+ style: {
571
+ color: 'var(--fg3)'
572
+ }
573
+ }, "platform"), /*#__PURE__*/React.createElement("span", {
574
+ style: {
575
+ color: 'var(--fg1)'
576
+ }
577
+ }, test.platform), /*#__PURE__*/React.createElement("span", {
578
+ style: {
579
+ color: 'var(--fg3)'
580
+ }
581
+ }, "retries"), /*#__PURE__*/React.createElement("span", {
582
+ style: {
583
+ color: 'var(--fg1)'
584
+ }
585
+ }, test.retries), /*#__PURE__*/React.createElement("span", {
586
+ style: {
587
+ color: 'var(--fg3)'
588
+ }
589
+ }, "severity"), /*#__PURE__*/React.createElement("span", {
590
+ style: {
591
+ color: 'var(--fg1)'
592
+ }
593
+ }, test.severity), /*#__PURE__*/React.createElement("span", {
594
+ style: {
595
+ color: 'var(--fg3)'
596
+ }
597
+ }, "file"), /*#__PURE__*/React.createElement("span", {
598
+ style: {
599
+ color: 'var(--fg1)',
600
+ fontSize: 10.5
601
+ }
602
+ }, test.file)));
603
+ }
604
+ function LegendDot({
605
+ color,
606
+ label
607
+ }) {
608
+ return /*#__PURE__*/React.createElement("div", {
609
+ style: {
610
+ display: 'flex',
611
+ alignItems: 'center',
612
+ gap: 8
613
+ }
614
+ }, /*#__PURE__*/React.createElement("span", {
615
+ style: {
616
+ width: 14,
617
+ height: 8,
618
+ borderRadius: 2,
619
+ background: color
620
+ }
621
+ }), /*#__PURE__*/React.createElement("span", {
622
+ style: {
623
+ fontFamily: 'var(--font-mono)',
624
+ fontSize: 11,
625
+ color: 'var(--fg2)'
626
+ }
627
+ }, label));
628
+ }
629
+
630
+ // ============== RETRY WATERFALL ==============
631
+ function RetryWaterfall({
632
+ attempts
633
+ }) {
634
+ // attempts: [{ status, dur, label }]
635
+ const max = Math.max(...attempts.map(a => a.dur));
636
+ return /*#__PURE__*/React.createElement("div", {
637
+ style: {
638
+ display: 'flex',
639
+ flexDirection: 'column',
640
+ gap: 8
641
+ }
642
+ }, attempts.map((a, i) => /*#__PURE__*/React.createElement("div", {
643
+ key: i,
644
+ style: {
645
+ display: 'grid',
646
+ gridTemplateColumns: '70px 1fr 70px',
647
+ alignItems: 'center',
648
+ gap: 12
649
+ }
650
+ }, /*#__PURE__*/React.createElement("div", {
651
+ style: {
652
+ fontFamily: 'var(--font-mono)',
653
+ fontSize: 12,
654
+ color: 'var(--fg3)'
655
+ }
656
+ }, "attempt ", i + 1), /*#__PURE__*/React.createElement("div", {
657
+ style: {
658
+ height: 18,
659
+ background: 'var(--bg-sunken)',
660
+ borderRadius: 3,
661
+ overflow: 'hidden'
662
+ }
663
+ }, /*#__PURE__*/React.createElement("div", {
664
+ style: {
665
+ width: `${a.dur / max * 100}%`,
666
+ height: '100%',
667
+ background: `var(--status-${a.status})`,
668
+ display: 'flex',
669
+ alignItems: 'center',
670
+ paddingLeft: 8,
671
+ color: '#fff',
672
+ fontSize: 11,
673
+ fontWeight: 600
674
+ }
675
+ }, a.label)), /*#__PURE__*/React.createElement("div", {
676
+ style: {
677
+ fontFamily: 'var(--font-mono)',
678
+ fontSize: 12,
679
+ color: 'var(--fg2)',
680
+ textAlign: 'right'
681
+ }
682
+ }, (a.dur / 1000).toFixed(2), "s"))));
683
+ }
684
+
685
+ // ============== SUITE HEATMAP ==============
686
+ function SuiteHeatmap({
687
+ suites,
688
+ runs
689
+ }) {
690
+ // suites: [{name, statuses: [last N statuses]}]
691
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
692
+ style: {
693
+ display: 'grid',
694
+ gridTemplateColumns: '240px repeat(' + runs + ', 1fr)',
695
+ alignItems: 'center',
696
+ gap: 4,
697
+ fontFamily: 'var(--font-mono)',
698
+ fontSize: 10,
699
+ color: 'var(--fg3)',
700
+ marginBottom: 6
701
+ }
702
+ }, /*#__PURE__*/React.createElement("div", null), [...Array(runs)].map((_, i) => /*#__PURE__*/React.createElement("div", {
703
+ key: i,
704
+ style: {
705
+ textAlign: 'center'
706
+ }
707
+ }, i === 0 ? 'oldest' : i === runs - 1 ? 'latest' : ''))), suites.map((s, i) => /*#__PURE__*/React.createElement("div", {
708
+ key: i,
709
+ style: {
710
+ display: 'grid',
711
+ gridTemplateColumns: '240px repeat(' + runs + ', 1fr)',
712
+ alignItems: 'center',
713
+ gap: 4,
714
+ marginBottom: 4
715
+ }
716
+ }, /*#__PURE__*/React.createElement("div", {
717
+ style: {
718
+ fontFamily: 'var(--font-mono)',
719
+ fontSize: 12,
720
+ color: 'var(--fg1)',
721
+ overflow: 'hidden',
722
+ textOverflow: 'ellipsis',
723
+ whiteSpace: 'nowrap'
724
+ }
725
+ }, s.name), s.statuses.map((st, j) => /*#__PURE__*/React.createElement("div", {
726
+ key: j,
727
+ title: st,
728
+ style: {
729
+ height: 20,
730
+ borderRadius: 3,
731
+ background: `var(--status-${st})`,
732
+ opacity: st === 'passed' ? 0.85 : 1
733
+ }
734
+ })))));
735
+ }
736
+
737
+ // ============== FLAKE-RATE SCATTER (quadrant) ==============
738
+ function FlakeScatter({
739
+ tests
740
+ }) {
741
+ const [hoverIdx, setHover] = React.useState(null);
742
+ const [hoverPos, setHoverPos] = React.useState({
743
+ x: 0,
744
+ y: 0
745
+ }); // chart-pixel coords inside .scatter-wrap
746
+ const wrapRef = React.useRef(null);
747
+ const W = 560,
748
+ H = 360,
749
+ padL = 56,
750
+ padR = 24,
751
+ padT = 24,
752
+ padB = 50;
753
+ const innerW = W - padL - padR,
754
+ innerH = H - padT - padB;
755
+ const maxDur = Math.max(...tests.map(t => t.avgDur));
756
+ const x = d => padL + d / maxDur * innerW;
757
+ const y = f => padT + innerH - f * innerH;
758
+ const flakeMid = 0.15,
759
+ flakeHigh = 0.35;
760
+ const sorted = [...tests].sort((a, b) => b.flakeRate - a.flakeRate);
761
+
762
+ // Convert SVG coords (which scale with viewBox) to live wrapper pixels for HTML tooltip
763
+ const svgToPx = (sx, sy) => {
764
+ const wrap = wrapRef.current;
765
+ if (!wrap) return {
766
+ x: 0,
767
+ y: 0
768
+ };
769
+ const r = wrap.getBoundingClientRect();
770
+ return {
771
+ x: sx / W * r.width,
772
+ y: sy / H * r.height
773
+ };
774
+ };
775
+ const onPointEnter = i => () => {
776
+ const t = tests[i];
777
+ const cx = x(t.avgDur),
778
+ cy = y(t.flakeRate);
779
+ setHoverPos(svgToPx(cx, cy));
780
+ setHover(i);
781
+ };
782
+ return /*#__PURE__*/React.createElement("div", {
783
+ style: {
784
+ display: 'grid',
785
+ gridTemplateColumns: '1fr 240px',
786
+ gap: 20,
787
+ alignItems: 'start'
788
+ }
789
+ }, /*#__PURE__*/React.createElement("div", {
790
+ ref: wrapRef,
791
+ className: "scatter-wrap",
792
+ style: {
793
+ position: 'relative'
794
+ },
795
+ onMouseLeave: () => setHover(null)
796
+ }, /*#__PURE__*/React.createElement("svg", {
797
+ viewBox: `0 0 ${W} ${H}`,
798
+ width: "100%",
799
+ style: {
800
+ display: 'block'
801
+ },
802
+ preserveAspectRatio: "xMidYMid meet"
803
+ }, /*#__PURE__*/React.createElement("rect", {
804
+ x: padL,
805
+ y: padT,
806
+ width: innerW,
807
+ height: y(flakeHigh) - padT,
808
+ fill: "var(--status-failed-bg)",
809
+ fillOpacity: "0.6"
810
+ }), /*#__PURE__*/React.createElement("rect", {
811
+ x: padL,
812
+ y: y(flakeHigh),
813
+ width: innerW,
814
+ height: y(flakeMid) - y(flakeHigh),
815
+ fill: "var(--status-broken-bg)",
816
+ fillOpacity: "0.55"
817
+ }), /*#__PURE__*/React.createElement("rect", {
818
+ x: padL,
819
+ y: y(flakeMid),
820
+ width: innerW,
821
+ height: padT + innerH - y(flakeMid),
822
+ fill: "var(--status-passed-bg)",
823
+ fillOpacity: "0.45"
824
+ }), /*#__PURE__*/React.createElement("text", {
825
+ x: padL + 10,
826
+ y: padT + 16,
827
+ fontFamily: "var(--font-mono)",
828
+ fontSize: "10",
829
+ fill: "var(--status-failed)",
830
+ letterSpacing: "1.5"
831
+ }, "CRITICAL \u226535%"), /*#__PURE__*/React.createElement("text", {
832
+ x: padL + 10,
833
+ y: y(flakeHigh) + 14,
834
+ fontFamily: "var(--font-mono)",
835
+ fontSize: "10",
836
+ fill: "var(--status-broken-fg)",
837
+ letterSpacing: "1.5"
838
+ }, "UNSTABLE 15\u201335%"), /*#__PURE__*/React.createElement("text", {
839
+ x: padL + 10,
840
+ y: y(flakeMid) + 14,
841
+ fontFamily: "var(--font-mono)",
842
+ fontSize: "10",
843
+ fill: "var(--status-passed)",
844
+ letterSpacing: "1.5"
845
+ }, "HEALTHY <15%"), [0, 0.15, 0.35, 0.5, 0.75, 1].map(f => /*#__PURE__*/React.createElement("g", {
846
+ key: f
847
+ }, /*#__PURE__*/React.createElement("line", {
848
+ x1: padL,
849
+ x2: padL + innerW,
850
+ y1: y(f),
851
+ y2: y(f),
852
+ stroke: "var(--line)",
853
+ strokeWidth: "1"
854
+ }), /*#__PURE__*/React.createElement("text", {
855
+ x: padL - 10,
856
+ y: y(f) + 4,
857
+ textAnchor: "end",
858
+ fontFamily: "var(--font-mono)",
859
+ fontSize: "11",
860
+ fill: "var(--fg3)",
861
+ fontVariantNumeric: "tabular-nums"
862
+ }, Math.round(f * 100), "%"))), [0, maxDur * 0.25, maxDur * 0.5, maxDur * 0.75, maxDur].map((d, i) => /*#__PURE__*/React.createElement("g", {
863
+ key: i
864
+ }, /*#__PURE__*/React.createElement("line", {
865
+ x1: x(d),
866
+ x2: x(d),
867
+ y1: padT,
868
+ y2: padT + innerH,
869
+ stroke: "var(--line)",
870
+ strokeWidth: "1",
871
+ opacity: i === 0 ? 0 : 0.5
872
+ }), /*#__PURE__*/React.createElement("text", {
873
+ x: x(d),
874
+ y: padT + innerH + 16,
875
+ textAnchor: "middle",
876
+ fontFamily: "var(--font-mono)",
877
+ fontSize: "11",
878
+ fill: "var(--fg3)",
879
+ fontVariantNumeric: "tabular-nums"
880
+ }, (d / 1000).toFixed(1), "s"))), /*#__PURE__*/React.createElement("text", {
881
+ x: padL - 44,
882
+ y: padT - 8,
883
+ fontFamily: "var(--font-mono)",
884
+ fontSize: "10",
885
+ fill: "var(--fg3)",
886
+ letterSpacing: "1.5"
887
+ }, "FLAKE RATE"), /*#__PURE__*/React.createElement("text", {
888
+ x: padL + innerW,
889
+ y: padT + innerH + 36,
890
+ textAnchor: "end",
891
+ fontFamily: "var(--font-mono)",
892
+ fontSize: "10",
893
+ fill: "var(--fg3)",
894
+ letterSpacing: "1.5"
895
+ }, "AVG DURATION \u2192"), /*#__PURE__*/React.createElement("line", {
896
+ x1: padL,
897
+ x2: padL + innerW,
898
+ y1: padT + innerH,
899
+ y2: padT + innerH,
900
+ stroke: "var(--line-strong)"
901
+ }), /*#__PURE__*/React.createElement("line", {
902
+ x1: padL,
903
+ x2: padL,
904
+ y1: padT,
905
+ y2: padT + innerH,
906
+ stroke: "var(--line-strong)"
907
+ }), tests.map((t, i) => {
908
+ const cx = x(t.avgDur),
909
+ cy = y(t.flakeRate);
910
+ const c = t.flakeRate >= flakeHigh ? 'var(--status-failed)' : t.flakeRate >= flakeMid ? 'var(--status-broken)' : 'var(--status-passed)';
911
+ const isHover = hoverIdx === i;
912
+ return /*#__PURE__*/React.createElement("circle", {
913
+ key: i,
914
+ cx: cx,
915
+ cy: cy,
916
+ r: isHover ? 8 : 6,
917
+ fill: c,
918
+ fillOpacity: isHover ? 1 : 0.78,
919
+ stroke: c,
920
+ strokeWidth: isHover ? 2 : 1,
921
+ style: {
922
+ cursor: 'pointer',
923
+ transition: 'r 120ms, fill-opacity 120ms'
924
+ },
925
+ onMouseEnter: onPointEnter(i)
926
+ });
927
+ })), hoverIdx !== null && (() => {
928
+ const t = tests[hoverIdx];
929
+ const wrap = wrapRef.current;
930
+ const w = wrap ? wrap.getBoundingClientRect().width : W;
931
+ const h = wrap ? wrap.getBoundingClientRect().height : H;
932
+ // edge-aware: flip horizontally / vertically if too close to edges
933
+ const TT_W = 196,
934
+ TT_H = 50;
935
+ let left = hoverPos.x + 14;
936
+ let top = hoverPos.y - TT_H - 10;
937
+ if (left + TT_W > w - 6) left = hoverPos.x - 14 - TT_W;
938
+ if (top < 6) top = hoverPos.y + 14;
939
+ return /*#__PURE__*/React.createElement("div", {
940
+ style: {
941
+ position: 'absolute',
942
+ left,
943
+ top,
944
+ width: TT_W,
945
+ pointerEvents: 'none',
946
+ background: '#0B1220',
947
+ color: '#fff',
948
+ border: '1px solid rgba(255,255,255,0.08)',
949
+ borderRadius: 6,
950
+ padding: '8px 10px',
951
+ boxShadow: '0 6px 18px rgba(0,0,0,0.35)',
952
+ fontFamily: 'var(--font-mono)',
953
+ zIndex: 10
954
+ }
955
+ }, /*#__PURE__*/React.createElement("div", {
956
+ style: {
957
+ fontSize: 12.5,
958
+ fontWeight: 700,
959
+ lineHeight: 1.2,
960
+ whiteSpace: 'nowrap',
961
+ overflow: 'hidden',
962
+ textOverflow: 'ellipsis'
963
+ }
964
+ }, t.name), /*#__PURE__*/React.createElement("div", {
965
+ style: {
966
+ fontSize: 11,
967
+ color: 'rgba(255,255,255,0.65)',
968
+ marginTop: 3
969
+ }
970
+ }, Math.round(t.flakeRate * 100), "% flaky \xB7 ", (t.avgDur / 1000).toFixed(1), "s avg"));
971
+ })()), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
972
+ className: "k-overline",
973
+ style: {
974
+ marginBottom: 8
975
+ }
976
+ }, "Worst offenders"), /*#__PURE__*/React.createElement("div", {
977
+ style: {
978
+ border: '1px solid var(--line)',
979
+ borderRadius: 6,
980
+ overflow: 'hidden',
981
+ background: 'var(--bg-elev)'
982
+ }
983
+ }, sorted.slice(0, 8).map((t, i) => {
984
+ const tIdx = tests.indexOf(t);
985
+ const c = t.flakeRate >= flakeHigh ? 'var(--status-failed)' : t.flakeRate >= flakeMid ? 'var(--status-broken)' : 'var(--status-passed)';
986
+ const active = hoverIdx === tIdx;
987
+ return /*#__PURE__*/React.createElement("div", {
988
+ key: i,
989
+ onMouseEnter: () => {
990
+ // also reposition the tooltip near this point in the chart
991
+ const cx = x(t.avgDur),
992
+ cy = y(t.flakeRate);
993
+ setHoverPos(svgToPx(cx, cy));
994
+ setHover(tIdx);
995
+ },
996
+ onMouseLeave: () => setHover(null),
997
+ style: {
998
+ display: 'grid',
999
+ gridTemplateColumns: '8px 1fr auto',
1000
+ gap: 8,
1001
+ padding: '8px 10px',
1002
+ borderBottom: i < 7 ? '1px solid var(--line)' : 'none',
1003
+ alignItems: 'center',
1004
+ cursor: 'pointer',
1005
+ background: active ? 'var(--bg-hover)' : 'transparent',
1006
+ transition: 'background var(--dur-fast)'
1007
+ }
1008
+ }, /*#__PURE__*/React.createElement("span", {
1009
+ style: {
1010
+ width: 8,
1011
+ height: 8,
1012
+ borderRadius: 2,
1013
+ background: c
1014
+ }
1015
+ }), /*#__PURE__*/React.createElement("div", {
1016
+ style: {
1017
+ fontFamily: 'var(--font-mono)',
1018
+ fontSize: 11.5,
1019
+ color: 'var(--fg1)',
1020
+ overflow: 'hidden',
1021
+ textOverflow: 'ellipsis',
1022
+ whiteSpace: 'nowrap'
1023
+ }
1024
+ }, t.name), /*#__PURE__*/React.createElement("div", {
1025
+ style: {
1026
+ fontFamily: 'var(--font-mono)',
1027
+ fontSize: 11,
1028
+ color: c,
1029
+ fontWeight: 700,
1030
+ fontVariantNumeric: 'tabular-nums'
1031
+ }
1032
+ }, Math.round(t.flakeRate * 100), "%"));
1033
+ }))));
1034
+ }
1035
+
1036
+ // ============== HORIZONTAL BAR CHART ==============
1037
+ function HBars({
1038
+ data,
1039
+ max
1040
+ }) {
1041
+ const m = max || Math.max(...data.map(d => d.value));
1042
+ return /*#__PURE__*/React.createElement("div", {
1043
+ style: {
1044
+ display: 'flex',
1045
+ flexDirection: 'column',
1046
+ gap: 8
1047
+ }
1048
+ }, data.map((d, i) => /*#__PURE__*/React.createElement("div", {
1049
+ key: i,
1050
+ style: {
1051
+ display: 'grid',
1052
+ gridTemplateColumns: '180px 1fr 50px',
1053
+ alignItems: 'center',
1054
+ gap: 12
1055
+ }
1056
+ }, /*#__PURE__*/React.createElement("div", {
1057
+ style: {
1058
+ fontFamily: 'var(--font-mono)',
1059
+ fontSize: 12,
1060
+ color: 'var(--fg1)',
1061
+ overflow: 'hidden',
1062
+ textOverflow: 'ellipsis',
1063
+ whiteSpace: 'nowrap'
1064
+ }
1065
+ }, d.label), /*#__PURE__*/React.createElement("div", {
1066
+ style: {
1067
+ height: 14,
1068
+ background: 'var(--bg-sunken)',
1069
+ borderRadius: 3,
1070
+ overflow: 'hidden'
1071
+ }
1072
+ }, /*#__PURE__*/React.createElement("div", {
1073
+ style: {
1074
+ width: `${d.value / m * 100}%`,
1075
+ height: '100%',
1076
+ background: d.color || 'var(--brand-blue-500)',
1077
+ borderRadius: 3
1078
+ }
1079
+ })), /*#__PURE__*/React.createElement("div", {
1080
+ style: {
1081
+ fontFamily: 'var(--font-mono)',
1082
+ fontSize: 12,
1083
+ color: 'var(--fg2)',
1084
+ textAlign: 'right'
1085
+ }
1086
+ }, d.display ?? d.value))));
1087
+ }
1088
+
1089
+ // ============== DURATION HISTOGRAM ==============
1090
+ function DurationHistogram({
1091
+ buckets
1092
+ }) {
1093
+ const W = 720,
1094
+ H = 180,
1095
+ padL = 28,
1096
+ padR = 12,
1097
+ padT = 12,
1098
+ padB = 28;
1099
+ const innerW = W - padL - padR,
1100
+ innerH = H - padT - padB;
1101
+ const max = Math.max(...buckets.map(b => b.n));
1102
+ const bw = innerW / buckets.length - 4;
1103
+ return /*#__PURE__*/React.createElement("svg", {
1104
+ viewBox: `0 0 ${W} ${H}`,
1105
+ width: "100%",
1106
+ style: {
1107
+ display: 'block'
1108
+ }
1109
+ }, [0, max / 2, max].map((m, i) => /*#__PURE__*/React.createElement("g", {
1110
+ key: i
1111
+ }, /*#__PURE__*/React.createElement("line", {
1112
+ x1: padL,
1113
+ x2: W - padR,
1114
+ y1: padT + innerH - m / max * innerH,
1115
+ y2: padT + innerH - m / max * innerH,
1116
+ stroke: "var(--line)",
1117
+ strokeDasharray: "2 4"
1118
+ }), /*#__PURE__*/React.createElement("text", {
1119
+ x: padL - 6,
1120
+ y: padT + innerH - m / max * innerH + 3,
1121
+ textAnchor: "end",
1122
+ fontFamily: "var(--font-mono)",
1123
+ fontSize: "10",
1124
+ fill: "var(--fg3)"
1125
+ }, Math.round(m)))), buckets.map((b, i) => {
1126
+ const h = b.n / max * innerH;
1127
+ const x = padL + i * (innerW / buckets.length) + 2;
1128
+ return /*#__PURE__*/React.createElement("g", {
1129
+ key: i
1130
+ }, /*#__PURE__*/React.createElement("rect", {
1131
+ x: x,
1132
+ y: padT + innerH - h,
1133
+ width: bw,
1134
+ height: h,
1135
+ fill: "var(--brand-blue-500)",
1136
+ fillOpacity: "0.85",
1137
+ rx: "2"
1138
+ }), /*#__PURE__*/React.createElement("text", {
1139
+ x: x + bw / 2,
1140
+ y: H - 10,
1141
+ textAnchor: "middle",
1142
+ fontFamily: "var(--font-mono)",
1143
+ fontSize: "10",
1144
+ fill: "var(--fg3)"
1145
+ }, b.label));
1146
+ }));
1147
+ }
1148
+ Object.assign(window, {
1149
+ TrendChartV2,
1150
+ TimelineGantt,
1151
+ RetryWaterfall,
1152
+ SuiteHeatmap,
1153
+ FlakeScatter,
1154
+ HBars,
1155
+ DurationHistogram
1156
+ });