@unofficialbox/box-open-elements 0.28.7 → 0.29.1
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 +175 -43
- 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();
|
|
@@ -569,7 +612,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
569
612
|
this.shadowRoot.append(keyboardChooser);
|
|
570
613
|
keyboardPicker.addEventListener('kind-pick', event => {
|
|
571
614
|
const edit = this.keyboardInsertion;
|
|
572
|
-
this.closeKeyboardChooser();
|
|
615
|
+
this.closeKeyboardChooser(true);
|
|
616
|
+
// Keep a stable step focused while the host applies and accepts the edit.
|
|
617
|
+
// World refreshes preserve step focus, including deferred acceptance.
|
|
618
|
+
if (edit?.from)
|
|
619
|
+
this.focusBox(edit.from);
|
|
573
620
|
if (edit)
|
|
574
621
|
this.requestEdit({ ...edit, kind: event.detail.kind });
|
|
575
622
|
});
|
|
@@ -705,7 +752,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
705
752
|
const trigger = document.createElement("button");
|
|
706
753
|
trigger.dataset.command = part === "palette" ? "palette" : "details";
|
|
707
754
|
trigger.textContent = part === "palette" ? "Add" : "Details";
|
|
708
|
-
trigger.setAttribute("aria-label", `Open ${label.toLowerCase()}`);
|
|
755
|
+
trigger.setAttribute("aria-label", part === "palette" ? "Add building blocks" : `Open ${label.toLowerCase()}`);
|
|
709
756
|
trigger.setAttribute('aria-expanded', 'false');
|
|
710
757
|
trigger.onclick = () => this.openDrawer(part);
|
|
711
758
|
toolbar.append(trigger);
|
|
@@ -1355,7 +1402,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
1355
1402
|
if (event.ctrlKey && event.altKey && this.catalog.length && !this.disableConnections) {
|
|
1356
1403
|
const side = direction[0] < 0 ? "west" : direction[0] > 0 ? "east" : direction[1] < 0 ? "north" : "south";
|
|
1357
1404
|
const pos = this.layoutValue.boxes[box.id];
|
|
1358
|
-
|
|
1405
|
+
const anchor = Array.from(this.shadowRoot.querySelectorAll('[part=port]'))
|
|
1406
|
+
.find(element => element.dataset.owner === box.id && element.dataset.side === side);
|
|
1407
|
+
const returnFocus = Array.from(this.shadowRoot.querySelectorAll('[data-box-id]'))
|
|
1408
|
+
.find(element => element.dataset.boxId === box.id);
|
|
1409
|
+
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
1410
|
return;
|
|
1360
1411
|
}
|
|
1361
1412
|
if (event.altKey) {
|
|
@@ -1479,7 +1530,6 @@ export class ProcessModeler extends BaseElement {
|
|
|
1479
1530
|
if (!ghost) {
|
|
1480
1531
|
ghost = svgElement("polyline");
|
|
1481
1532
|
ghost.setAttribute("part", "connection-preview");
|
|
1482
|
-
ghost.style.cssText = "fill:none;stroke:var(--boe-token-surface-surface-brand,#0061d5);stroke-width:3";
|
|
1483
1533
|
this.shadowRoot.querySelector('[part=lines]').append(ghost);
|
|
1484
1534
|
}
|
|
1485
1535
|
const start = this.portDrag.start;
|
|
@@ -1488,7 +1538,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
1488
1538
|
const pinned = target ? this.sideAt(target, point) : undefined;
|
|
1489
1539
|
this.shadowRoot.querySelectorAll('[data-box-id]').forEach(box => { box.dataset.connectTarget = String(box.dataset.boxId === target?.id); });
|
|
1490
1540
|
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
|
|
1541
|
+
this.showConnectTooltip(point, target ? pinned ? `Pin to the ${pinned} of ${target.title}` : `Connect to ${target.title}` : 'Drop here to add a connected step');
|
|
1492
1542
|
return;
|
|
1493
1543
|
}
|
|
1494
1544
|
if (this.endDrag) {
|
|
@@ -1589,12 +1639,23 @@ export class ProcessModeler extends BaseElement {
|
|
|
1589
1639
|
if (Math.hypot(point.x - port.start.x, point.y - port.start.y) < 8) {
|
|
1590
1640
|
const pos = this.layoutValue.boxes[port.id];
|
|
1591
1641
|
const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[port.side];
|
|
1592
|
-
this.
|
|
1642
|
+
const source = this.projection.boxes.find(box => box.id === port.id);
|
|
1643
|
+
const anchor = Array.from(this.shadowRoot.querySelectorAll('[part=port]'))
|
|
1644
|
+
.find(element => element.dataset.owner === port.id && element.dataset.side === port.side);
|
|
1645
|
+
this.openAnchoredChooser(this.nextEdit(source, port.side, { x: pos.x + offsets[0], y: pos.y + offsets[1] }), this.nextChooserTitle(source, port.side), anchor);
|
|
1593
1646
|
}
|
|
1594
1647
|
else {
|
|
1595
1648
|
const target = this.boxAt(point, port.id);
|
|
1596
1649
|
if (target)
|
|
1597
1650
|
this.requestEdit({ type: "connect", from: port.id, to: target.id, fromSide: port.side, toSide: this.sideAt(target, point) });
|
|
1651
|
+
else {
|
|
1652
|
+
const source = this.projection.boxes.find(box => box.id === port.id);
|
|
1653
|
+
const frame = this.boxAt(point, undefined, true);
|
|
1654
|
+
if ((frame?.id ?? null) !== (source.parentId ?? null))
|
|
1655
|
+
this.setStatus('Drop inside the same frame to add a connected step');
|
|
1656
|
+
else
|
|
1657
|
+
this.openChooserAtPoint({ type: 'add', from: port.id, fromSide: port.side, parentId: source.parentId, position: point }, `Add after ${source.title}`, point);
|
|
1658
|
+
}
|
|
1598
1659
|
}
|
|
1599
1660
|
}
|
|
1600
1661
|
return;
|
|
@@ -1773,11 +1834,45 @@ export class ProcessModeler extends BaseElement {
|
|
|
1773
1834
|
promoteModal(chooser);
|
|
1774
1835
|
picker.focus();
|
|
1775
1836
|
}
|
|
1837
|
+
nextEdit(box, side, position) {
|
|
1838
|
+
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape;
|
|
1839
|
+
const outgoing = this.projection.lines.filter(line => line.from === box.id);
|
|
1840
|
+
if ((!side || side === 'east') && shape !== 'gateway' && outgoing.length === 1)
|
|
1841
|
+
return { type: 'insert', lineId: outgoing[0].id, from: box.id, to: outgoing[0].to };
|
|
1842
|
+
return { type: 'add', from: box.id, ...(side ? { fromSide: side } : {}), parentId: box.parentId, ...(position ? { position } : {}) };
|
|
1843
|
+
}
|
|
1844
|
+
nextChooserTitle(box, side) {
|
|
1845
|
+
const edit = this.nextEdit(box, side);
|
|
1846
|
+
if (edit.type === 'insert') {
|
|
1847
|
+
const target = this.projection.boxes.find(candidate => candidate.id === edit.to);
|
|
1848
|
+
return `Insert between ${box.title} and ${target?.title ?? 'the next step'}`;
|
|
1849
|
+
}
|
|
1850
|
+
if (box.shape === 'gateway')
|
|
1851
|
+
return `Add a path from ${box.title}`;
|
|
1852
|
+
return side && side !== 'east' ? `Add ${{ north: 'above', south: 'below', west: 'before' }[side]} ${box.title}, connected` : `Add after ${box.title}`;
|
|
1853
|
+
}
|
|
1854
|
+
openChooserAtPoint(edit, title, point, returnFocus) {
|
|
1855
|
+
if (this.keyboardInsertion)
|
|
1856
|
+
this.closeKeyboardChooser();
|
|
1857
|
+
const anchor = document.createElement('span');
|
|
1858
|
+
anchor.style.cssText = `position:absolute;left:${point.x}px;top:${point.y}px;width:1px;height:1px;pointer-events:none`;
|
|
1859
|
+
this.shadowRoot.querySelector('[part=world]').append(anchor);
|
|
1860
|
+
this.transientChooserAnchor = anchor;
|
|
1861
|
+
this.openAnchoredChooser(edit, title, anchor, returnFocus);
|
|
1862
|
+
}
|
|
1863
|
+
openLineChooser(line, returnFocus) {
|
|
1864
|
+
const source = this.projection.boxes.find(box => box.id === line.from);
|
|
1865
|
+
const target = this.projection.boxes.find(box => box.id === line.to);
|
|
1866
|
+
const points = this.routedLines.get(line.id) ?? routeProcessLine(line, this.layoutValue, this.projection);
|
|
1867
|
+
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);
|
|
1868
|
+
}
|
|
1776
1869
|
closeKeyboardChooser(returnFocus = false) {
|
|
1777
1870
|
const chooser = this.shadowRoot?.querySelector('[part=keyboard-chooser]');
|
|
1778
1871
|
dismissPopover(chooser ?? null);
|
|
1779
1872
|
this.stopKeyboardAnchor?.();
|
|
1780
1873
|
this.stopKeyboardAnchor = undefined;
|
|
1874
|
+
this.transientChooserAnchor?.remove();
|
|
1875
|
+
this.transientChooserAnchor = undefined;
|
|
1781
1876
|
document.removeEventListener('pointerdown', this.dismissKeyboardChooserOutside, true);
|
|
1782
1877
|
this.keyboardInsertion = undefined;
|
|
1783
1878
|
if (returnFocus)
|
|
@@ -1790,32 +1885,35 @@ export class ProcessModeler extends BaseElement {
|
|
|
1790
1885
|
this.closeKeyboardChooser();
|
|
1791
1886
|
};
|
|
1792
1887
|
openKeyboardChooser(box) {
|
|
1793
|
-
const chooser = this.shadowRoot.querySelector('[part=keyboard-chooser]');
|
|
1794
1888
|
const shape = box.shape ?? this.catalog.find(kind => kind.kind === box.kind)?.shape ?? (box.frame ? 'frame' : 'task');
|
|
1795
1889
|
const outgoing = this.projection.lines.filter(line => line.from === box.id);
|
|
1796
1890
|
const target = outgoing.length === 1 ? this.projection.boxes.find(candidate => candidate.id === outgoing[0].to) : undefined;
|
|
1797
1891
|
const insert = shape !== 'gateway' && outgoing.length === 1 && target;
|
|
1892
|
+
const anchor = Array.from(this.shadowRoot.querySelectorAll('[data-box-id]'))
|
|
1893
|
+
.find(element => element.dataset.boxId === box.id);
|
|
1894
|
+
this.openAnchoredChooser(insert
|
|
1895
|
+
? { type: 'insert', lineId: outgoing[0].id, from: box.id, to: target.id }
|
|
1896
|
+
: { type: 'add', from: box.id }, insert ? `Insert between ${box.title} and ${target.title}`
|
|
1897
|
+
: shape === 'gateway' ? `Add a path from ${box.title}` : `Add after ${box.title}`, anchor);
|
|
1898
|
+
}
|
|
1899
|
+
openAnchoredChooser(edit, title, anchor, returnFocus) {
|
|
1900
|
+
const chooser = this.shadowRoot.querySelector('[part=keyboard-chooser]');
|
|
1798
1901
|
if (this.keyboardInsertion)
|
|
1799
1902
|
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}`;
|
|
1903
|
+
this.keyboardInsertion = edit;
|
|
1904
|
+
chooser.querySelector('[part=chooser-title]').textContent = title;
|
|
1806
1905
|
const picker = chooser.querySelector('box-kind-picker');
|
|
1807
1906
|
picker.catalog = this.catalog.filter(kind => !kind.placement);
|
|
1808
1907
|
picker.refresh();
|
|
1809
|
-
this.keyboardReturn =
|
|
1810
|
-
|
|
1811
|
-
if (!this.keyboardReturn || !promotePopover(chooser)) {
|
|
1908
|
+
this.keyboardReturn = returnFocus ?? anchor;
|
|
1909
|
+
if (!anchor || !promotePopover(chooser)) {
|
|
1812
1910
|
const edit = this.keyboardInsertion;
|
|
1813
1911
|
this.closeKeyboardChooser();
|
|
1814
1912
|
if (edit)
|
|
1815
1913
|
this.openChooser(edit);
|
|
1816
1914
|
return;
|
|
1817
1915
|
}
|
|
1818
|
-
this.stopKeyboardAnchor = trackAnchor(
|
|
1916
|
+
this.stopKeyboardAnchor = trackAnchor(anchor, chooser, {
|
|
1819
1917
|
placement: { side: 'right', align: 'start' }, offset: 8, padding: 12,
|
|
1820
1918
|
});
|
|
1821
1919
|
document.addEventListener('pointerdown', this.dismissKeyboardChooserOutside, true);
|
|
@@ -1970,14 +2068,14 @@ export class ProcessModeler extends BaseElement {
|
|
|
1970
2068
|
element.append(loop);
|
|
1971
2069
|
}
|
|
1972
2070
|
const metrics = this.showLastRunValue ? this.lastRunValue?.steps[box.id] : undefined;
|
|
1973
|
-
if (
|
|
2071
|
+
if (this.showLastRunValue && this.lastRunValue && shape === 'task') {
|
|
1974
2072
|
const line = document.createElement("span");
|
|
1975
2073
|
line.setAttribute("part", "metrics");
|
|
1976
|
-
line.textContent = metrics.notInRun ? "Not in
|
|
1977
|
-
metrics.callsPerSecond === undefined ? "" : `${metrics.callsPerSecond.
|
|
1978
|
-
metrics.p95Ms === undefined ? "" : `p95 ${metrics.p95Ms
|
|
2074
|
+
line.textContent = !metrics || metrics.notInRun ? "Not in the last run" : [
|
|
2075
|
+
metrics.callsPerSecond === undefined ? "" : `${metrics.callsPerSecond.toFixed(1)}/s`,
|
|
2076
|
+
metrics.p95Ms === undefined ? "" : `p95 ${formatRunDuration(metrics.p95Ms)}`,
|
|
1979
2077
|
].filter(Boolean).join(" · ");
|
|
1980
|
-
if (metrics
|
|
2078
|
+
if (metrics?.failedShare !== undefined && metrics.failedShare > 0 && !metrics.notInRun) {
|
|
1981
2079
|
const failed = document.createElement('span');
|
|
1982
2080
|
failed.setAttribute('part', 'metric-warning');
|
|
1983
2081
|
failed.textContent = `${(metrics.failedShare * 100).toFixed(1)}% failed`;
|
|
@@ -1992,10 +2090,20 @@ export class ProcessModeler extends BaseElement {
|
|
|
1992
2090
|
element.append(message);
|
|
1993
2091
|
}
|
|
1994
2092
|
element.addEventListener("keydown", event => {
|
|
1995
|
-
if (event.target
|
|
2093
|
+
if (event.target !== element)
|
|
2094
|
+
return;
|
|
2095
|
+
if (event.key === " ") {
|
|
1996
2096
|
event.preventDefault();
|
|
1997
2097
|
element.click();
|
|
1998
2098
|
}
|
|
2099
|
+
// Let Enter reach the canvas handler after selecting the focused step.
|
|
2100
|
+
// Cancelling it here prevents the editor from opening and taking focus.
|
|
2101
|
+
else if (event.key === "Enter") {
|
|
2102
|
+
// Connection completion consumes Enter; it must not also edit the target.
|
|
2103
|
+
if (this.connecting || this.pendingReattach)
|
|
2104
|
+
event.preventDefault();
|
|
2105
|
+
element.click();
|
|
2106
|
+
}
|
|
1999
2107
|
});
|
|
2000
2108
|
element.addEventListener("click", event => {
|
|
2001
2109
|
if (this.suppressClick && event.detail !== 0) {
|
|
@@ -2021,7 +2129,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2021
2129
|
else
|
|
2022
2130
|
this.select(box.id);
|
|
2023
2131
|
});
|
|
2024
|
-
if (!this.disableConnections && !this.locked) {
|
|
2132
|
+
if (!this.disableConnections && !this.locked && !['end', 'finish'].includes(box.kind)) {
|
|
2025
2133
|
for (const side of ["north", "east", "south", "west"]) {
|
|
2026
2134
|
const port = document.createElement("button");
|
|
2027
2135
|
port.type = "button";
|
|
@@ -2034,7 +2142,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2034
2142
|
if (!event.detail) {
|
|
2035
2143
|
const pos = this.layoutValue.boxes[box.id];
|
|
2036
2144
|
const offsets = { east: [320, 0], west: [-320, 0], north: [0, -170], south: [0, 170] }[side];
|
|
2037
|
-
this.
|
|
2145
|
+
this.openAnchoredChooser(this.nextEdit(box, side, { x: pos.x + offsets[0], y: pos.y + offsets[1] }), this.nextChooserTitle(box, side), port);
|
|
2038
2146
|
}
|
|
2039
2147
|
});
|
|
2040
2148
|
element.append(port);
|
|
@@ -2096,7 +2204,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2096
2204
|
if (line.dashed)
|
|
2097
2205
|
path.setAttribute("stroke-dasharray", "6 4");
|
|
2098
2206
|
if (this.showLastRunValue && line.share !== undefined) {
|
|
2099
|
-
path.style.strokeWidth = `${1.5 + Math.max(0, Math.min(1, line.share)) * 2}px`;
|
|
2207
|
+
path.style.strokeWidth = `${Math.round((1.5 + Math.max(0, Math.min(1, line.share)) * 2.5) * 100) / 100}px`;
|
|
2100
2208
|
path.setAttribute('aria-label', `${Math.round(line.share * 100)}% of last-run traffic`);
|
|
2101
2209
|
}
|
|
2102
2210
|
lines.append(path);
|
|
@@ -2138,6 +2246,11 @@ export class ProcessModeler extends BaseElement {
|
|
|
2138
2246
|
const branchIndex = this.projection.lines.filter(candidate => candidate.from === line.from).indexOf(line);
|
|
2139
2247
|
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
2248
|
label.append(text);
|
|
2249
|
+
if (this.showLastRunValue && line.share !== undefined) {
|
|
2250
|
+
const share = document.createElement('small');
|
|
2251
|
+
share.textContent = `${Math.round(line.share * 100)}%`;
|
|
2252
|
+
label.append(share);
|
|
2253
|
+
}
|
|
2141
2254
|
const actions = document.createElement("div");
|
|
2142
2255
|
actions.setAttribute("part", "connection-actions");
|
|
2143
2256
|
label.append(actions);
|
|
@@ -2186,7 +2299,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2186
2299
|
to: line.to,
|
|
2187
2300
|
};
|
|
2188
2301
|
if (type === "insert" && this.catalog.length) {
|
|
2189
|
-
this.
|
|
2302
|
+
this.openLineChooser(line, button);
|
|
2190
2303
|
}
|
|
2191
2304
|
else
|
|
2192
2305
|
this.requestEdit(edit);
|
|
@@ -2675,8 +2788,13 @@ export class ProcessModeler extends BaseElement {
|
|
|
2675
2788
|
const report = document.createElement('section');
|
|
2676
2789
|
report.setAttribute('part', 'inspector-metrics');
|
|
2677
2790
|
const title = document.createElement('h3');
|
|
2678
|
-
title.textContent =
|
|
2791
|
+
title.textContent = 'Last run';
|
|
2679
2792
|
report.append(title);
|
|
2793
|
+
if (this.lastRunValue?.label) {
|
|
2794
|
+
const source = document.createElement('p');
|
|
2795
|
+
source.textContent = `From ${this.lastRunValue.label}.`;
|
|
2796
|
+
report.append(source);
|
|
2797
|
+
}
|
|
2680
2798
|
if (metrics.notInRun) {
|
|
2681
2799
|
const note = document.createElement('p');
|
|
2682
2800
|
note.textContent = 'Not in this run';
|
|
@@ -2684,9 +2802,7 @@ export class ProcessModeler extends BaseElement {
|
|
|
2684
2802
|
}
|
|
2685
2803
|
else {
|
|
2686
2804
|
const list = document.createElement('dl');
|
|
2687
|
-
for (const [label, value] of [['Per second', metrics.callsPerSecond
|
|
2688
|
-
if (value === undefined)
|
|
2689
|
-
continue;
|
|
2805
|
+
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
2806
|
const term = document.createElement('dt');
|
|
2691
2807
|
term.textContent = label;
|
|
2692
2808
|
const detail = document.createElement('dd');
|
|
@@ -2718,26 +2834,35 @@ export class ProcessModeler extends BaseElement {
|
|
|
2718
2834
|
}
|
|
2719
2835
|
renderSelectionToolbar() {
|
|
2720
2836
|
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(',')}`;
|
|
2837
|
+
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
2838
|
if (key === this.selectionToolbarKey)
|
|
2723
2839
|
return;
|
|
2724
2840
|
this.selectionToolbarKey = key;
|
|
2725
2841
|
toolbar.replaceChildren();
|
|
2726
2842
|
if (!this.selectedIds.size && !this.selectedLineId)
|
|
2727
2843
|
return;
|
|
2728
|
-
const add = (label, command, action) => {
|
|
2844
|
+
const add = (label, command, action, withPlus = false) => {
|
|
2729
2845
|
const button = document.createElement('button');
|
|
2730
2846
|
button.type = 'button';
|
|
2731
2847
|
button.dataset.selectionCommand = command;
|
|
2732
|
-
button.textContent = label;
|
|
2733
2848
|
button.setAttribute('aria-label', label);
|
|
2734
2849
|
button.disabled = this.locked;
|
|
2735
2850
|
button.onclick = action;
|
|
2851
|
+
if (withPlus) {
|
|
2852
|
+
const icon = document.createElement('span');
|
|
2853
|
+
icon.setAttribute('part', 'selection-plus');
|
|
2854
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
2855
|
+
icon.textContent = '+';
|
|
2856
|
+
button.append(icon);
|
|
2857
|
+
}
|
|
2858
|
+
const text = document.createElement('span');
|
|
2859
|
+
text.textContent = label;
|
|
2860
|
+
button.append(text);
|
|
2736
2861
|
toolbar.append(button);
|
|
2737
2862
|
};
|
|
2738
2863
|
if (this.selectedLine) {
|
|
2739
2864
|
const line = this.selectedLine;
|
|
2740
|
-
add('Insert a step', 'insert', () => this.
|
|
2865
|
+
add('Insert a step', 'insert', () => this.openLineChooser(line, toolbar.querySelector('[data-selection-command=insert]') ?? undefined), true);
|
|
2741
2866
|
if (this.layoutValue.lines?.[line.id])
|
|
2742
2867
|
add('Reset line', 'reset-line', () => this.resetLine(line.id));
|
|
2743
2868
|
if (!this.disableConnections)
|
|
@@ -2746,11 +2871,16 @@ export class ProcessModeler extends BaseElement {
|
|
|
2746
2871
|
else if (this.selectedIds.size === 1) {
|
|
2747
2872
|
const box = this.selected;
|
|
2748
2873
|
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
2874
|
if (this.narrowValue)
|
|
2753
2875
|
add('Details', 'details', () => this.openDrawer('inspector'));
|
|
2876
|
+
if (!['end', 'finish', 'section', 'note'].includes(box.kind))
|
|
2877
|
+
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);
|
|
2878
|
+
if (failurePath)
|
|
2879
|
+
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);
|
|
2880
|
+
if (!['start', 'timer', 'section'].includes(box.kind))
|
|
2881
|
+
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 } }); });
|
|
2882
|
+
if (!['start', 'timer'].includes(box.kind))
|
|
2883
|
+
add('Delete', 'delete-many', () => this.selectionCommand('delete-many'));
|
|
2754
2884
|
}
|
|
2755
2885
|
else {
|
|
2756
2886
|
add('Line up', 'align', () => this.selectionCommand('align'));
|
|
@@ -2780,7 +2910,9 @@ export class ProcessModeler extends BaseElement {
|
|
|
2780
2910
|
checksStatus.dataset.state = count ? 'bad' : 'ready';
|
|
2781
2911
|
}
|
|
2782
2912
|
paintViewport() {
|
|
2783
|
-
this.shadowRoot.querySelector("[part=world]")
|
|
2913
|
+
const world = this.shadowRoot.querySelector("[part=world]");
|
|
2914
|
+
world.style.setProperty('--boe-process-inverse-zoom', String(1 / this.viewport.zoom));
|
|
2915
|
+
world.style.transform =
|
|
2784
2916
|
`translate(${this.viewport.x}px,${this.viewport.y}px) scale(${this.viewport.zoom})`;
|
|
2785
2917
|
const canvas = this.shadowRoot.querySelector("[part=canvas]");
|
|
2786
2918
|
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) {
|