@unofficialbox/box-open-elements 0.28.7 → 0.29.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.
- package/README.md +13 -13
- package/dist/element-maps.d.ts +15 -1
- package/dist/entries/box-plot.d.ts +1 -0
- package/dist/entries/box-plot.js +1 -0
- package/dist/entries/trace-waterfall.d.ts +1 -0
- package/dist/entries/trace-waterfall.js +1 -0
- package/dist/native-types.d.ts +2491 -0
- package/dist/native-types.js +1 -0
- package/dist/patterns/form-wizard/form-wizard.d.ts +9 -1
- package/dist/patterns/insights/box-plot.d.ts +55 -0
- package/dist/patterns/insights/box-plot.js +201 -0
- package/dist/patterns/insights/index.d.ts +1 -0
- package/dist/patterns/insights/index.js +1 -0
- package/dist/patterns/process-modeler/design.d.ts +1 -1
- package/dist/patterns/process-modeler/design.js +7 -1
- package/dist/patterns/process-modeler/process-modeler.d.ts +9 -0
- package/dist/patterns/process-modeler/process-modeler.js +158 -40
- package/dist/patterns/run/index.d.ts +1 -0
- package/dist/patterns/run/index.js +1 -0
- package/dist/patterns/run/run-trace.d.ts +3 -0
- package/dist/patterns/run/run-trace.js +52 -13
- package/dist/patterns/run/trace-waterfall.d.ts +80 -0
- package/dist/patterns/run/trace-waterfall.js +360 -0
- package/dist/react-jsx.d.ts +21 -0
- package/dist/react-jsx.js +1 -0
- package/package.json +15 -2
|
@@ -13,6 +13,7 @@ const emit = (element, name, detail) => element.dispatchEvent(new CustomEvent(na
|
|
|
13
13
|
composed: true,
|
|
14
14
|
cancelable: true,
|
|
15
15
|
}));
|
|
16
|
+
const formatRunDuration = (ms) => ms >= 1000 ? `${(ms / 1000).toFixed(ms >= 10000 ? 0 : 1)} s` : `${Math.round(ms)} ms`;
|
|
16
17
|
/** Projects host documents without owning workflow schemas or persistence. */
|
|
17
18
|
export class ProcessModeler extends BaseElement {
|
|
18
19
|
static tagName = "box-process-modeler";
|
|
@@ -47,6 +48,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
47
48
|
insertion;
|
|
48
49
|
keyboardInsertion;
|
|
49
50
|
keyboardReturn;
|
|
51
|
+
transientChooserAnchor;
|
|
50
52
|
stopKeyboardAnchor;
|
|
51
53
|
activeDrawer;
|
|
52
54
|
drawerReturn;
|
|
@@ -322,7 +324,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
322
324
|
if (command.layout)
|
|
323
325
|
this.layoutValue = structuredClone(command.layout);
|
|
324
326
|
this.refresh();
|
|
325
|
-
|
|
327
|
+
const inserted = edit.type === 'insert' && !edit.boxId && !command.layout
|
|
328
|
+
? this.projection.boxes.find(box => !previousIds.has(box.id)) : undefined;
|
|
329
|
+
const reflowed = inserted ? this.reflowStraightInsert(edit, inserted.id, before) : false;
|
|
330
|
+
if (!reflowed &&
|
|
331
|
+
!command.layout &&
|
|
326
332
|
edit.position &&
|
|
327
333
|
Number.isFinite(edit.position.x) &&
|
|
328
334
|
Number.isFinite(edit.position.y)) {
|
|
@@ -422,6 +428,42 @@ export class ProcessModeler extends BaseElement {
|
|
|
422
428
|
}
|
|
423
429
|
return next;
|
|
424
430
|
}
|
|
431
|
+
/** Make room only on a simple, root-level horizontal connection. */
|
|
432
|
+
reflowStraightInsert(edit, insertedId, before) {
|
|
433
|
+
const source = this.projection.boxes.find(box => box.id === edit.from);
|
|
434
|
+
const target = this.projection.boxes.find(box => box.id === edit.to);
|
|
435
|
+
const inserted = this.projection.boxes.find(box => box.id === insertedId);
|
|
436
|
+
if (!source || !target || !inserted || source.parentId || target.parentId || inserted.parentId)
|
|
437
|
+
return false;
|
|
438
|
+
const start = before.boxes[source.id], end = before.boxes[target.id];
|
|
439
|
+
const fresh = this.layoutValue.boxes[insertedId];
|
|
440
|
+
if (!start || !end || !fresh)
|
|
441
|
+
return false;
|
|
442
|
+
const sourceWidth = start.width ?? 224, sourceHeight = start.height ?? 64;
|
|
443
|
+
const targetHeight = end.height ?? 64, width = fresh.width ?? 224, height = fresh.height ?? 64;
|
|
444
|
+
if (start.x + sourceWidth >= end.x || Math.abs(start.y + sourceHeight / 2 - end.y - targetHeight / 2) > 24)
|
|
445
|
+
return false;
|
|
446
|
+
const x = start.x + sourceWidth + 80;
|
|
447
|
+
const delta = Math.max(0, x + width + 80 - end.x);
|
|
448
|
+
const next = this.layout;
|
|
449
|
+
const shifted = new Set(this.projection.boxes.filter(box => !box.parentId && before.boxes[box.id]?.x >= end.x).map(box => box.id));
|
|
450
|
+
for (let changed = true; changed;) {
|
|
451
|
+
changed = false;
|
|
452
|
+
for (const box of this.projection.boxes)
|
|
453
|
+
if (box.parentId && shifted.has(box.parentId) && !shifted.has(box.id)) {
|
|
454
|
+
shifted.add(box.id);
|
|
455
|
+
changed = true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
for (const box of this.projection.boxes) {
|
|
459
|
+
if (box.id === insertedId || !shifted.has(box.id))
|
|
460
|
+
continue;
|
|
461
|
+
next.boxes[box.id] = { ...next.boxes[box.id], x: next.boxes[box.id].x + delta };
|
|
462
|
+
}
|
|
463
|
+
next.boxes[insertedId] = { ...fresh, x, y: start.y + (sourceHeight - height) / 2 };
|
|
464
|
+
this.layoutValue = next;
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
425
467
|
tidy() {
|
|
426
468
|
if (this.locked)
|
|
427
469
|
return;
|
|
@@ -439,7 +481,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
439
481
|
element.animate([{ transform: `translate(${dx}px, ${dy}px)` }, { transform: 'translate(0, 0)' }], { duration: 320, easing: 'ease-out' });
|
|
440
482
|
}
|
|
441
483
|
}
|
|
442
|
-
this.
|
|
484
|
+
this.fitTo(this.narrowValue ? 0.55 : 0.7);
|
|
443
485
|
}
|
|
444
486
|
resize(id, width, height) {
|
|
445
487
|
if (!this.layoutValue.boxes[id] ||
|
|
@@ -510,10 +552,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
510
552
|
this.viewport.zoom = next;
|
|
511
553
|
this.paintViewport();
|
|
512
554
|
}
|
|
513
|
-
fit() {
|
|
555
|
+
fit() { this.fitTo(0.25); }
|
|
556
|
+
fitTo(minimumZoom) {
|
|
514
557
|
const bounds = this.bounds();
|
|
515
558
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
516
|
-
this.viewport.zoom = Math.max(
|
|
559
|
+
this.viewport.zoom = Math.max(minimumZoom, Math.min(1.5, ((canvas.clientWidth || 700) - 40) / bounds.width, ((canvas.clientHeight || 480) - 40) / bounds.height));
|
|
517
560
|
this.viewport.x = 20 - bounds.x * this.viewport.zoom;
|
|
518
561
|
this.viewport.y = 20 - bounds.y * this.viewport.zoom;
|
|
519
562
|
this.paintViewport();
|
|
@@ -1355,7 +1398,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
1355
1398
|
if (event.ctrlKey && event.altKey && this.catalog.length && !this.disableConnections) {
|
|
1356
1399
|
const side = direction[0] < 0 ? "west" : direction[0] > 0 ? "east" : direction[1] < 0 ? "north" : "south";
|
|
1357
1400
|
const pos = this.layoutValue.boxes[box.id];
|
|
1358
|
-
|
|
1401
|
+
const anchor = Array.from(this.shadowRoot.querySelectorAll('[part=port]'))
|
|
1402
|
+
.find(element => element.dataset.owner === box.id && element.dataset.side === side);
|
|
1403
|
+
const returnFocus = Array.from(this.shadowRoot.querySelectorAll('[data-box-id]'))
|
|
1404
|
+
.find(element => element.dataset.boxId === box.id);
|
|
1405
|
+
this.openAnchoredChooser(this.nextEdit(box, side, { x: pos.x + direction[0] * 320, y: pos.y + direction[1] * 170 }), this.nextChooserTitle(box, side), anchor, returnFocus);
|
|
1359
1406
|
return;
|
|
1360
1407
|
}
|
|
1361
1408
|
if (event.altKey) {
|
|
@@ -1479,7 +1526,6 @@ export class ProcessModeler extends BaseElement {
|
|
|
1479
1526
|
if (!ghost) {
|
|
1480
1527
|
ghost = svgElement("polyline");
|
|
1481
1528
|
ghost.setAttribute("part", "connection-preview");
|
|
1482
|
-
ghost.style.cssText = "fill:none;stroke:var(--boe-token-surface-surface-brand,#0061d5);stroke-width:3";
|
|
1483
1529
|
this.shadowRoot.querySelector('[part=lines]').append(ghost);
|
|
1484
1530
|
}
|
|
1485
1531
|
const start = this.portDrag.start;
|
|
@@ -1488,7 +1534,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
1488
1534
|
const pinned = target ? this.sideAt(target, point) : undefined;
|
|
1489
1535
|
this.shadowRoot.querySelectorAll('[data-box-id]').forEach(box => { box.dataset.connectTarget = String(box.dataset.boxId === target?.id); });
|
|
1490
1536
|
this.shadowRoot.querySelectorAll('[part=port]').forEach(port => { port.dataset.hot = String(port.dataset.owner === target?.id && port.dataset.side === pinned); });
|
|
1491
|
-
this.showConnectTooltip(point, target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Connect to ${target.title}` : 'Drop
|
|
1537
|
+
this.showConnectTooltip(point, target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Connect to ${target.title}` : 'Drop here to add a connected step');
|
|
1492
1538
|
return;
|
|
1493
1539
|
}
|
|
1494
1540
|
if (this.endDrag) {
|
|
@@ -1589,12 +1635,23 @@ export class ProcessModeler extends BaseElement {
|
|
|
1589
1635
|
if (Math.hypot(point.x - port.start.x, point.y - port.start.y) < 8) {
|
|
1590
1636
|
const pos = this.layoutValue.boxes[port.id];
|
|
1591
1637
|
const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[port.side];
|
|
1592
|
-
this.
|
|
1638
|
+
const source = this.projection.boxes.find(box => box.id === port.id);
|
|
1639
|
+
const anchor = Array.from(this.shadowRoot.querySelectorAll('[part=port]'))
|
|
1640
|
+
.find(element => element.dataset.owner === port.id && element.dataset.side === port.side);
|
|
1641
|
+
this.openAnchoredChooser(this.nextEdit(source, port.side, { x: pos.x + offsets[0], y: pos.y + offsets[1] }), this.nextChooserTitle(source, port.side), anchor);
|
|
1593
1642
|
}
|
|
1594
1643
|
else {
|
|
1595
1644
|
const target = this.boxAt(point, port.id);
|
|
1596
1645
|
if (target)
|
|
1597
1646
|
this.requestEdit({ type: "connect", from: port.id, to: target.id, fromSide: port.side, toSide: this.sideAt(target, point) });
|
|
1647
|
+
else {
|
|
1648
|
+
const source = this.projection.boxes.find(box => box.id === port.id);
|
|
1649
|
+
const frame = this.boxAt(point, undefined, true);
|
|
1650
|
+
if ((frame?.id ?? null) !== (source.parentId ?? null))
|
|
1651
|
+
this.setStatus('Drop inside the same frame to add a connected step');
|
|
1652
|
+
else
|
|
1653
|
+
this.openChooserAtPoint({ type: 'add', from: port.id, fromSide: port.side, parentId: source.parentId, position: point }, `Add after ${source.title}`, point);
|
|
1654
|
+
}
|
|
1598
1655
|
}
|
|
1599
1656
|
}
|
|
1600
1657
|
return;
|
|
@@ -1773,11 +1830,45 @@ export class ProcessModeler extends BaseElement {
|
|
|
1773
1830
|
promoteModal(chooser);
|
|
1774
1831
|
picker.focus();
|
|
1775
1832
|
}
|
|
1833
|
+
nextEdit(box, side, position) {
|
|
1834
|
+
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape;
|
|
1835
|
+
const outgoing = this.projection.lines.filter(line => line.from === box.id);
|
|
1836
|
+
if ((!side || side === 'east') && shape !== 'gateway' && outgoing.length === 1)
|
|
1837
|
+
return { type: 'insert', lineId: outgoing[0].id, from: box.id, to: outgoing[0].to };
|
|
1838
|
+
return { type: 'add', from: box.id, ...(side ? { fromSide: side } : {}), parentId: box.parentId, ...(position ? { position } : {}) };
|
|
1839
|
+
}
|
|
1840
|
+
nextChooserTitle(box, side) {
|
|
1841
|
+
const edit = this.nextEdit(box, side);
|
|
1842
|
+
if (edit.type === 'insert') {
|
|
1843
|
+
const target = this.projection.boxes.find(candidate => candidate.id === edit.to);
|
|
1844
|
+
return `Insert between ${box.title} and ${target?.title ?? 'the next step'}`;
|
|
1845
|
+
}
|
|
1846
|
+
if (box.shape === 'gateway')
|
|
1847
|
+
return `Add a path from ${box.title}`;
|
|
1848
|
+
return side && side !== 'east' ? `Add ${{ north: 'above', south: 'below', west: 'before' }[side]} ${box.title}, connected` : `Add after ${box.title}`;
|
|
1849
|
+
}
|
|
1850
|
+
openChooserAtPoint(edit, title, point, returnFocus) {
|
|
1851
|
+
if (this.keyboardInsertion)
|
|
1852
|
+
this.closeKeyboardChooser();
|
|
1853
|
+
const anchor = document.createElement('span');
|
|
1854
|
+
anchor.style.cssText = `position:absolute;left:${point.x}px;top:${point.y}px;width:1px;height:1px;pointer-events:none`;
|
|
1855
|
+
this.shadowRoot.querySelector('[part=world]').append(anchor);
|
|
1856
|
+
this.transientChooserAnchor = anchor;
|
|
1857
|
+
this.openAnchoredChooser(edit, title, anchor, returnFocus);
|
|
1858
|
+
}
|
|
1859
|
+
openLineChooser(line, returnFocus) {
|
|
1860
|
+
const source = this.projection.boxes.find(box => box.id === line.from);
|
|
1861
|
+
const target = this.projection.boxes.find(box => box.id === line.to);
|
|
1862
|
+
const points = this.routedLines.get(line.id) ?? routeProcessLine(line, this.layoutValue, this.projection);
|
|
1863
|
+
this.openChooserAtPoint({ type: 'insert', lineId: line.id, from: line.from, to: line.to }, `Insert between ${source?.title ?? line.from} and ${target?.title ?? line.to}`, lineMidpoint(points), returnFocus);
|
|
1864
|
+
}
|
|
1776
1865
|
closeKeyboardChooser(returnFocus = false) {
|
|
1777
1866
|
const chooser = this.shadowRoot?.querySelector('[part=keyboard-chooser]');
|
|
1778
1867
|
dismissPopover(chooser ?? null);
|
|
1779
1868
|
this.stopKeyboardAnchor?.();
|
|
1780
1869
|
this.stopKeyboardAnchor = undefined;
|
|
1870
|
+
this.transientChooserAnchor?.remove();
|
|
1871
|
+
this.transientChooserAnchor = undefined;
|
|
1781
1872
|
document.removeEventListener('pointerdown', this.dismissKeyboardChooserOutside, true);
|
|
1782
1873
|
this.keyboardInsertion = undefined;
|
|
1783
1874
|
if (returnFocus)
|
|
@@ -1790,32 +1881,35 @@ export class ProcessModeler extends BaseElement {
|
|
|
1790
1881
|
this.closeKeyboardChooser();
|
|
1791
1882
|
};
|
|
1792
1883
|
openKeyboardChooser(box) {
|
|
1793
|
-
const chooser = this.shadowRoot.querySelector('[part=keyboard-chooser]');
|
|
1794
1884
|
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape ?? (box.frame ? 'frame' : 'task');
|
|
1795
1885
|
const outgoing = this.projection.lines.filter(line => line.from === box.id);
|
|
1796
1886
|
const target = outgoing.length === 1 ? this.projection.boxes.find(candidate => candidate.id === outgoing[0].to) : undefined;
|
|
1797
1887
|
const insert = shape !== 'gateway' && outgoing.length === 1 && target;
|
|
1888
|
+
const anchor = Array.from(this.shadowRoot.querySelectorAll('[data-box-id]'))
|
|
1889
|
+
.find(element => element.dataset.boxId === box.id);
|
|
1890
|
+
this.openAnchoredChooser(insert
|
|
1891
|
+
? { type: 'insert', lineId: outgoing[0].id, from: box.id, to: target.id }
|
|
1892
|
+
: { type: 'add', from: box.id }, insert ? `Insert between ${box.title} and ${target.title}`
|
|
1893
|
+
: shape === 'gateway' ? `Add a path from ${box.title}` : `Add after ${box.title}`, anchor);
|
|
1894
|
+
}
|
|
1895
|
+
openAnchoredChooser(edit, title, anchor, returnFocus) {
|
|
1896
|
+
const chooser = this.shadowRoot.querySelector('[part=keyboard-chooser]');
|
|
1798
1897
|
if (this.keyboardInsertion)
|
|
1799
1898
|
this.closeKeyboardChooser();
|
|
1800
|
-
this.keyboardInsertion =
|
|
1801
|
-
|
|
1802
|
-
: { type: 'add', from: box.id };
|
|
1803
|
-
chooser.querySelector('[part=chooser-title]').textContent = insert
|
|
1804
|
-
? `Insert between ${box.title} and ${target.title}`
|
|
1805
|
-
: shape === 'gateway' ? `Add a path from ${box.title}` : `Add after ${box.title}`;
|
|
1899
|
+
this.keyboardInsertion = edit;
|
|
1900
|
+
chooser.querySelector('[part=chooser-title]').textContent = title;
|
|
1806
1901
|
const picker = chooser.querySelector('box-kind-picker');
|
|
1807
1902
|
picker.catalog = this.catalog.filter(kind => !kind.placement);
|
|
1808
1903
|
picker.refresh();
|
|
1809
|
-
this.keyboardReturn =
|
|
1810
|
-
|
|
1811
|
-
if (!this.keyboardReturn || !promotePopover(chooser)) {
|
|
1904
|
+
this.keyboardReturn = returnFocus ?? anchor;
|
|
1905
|
+
if (!anchor || !promotePopover(chooser)) {
|
|
1812
1906
|
const edit = this.keyboardInsertion;
|
|
1813
1907
|
this.closeKeyboardChooser();
|
|
1814
1908
|
if (edit)
|
|
1815
1909
|
this.openChooser(edit);
|
|
1816
1910
|
return;
|
|
1817
1911
|
}
|
|
1818
|
-
this.stopKeyboardAnchor = trackAnchor(
|
|
1912
|
+
this.stopKeyboardAnchor = trackAnchor(anchor, chooser, {
|
|
1819
1913
|
placement: { side: 'right', align: 'start' }, offset: 8, padding: 12,
|
|
1820
1914
|
});
|
|
1821
1915
|
document.addEventListener('pointerdown', this.dismissKeyboardChooserOutside, true);
|
|
@@ -1970,14 +2064,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
1970
2064
|
element.append(loop);
|
|
1971
2065
|
}
|
|
1972
2066
|
const metrics = this.showLastRunValue ? this.lastRunValue?.steps[box.id] : undefined;
|
|
1973
|
-
if (
|
|
2067
|
+
if (this.showLastRunValue && this.lastRunValue && shape === 'task') {
|
|
1974
2068
|
const line = document.createElement("span");
|
|
1975
2069
|
line.setAttribute("part", "metrics");
|
|
1976
|
-
line.textContent = metrics.notInRun ? "Not in
|
|
1977
|
-
metrics.callsPerSecond === undefined ? "" : `${metrics.callsPerSecond.
|
|
1978
|
-
metrics.p95Ms === undefined ? "" : `p95 ${metrics.p95Ms
|
|
2070
|
+
line.textContent = !metrics || metrics.notInRun ? "Not in the last run" : [
|
|
2071
|
+
metrics.callsPerSecond === undefined ? "" : `${metrics.callsPerSecond.toFixed(1)}/s`,
|
|
2072
|
+
metrics.p95Ms === undefined ? "" : `p95 ${formatRunDuration(metrics.p95Ms)}`,
|
|
1979
2073
|
].filter(Boolean).join(" · ");
|
|
1980
|
-
if (metrics
|
|
2074
|
+
if (metrics?.failedShare !== undefined && metrics.failedShare > 0 && !metrics.notInRun) {
|
|
1981
2075
|
const failed = document.createElement('span');
|
|
1982
2076
|
failed.setAttribute('part', 'metric-warning');
|
|
1983
2077
|
failed.textContent = `${(metrics.failedShare * 100).toFixed(1)}% failed`;
|
|
@@ -2021,7 +2115,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2021
2115
|
else
|
|
2022
2116
|
this.select(box.id);
|
|
2023
2117
|
});
|
|
2024
|
-
if (!this.disableConnections && !this.locked) {
|
|
2118
|
+
if (!this.disableConnections && !this.locked && !['end', 'finish'].includes(box.kind)) {
|
|
2025
2119
|
for (const side of ["north", "east", "south", "west"]) {
|
|
2026
2120
|
const port = document.createElement("button");
|
|
2027
2121
|
port.type = "button";
|
|
@@ -2034,7 +2128,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2034
2128
|
if (!event.detail) {
|
|
2035
2129
|
const pos = this.layoutValue.boxes[box.id];
|
|
2036
2130
|
const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[side];
|
|
2037
|
-
this.
|
|
2131
|
+
this.openAnchoredChooser(this.nextEdit(box, side, { x: pos.x + offsets[0], y: pos.y + offsets[1] }), this.nextChooserTitle(box, side), port);
|
|
2038
2132
|
}
|
|
2039
2133
|
});
|
|
2040
2134
|
element.append(port);
|
|
@@ -2096,7 +2190,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2096
2190
|
if (line.dashed)
|
|
2097
2191
|
path.setAttribute("stroke-dasharray", "6 4");
|
|
2098
2192
|
if (this.showLastRunValue && line.share !== undefined) {
|
|
2099
|
-
path.style.strokeWidth = `${1.5 + Math.max(0, Math.min(1, line.share)) * 2}px`;
|
|
2193
|
+
path.style.strokeWidth = `${Math.round((1.5 + Math.max(0, Math.min(1, line.share)) * 2.5) * 100) / 100}px`;
|
|
2100
2194
|
path.setAttribute('aria-label', `${Math.round(line.share * 100)}% of last-run traffic`);
|
|
2101
2195
|
}
|
|
2102
2196
|
lines.append(path);
|
|
@@ -2138,6 +2232,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
2138
2232
|
const branchIndex = this.projection.lines.filter(candidate => candidate.from === line.from).indexOf(line);
|
|
2139
2233
|
text.textContent = line.label ?? (line.weight !== undefined ? String(line.weight) : /decision/i.test(source.kind) ? ["Yes", "No"][branchIndex] ?? `Route ${branchIndex + 1}` : /try/i.test(source.kind) && line.dashed ? "If it fails" : "");
|
|
2140
2234
|
label.append(text);
|
|
2235
|
+
if (this.showLastRunValue && line.share !== undefined) {
|
|
2236
|
+
const share = document.createElement('small');
|
|
2237
|
+
share.textContent = `${Math.round(line.share * 100)}%`;
|
|
2238
|
+
label.append(share);
|
|
2239
|
+
}
|
|
2141
2240
|
const actions = document.createElement("div");
|
|
2142
2241
|
actions.setAttribute("part", "connection-actions");
|
|
2143
2242
|
label.append(actions);
|
|
@@ -2186,7 +2285,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2186
2285
|
to: line.to,
|
|
2187
2286
|
};
|
|
2188
2287
|
if (type === "insert" && this.catalog.length) {
|
|
2189
|
-
this.
|
|
2288
|
+
this.openLineChooser(line, button);
|
|
2190
2289
|
}
|
|
2191
2290
|
else
|
|
2192
2291
|
this.requestEdit(edit);
|
|
@@ -2675,8 +2774,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
2675
2774
|
const report = document.createElement('section');
|
|
2676
2775
|
report.setAttribute('part', 'inspector-metrics');
|
|
2677
2776
|
const title = document.createElement('h3');
|
|
2678
|
-
title.textContent =
|
|
2777
|
+
title.textContent = 'Last run';
|
|
2679
2778
|
report.append(title);
|
|
2779
|
+
if (this.lastRunValue?.label) {
|
|
2780
|
+
const source = document.createElement('p');
|
|
2781
|
+
source.textContent = `From ${this.lastRunValue.label}.`;
|
|
2782
|
+
report.append(source);
|
|
2783
|
+
}
|
|
2680
2784
|
if (metrics.notInRun) {
|
|
2681
2785
|
const note = document.createElement('p');
|
|
2682
2786
|
note.textContent = 'Not in this run';
|
|
@@ -2684,9 +2788,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2684
2788
|
}
|
|
2685
2789
|
else {
|
|
2686
2790
|
const list = document.createElement('dl');
|
|
2687
|
-
for (const [label, value] of [['Per second', metrics.callsPerSecond
|
|
2688
|
-
if (value === undefined)
|
|
2689
|
-
continue;
|
|
2791
|
+
for (const [label, value] of [['Per second', metrics.callsPerSecond === undefined ? '–' : metrics.callsPerSecond.toFixed(1)], ['95% finished within', metrics.p95Ms === undefined ? '–' : formatRunDuration(metrics.p95Ms)], ['Failed', metrics.failedShare === undefined ? '–' : `${(metrics.failedShare * 100).toFixed(1)}%`]]) {
|
|
2690
2792
|
const term = document.createElement('dt');
|
|
2691
2793
|
term.textContent = label;
|
|
2692
2794
|
const detail = document.createElement('dd');
|
|
@@ -2718,26 +2820,35 @@ export class ProcessModeler extends BaseElement {
|
|
|
2718
2820
|
}
|
|
2719
2821
|
renderSelectionToolbar() {
|
|
2720
2822
|
const toolbar = this.shadowRoot.querySelector('[part=selection-toolbar]');
|
|
2721
|
-
const key = `${[...this.selectedIds].join(',')}|${this.selectedLineId}|${Boolean(this.selectedLineId && this.layoutValue.lines?.[this.selectedLineId])}|${this.locked}|${this.narrowValue}|${this.projection.lines.filter(line => line.from === this.selectedId).map(line => `${line.id}:${line.label}:${line.dashed}`).join(',')}`;
|
|
2823
|
+
const key = `${[...this.selectedIds].join(',')}|${this.selected?.kind}|${this.selected?.shape}|${this.selectedLineId}|${Boolean(this.selectedLineId && this.layoutValue.lines?.[this.selectedLineId])}|${this.locked}|${this.narrowValue}|${this.projection.lines.filter(line => line.from === this.selectedId).map(line => `${line.id}:${line.label}:${line.dashed}`).join(',')}`;
|
|
2722
2824
|
if (key === this.selectionToolbarKey)
|
|
2723
2825
|
return;
|
|
2724
2826
|
this.selectionToolbarKey = key;
|
|
2725
2827
|
toolbar.replaceChildren();
|
|
2726
2828
|
if (!this.selectedIds.size && !this.selectedLineId)
|
|
2727
2829
|
return;
|
|
2728
|
-
const add = (label, command, action) => {
|
|
2830
|
+
const add = (label, command, action, withPlus = false) => {
|
|
2729
2831
|
const button = document.createElement('button');
|
|
2730
2832
|
button.type = 'button';
|
|
2731
2833
|
button.dataset.selectionCommand = command;
|
|
2732
|
-
button.textContent = label;
|
|
2733
2834
|
button.setAttribute('aria-label', label);
|
|
2734
2835
|
button.disabled = this.locked;
|
|
2735
2836
|
button.onclick = action;
|
|
2837
|
+
if (withPlus) {
|
|
2838
|
+
const icon = document.createElement('span');
|
|
2839
|
+
icon.setAttribute('part', 'selection-plus');
|
|
2840
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
2841
|
+
icon.textContent = '+';
|
|
2842
|
+
button.append(icon);
|
|
2843
|
+
}
|
|
2844
|
+
const text = document.createElement('span');
|
|
2845
|
+
text.textContent = label;
|
|
2846
|
+
button.append(text);
|
|
2736
2847
|
toolbar.append(button);
|
|
2737
2848
|
};
|
|
2738
2849
|
if (this.selectedLine) {
|
|
2739
2850
|
const line = this.selectedLine;
|
|
2740
|
-
add('Insert a step', 'insert', () => this.
|
|
2851
|
+
add('Insert a step', 'insert', () => this.openLineChooser(line, toolbar.querySelector('[data-selection-command=insert]') ?? undefined), true);
|
|
2741
2852
|
if (this.layoutValue.lines?.[line.id])
|
|
2742
2853
|
add('Reset line', 'reset-line', () => this.resetLine(line.id));
|
|
2743
2854
|
if (!this.disableConnections)
|
|
@@ -2746,11 +2857,16 @@ export class ProcessModeler extends BaseElement {
|
|
|
2746
2857
|
else if (this.selectedIds.size === 1) {
|
|
2747
2858
|
const box = this.selected;
|
|
2748
2859
|
const failurePath = box.kind === 'try' && !this.projection.lines.some(line => line.from === box.id && (line.dashed || line.label === 'If it fails'));
|
|
2749
|
-
add(box.shape === 'gateway' ? 'Add a path' : failurePath ? 'If it fails' : 'Add next', 'add-next', () => this.openChooser({ type: 'add', from: box.id, ...(failurePath ? { routeLabel: 'If it fails', dashed: true } : {}) }));
|
|
2750
|
-
add('Duplicate', 'duplicate', () => { const position = this.layoutValue.boxes[box.id]; this.requestEdit({ type: 'duplicate', sourceId: box.id, position: { x: position.x + 32, y: position.y + 32 } }); });
|
|
2751
|
-
add('Delete', 'delete-many', () => this.selectionCommand('delete-many'));
|
|
2752
2860
|
if (this.narrowValue)
|
|
2753
2861
|
add('Details', 'details', () => this.openDrawer('inspector'));
|
|
2862
|
+
if (!['end', 'finish', 'section', 'note'].includes(box.kind))
|
|
2863
|
+
add(box.shape === 'gateway' ? 'Add a path' : 'Add next', 'add-next', () => this.openAnchoredChooser(this.nextEdit(box), this.nextChooserTitle(box), toolbar.querySelector('[data-selection-command=add-next]') ?? undefined), true);
|
|
2864
|
+
if (failurePath)
|
|
2865
|
+
add('If it fails', 'add-failure', () => this.openAnchoredChooser({ type: 'add', from: box.id, routeLabel: 'If it fails', dashed: true }, `If a step in ${box.title} fails`, toolbar.querySelector('[data-selection-command=add-failure]') ?? undefined, Array.from(this.shadowRoot.querySelectorAll('[data-box-id]')).find(element => element.dataset.boxId === box.id)), true);
|
|
2866
|
+
if (!['start', 'timer', 'section'].includes(box.kind))
|
|
2867
|
+
add('Duplicate', 'duplicate', () => { const position = this.layoutValue.boxes[box.id]; this.requestEdit({ type: 'duplicate', sourceId: box.id, position: { x: position.x + 32, y: position.y + 32 } }); });
|
|
2868
|
+
if (!['start', 'timer'].includes(box.kind))
|
|
2869
|
+
add('Delete', 'delete-many', () => this.selectionCommand('delete-many'));
|
|
2754
2870
|
}
|
|
2755
2871
|
else {
|
|
2756
2872
|
add('Line up', 'align', () => this.selectionCommand('align'));
|
|
@@ -2780,7 +2896,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
2780
2896
|
checksStatus.dataset.state = count ? 'bad' : 'ready';
|
|
2781
2897
|
}
|
|
2782
2898
|
paintViewport() {
|
|
2783
|
-
this.shadowRoot.querySelector("[part=world]")
|
|
2899
|
+
const world = this.shadowRoot.querySelector("[part=world]");
|
|
2900
|
+
world.style.setProperty('--boe-process-inverse-zoom', String(1 / this.viewport.zoom));
|
|
2901
|
+
world.style.transform =
|
|
2784
2902
|
`translate(${this.viewport.x}px,${this.viewport.y}px) scale(${this.viewport.zoom})`;
|
|
2785
2903
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
2786
2904
|
const grid = 16 * this.viewport.zoom * (this.viewport.zoom < 0.5 ? 4 : 1);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RunResolution, RunStep, RunStepStatus } from "./types.js";
|
|
2
2
|
import { BaseElement } from "../../core/index.js";
|
|
3
|
+
export type RunTraceDensity = "default" | "compact";
|
|
3
4
|
/** Every status in words — colour never carries the meaning alone. */
|
|
4
5
|
export declare const RUN_STEP_STATUS_LABEL: Record<RunStepStatus, string>;
|
|
5
6
|
/**
|
|
@@ -28,6 +29,8 @@ export declare class RunTrace extends BaseElement {
|
|
|
28
29
|
private readonly previousStatuses;
|
|
29
30
|
get heading(): string;
|
|
30
31
|
set heading(value: string);
|
|
32
|
+
get density(): RunTraceDensity;
|
|
33
|
+
set density(value: RunTraceDensity);
|
|
31
34
|
get steps(): RunStep[];
|
|
32
35
|
set steps(value: RunStep[]);
|
|
33
36
|
/** The resolved run — same object the render uses. */
|
|
@@ -34,6 +34,29 @@ const STATUS_TONE = {
|
|
|
34
34
|
const elementStyles = `
|
|
35
35
|
:host {
|
|
36
36
|
display: block;
|
|
37
|
+
--_run-marker-column: var(--boe-run-trace-marker-column-width, 1.1rem);
|
|
38
|
+
--_run-marker-size: var(--boe-run-trace-marker-size, 1rem);
|
|
39
|
+
--_run-marker-inline: var(--boe-run-trace-marker-inline-inset, 0.15rem);
|
|
40
|
+
--_run-marker-block: var(--boe-run-trace-marker-block-offset, 0.2rem);
|
|
41
|
+
--_run-step-padding: var(--boe-run-trace-step-padding-block, 0.55rem);
|
|
42
|
+
--_run-step-row-gap: var(--boe-run-trace-step-row-gap, 0rem);
|
|
43
|
+
--_run-step-column-gap: var(--boe-run-trace-step-column-gap, 0.65rem);
|
|
44
|
+
--_run-child-row-gap: var(--boe-run-trace-child-row-gap, 0.4rem);
|
|
45
|
+
--_run-connector-start-gap: var(--boe-run-trace-connector-start-gap, 0.1rem);
|
|
46
|
+
--_run-connector-overhang: var(--boe-run-trace-connector-end-overhang, 0.35rem);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:host([density="compact"]) {
|
|
50
|
+
--_run-marker-column: var(--boe-run-trace-marker-column-width, 1rem);
|
|
51
|
+
--_run-marker-size: var(--boe-run-trace-marker-size, 0.875rem);
|
|
52
|
+
--_run-marker-inline: var(--boe-run-trace-marker-inline-inset, 0.1rem);
|
|
53
|
+
--_run-marker-block: var(--boe-run-trace-marker-block-offset, 0.15rem);
|
|
54
|
+
--_run-step-padding: var(--boe-run-trace-step-padding-block, 0.35rem);
|
|
55
|
+
--_run-step-row-gap: var(--boe-run-trace-step-row-gap, 0rem);
|
|
56
|
+
--_run-step-column-gap: var(--boe-run-trace-step-column-gap, 0.5rem);
|
|
57
|
+
--_run-child-row-gap: var(--boe-run-trace-child-row-gap, 0.25rem);
|
|
58
|
+
--_run-connector-start-gap: var(--boe-run-trace-connector-start-gap, 0.04rem);
|
|
59
|
+
--_run-connector-overhang: var(--boe-run-trace-connector-end-overhang, 0.25rem);
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
/* The host's own display would otherwise beat the UA rule for [hidden],
|
|
@@ -94,6 +117,8 @@ const elementStyles = `
|
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
[part="steps"] {
|
|
120
|
+
display: grid;
|
|
121
|
+
row-gap: var(--_run-step-row-gap);
|
|
97
122
|
margin: 0;
|
|
98
123
|
padding: 0;
|
|
99
124
|
list-style: none;
|
|
@@ -104,28 +129,33 @@ const elementStyles = `
|
|
|
104
129
|
[part="step"] {
|
|
105
130
|
position: relative;
|
|
106
131
|
display: grid;
|
|
107
|
-
grid-template-columns:
|
|
108
|
-
gap:
|
|
109
|
-
padding:
|
|
132
|
+
grid-template-columns: var(--_run-marker-column) minmax(0, 1fr);
|
|
133
|
+
column-gap: var(--_run-step-column-gap);
|
|
134
|
+
padding: var(--_run-step-padding) 0 var(--_run-step-padding) var(--_run-marker-inline);
|
|
110
135
|
}
|
|
111
136
|
|
|
112
137
|
[part="step"]:not(:last-child)::after {
|
|
113
138
|
content: "";
|
|
114
139
|
position: absolute;
|
|
115
|
-
left: calc(
|
|
116
|
-
top:
|
|
117
|
-
bottom: -
|
|
140
|
+
left: calc(var(--_run-marker-inline) + var(--_run-marker-column) / 2 - 1px);
|
|
141
|
+
top: calc(var(--_run-step-padding) + var(--_run-marker-block) + var(--_run-marker-column) / 2 + var(--_run-marker-size) / 2 + var(--_run-connector-start-gap));
|
|
142
|
+
bottom: calc(-1 * var(--_run-connector-overhang) - var(--_run-step-row-gap));
|
|
118
143
|
width: 2px;
|
|
119
144
|
background: color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 72%, transparent);
|
|
120
145
|
}
|
|
121
146
|
|
|
122
147
|
[part="marker"] {
|
|
123
|
-
inline-size:
|
|
124
|
-
block-size:
|
|
125
|
-
margin-top:
|
|
148
|
+
inline-size: var(--_run-marker-column);
|
|
149
|
+
block-size: var(--_run-marker-column);
|
|
150
|
+
margin-top: var(--_run-marker-block);
|
|
126
151
|
border-radius: 999px;
|
|
127
|
-
|
|
128
|
-
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
[part="marker"] .boe-status {
|
|
157
|
+
inline-size: var(--_run-marker-size);
|
|
158
|
+
block-size: var(--_run-marker-size);
|
|
129
159
|
}
|
|
130
160
|
|
|
131
161
|
[part="step"][data-status="running"] [part="marker"] {
|
|
@@ -218,7 +248,7 @@ const elementStyles = `
|
|
|
218
248
|
padding: 0;
|
|
219
249
|
list-style: none;
|
|
220
250
|
display: grid;
|
|
221
|
-
gap:
|
|
251
|
+
gap: var(--_run-child-row-gap);
|
|
222
252
|
}
|
|
223
253
|
|
|
224
254
|
[part="child"] {
|
|
@@ -254,7 +284,7 @@ const elementStyles = `
|
|
|
254
284
|
:host([variant="plain"]) [part="panel"] { border: 0; border-radius: 0; background: none; padding: 0; }
|
|
255
285
|
:host([variant="plain"]) [part="header"],
|
|
256
286
|
:host([variant="plain"]) [part="summary"] { display: none; }
|
|
257
|
-
:host([variant="plain"]) [part="step"] { padding-block: 0.35rem; }
|
|
287
|
+
:host([variant="plain"]) [part="step"] { padding-block: var(--boe-run-trace-step-padding-block, 0.35rem); }
|
|
258
288
|
:host([variant="plain"]) [part="step"]::after { display: none; }
|
|
259
289
|
:host([variant="plain"]) [part="status"] { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
260
290
|
:host([variant="plain"]) [part="toggle"] { display: none; }
|
|
@@ -304,6 +334,15 @@ export class RunTrace extends BaseElement {
|
|
|
304
334
|
set heading(value) {
|
|
305
335
|
this.setAttribute("heading", value);
|
|
306
336
|
}
|
|
337
|
+
get density() {
|
|
338
|
+
return this.getAttribute("density") === "compact" ? "compact" : "default";
|
|
339
|
+
}
|
|
340
|
+
set density(value) {
|
|
341
|
+
if (value === "compact")
|
|
342
|
+
this.setAttribute("density", "compact");
|
|
343
|
+
else
|
|
344
|
+
this.removeAttribute("density");
|
|
345
|
+
}
|
|
307
346
|
get steps() {
|
|
308
347
|
const raw = this.getAttribute("steps");
|
|
309
348
|
if (!raw) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export type TraceSpanStatus = "ok" | "failed" | "skipped" | "running";
|
|
3
|
+
export interface TraceMarker {
|
|
4
|
+
atMs: number;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TraceSpan {
|
|
8
|
+
id: string;
|
|
9
|
+
parentId?: string;
|
|
10
|
+
label: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
startMs: number;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
status: TraceSpanStatus;
|
|
15
|
+
system?: boolean;
|
|
16
|
+
markers?: TraceMarker[];
|
|
17
|
+
detail?: {
|
|
18
|
+
input?: unknown;
|
|
19
|
+
response?: unknown;
|
|
20
|
+
attributes?: unknown;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface TraceRow {
|
|
24
|
+
span: TraceSpan;
|
|
25
|
+
comparisonOnly: boolean;
|
|
26
|
+
parentId?: string;
|
|
27
|
+
depth: number;
|
|
28
|
+
hasChildren: boolean;
|
|
29
|
+
expanded: boolean;
|
|
30
|
+
position: number;
|
|
31
|
+
siblings: number;
|
|
32
|
+
}
|
|
33
|
+
export interface TraceLayout {
|
|
34
|
+
rows: TraceRow[];
|
|
35
|
+
hiddenCount: number;
|
|
36
|
+
endMs: number;
|
|
37
|
+
}
|
|
38
|
+
/** Invalid/duplicate spans are omitted; invalid parent relationships become roots. */
|
|
39
|
+
export declare function validTraceSpans(spans: TraceSpan[]): TraceSpan[];
|
|
40
|
+
export declare function traceSpanDuration(span: TraceSpan, nowMs: number): number;
|
|
41
|
+
/** Stable report layout: input sibling order, one zero-based axis, no browser dependency. */
|
|
42
|
+
export declare function traceWaterfallLayout(spans: TraceSpan[], options?: {
|
|
43
|
+
showSystem?: boolean;
|
|
44
|
+
search?: string;
|
|
45
|
+
collapsed?: ReadonlySet<string>;
|
|
46
|
+
nowMs?: number;
|
|
47
|
+
comparisonSpans?: TraceSpan[];
|
|
48
|
+
}): TraceLayout;
|
|
49
|
+
/** Host-driven live or completed trace; requests selection, never owns host sessions. */
|
|
50
|
+
export declare class TraceWaterfall extends BaseElement {
|
|
51
|
+
static readonly tagName = "box-trace-waterfall";
|
|
52
|
+
static get observedAttributes(): string[];
|
|
53
|
+
private _spans;
|
|
54
|
+
private _comparisonSpans;
|
|
55
|
+
private collapsed;
|
|
56
|
+
private search;
|
|
57
|
+
private table;
|
|
58
|
+
private code;
|
|
59
|
+
private fullScreen;
|
|
60
|
+
private focusedId;
|
|
61
|
+
get heading(): string;
|
|
62
|
+
set heading(value: string);
|
|
63
|
+
get spans(): TraceSpan[];
|
|
64
|
+
set spans(value: TraceSpan[]);
|
|
65
|
+
get comparisonSpans(): TraceSpan[];
|
|
66
|
+
set comparisonSpans(value: TraceSpan[]);
|
|
67
|
+
get showSystem(): boolean;
|
|
68
|
+
set showSystem(value: boolean);
|
|
69
|
+
get selectedSpanId(): string;
|
|
70
|
+
set selectedSpanId(value: string);
|
|
71
|
+
get nowMs(): number;
|
|
72
|
+
set nowMs(value: number);
|
|
73
|
+
get layout(): TraceLayout;
|
|
74
|
+
protected renderTemplate(): void;
|
|
75
|
+
private select;
|
|
76
|
+
private focusRow;
|
|
77
|
+
private toggle;
|
|
78
|
+
protected setupListeners(): void;
|
|
79
|
+
protected update(): void;
|
|
80
|
+
}
|