@webpieces/nx-webpieces-rules 0.4.883 → 0.4.885
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 +41 -8
- package/package.json +7 -7
- package/src/executors/visualize-runtime/executor.js +13 -1
- package/src/executors/visualize-runtime/executor.js.map +1 -1
- package/src/lib/dot-syntax.js +26 -0
- package/src/lib/dot-syntax.js.map +1 -1
- package/src/lib/graph-color-modes.d.ts +2 -1
- package/src/lib/graph-color-modes.js +5 -2
- package/src/lib/graph-color-modes.js.map +1 -1
- package/src/lib/graph-impact.js +1 -0
- package/src/lib/graph-impact.js.map +1 -1
- package/src/lib/graph-legend.d.ts +1 -1
- package/src/lib/graph-legend.js.map +1 -1
- package/src/lib/graph-mode-state.client.d.ts +32 -0
- package/src/lib/graph-mode-state.client.js +141 -0
- package/src/lib/graph-mode-state.client.js.map +1 -0
- package/src/lib/graph-page-shell.d.ts +5 -1
- package/src/lib/graph-page-shell.js +31 -12
- package/src/lib/graph-page-shell.js.map +1 -1
- package/src/lib/graph-render-model.d.ts +7 -5
- package/src/lib/graph-render-model.js +11 -8
- package/src/lib/graph-render-model.js.map +1 -1
- package/src/lib/graph-visualizer.client.d.ts +11 -46
- package/src/lib/graph-visualizer.client.js +103 -160
- package/src/lib/graph-visualizer.client.js.map +1 -1
- package/src/lib/graph-visualizer.js +2 -1
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-details.d.ts +4 -1
- package/src/lib/runtime-details.js +102 -55
- package/src/lib/runtime-details.js.map +1 -1
- package/src/lib/runtime-html-page.d.ts +13 -40
- package/src/lib/runtime-html-page.js +96 -129
- package/src/lib/runtime-html-page.js.map +1 -1
- package/src/lib/runtime-viewer-adapter.d.ts +16 -0
- package/src/lib/runtime-viewer-adapter.js +58 -0
- package/src/lib/runtime-viewer-adapter.js.map +1 -0
- package/src/lib/runtime-visualizer.client.d.ts +9 -19
- package/src/lib/runtime-visualizer.client.js +75 -75
- package/src/lib/runtime-visualizer.client.js.map +1 -1
- package/src/lib/runtime-visualizer.d.ts +2 -16
- package/src/lib/runtime-visualizer.js +22 -44
- package/src/lib/runtime-visualizer.js.map +1 -1
- package/src/lib/runtime-viz-options.d.ts +19 -0
- package/src/lib/runtime-viz-options.js +22 -0
- package/src/lib/runtime-viz-options.js.map +1 -0
|
@@ -1,150 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* The four color modes (graph-color-modes.ts), the viewer's remembered choice, and Impact's data.
|
|
4
|
-
*
|
|
5
|
-
* The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, `#product`, which win over the
|
|
6
|
-
* remembered one) and remembered per viewer in localStorage. Product is unavailable on a page whose
|
|
7
|
-
* workspace declares no `product:` tag. Storage can be missing or throw (a private window,
|
|
8
|
-
* blocked site data, a file:// page in some browsers), so every access is wrapped and the page works
|
|
9
|
-
* without it.
|
|
10
|
-
*
|
|
11
|
-
* Impact can carry two scans (#1163): "Changed on this branch" (kind `branch`) and "Last commit"
|
|
12
|
-
* (kind `commit`). The ACTIVE one starts as the sidecar's `defaultKind` (or the viewer's remembered
|
|
13
|
-
* choice, when that scan exists and is available) and is what every Impact color, note and Filter
|
|
14
|
-
* set reads.
|
|
15
|
-
*/
|
|
16
|
-
class WpModeState {
|
|
17
|
-
hasProducts;
|
|
18
|
-
static MODES = ['runtime', 'architecture', 'impact', 'product'];
|
|
19
|
-
static NAMES = ['Runtime', 'Architecture', 'Impact', 'Product'];
|
|
20
|
-
static SUBTITLES = [
|
|
21
|
-
'where the code can run',
|
|
22
|
-
'servers · clients · APIs',
|
|
23
|
-
'what changed',
|
|
24
|
-
'which products it belongs to',
|
|
25
|
-
];
|
|
26
|
-
static STORAGE_KEY = 'wp-architecture-graph-mode';
|
|
27
|
-
static KIND_STORAGE_KEY = 'wp-architecture-graph-impact-kind';
|
|
28
|
-
scans = window.__WP_IMPACT__?.scans ?? [];
|
|
29
|
-
kind = '';
|
|
30
|
-
mode = 'runtime';
|
|
31
|
-
/** @param hasProducts some box belongs to a product, so the Product mode has something to show */
|
|
32
|
-
constructor(hasProducts) {
|
|
33
|
-
this.hasProducts = hasProducts;
|
|
34
|
-
this.kind = this.initialKind();
|
|
35
|
-
this.mode = this.initial();
|
|
36
|
-
}
|
|
37
|
-
/** '' when Product can be shown, else why not. */
|
|
38
|
-
productReason() {
|
|
39
|
-
return this.hasProducts
|
|
40
|
-
? ''
|
|
41
|
-
: 'No products here: tag servers, clients and apps product:<name> in their project.json, then regenerate.';
|
|
42
|
-
}
|
|
43
|
-
/** The active scan; null when the page has no Impact data. */
|
|
44
|
-
get impact() {
|
|
45
|
-
return this.scans.find((scan) => scan.kind === this.kind) ?? null;
|
|
46
|
-
}
|
|
47
|
-
/** '' when Impact can be shown, else the one line the drawer and the Filter popover say instead. */
|
|
48
|
-
impactReason() {
|
|
49
|
-
const impact = this.impact;
|
|
50
|
-
if (impact === null)
|
|
51
|
-
return 'No impact data here. Run pnpm arch:visualize (nx run architecture:visualize) to compute it — no full regenerate needed.';
|
|
52
|
-
return impact.available ? '' : `Impact unavailable (${impact.label}): ${impact.reason}.`;
|
|
53
|
-
}
|
|
54
|
-
/** Switch the active scan; false (and nothing changes) for a missing or unavailable one. */
|
|
55
|
-
setKind(kind) {
|
|
56
|
-
if (!this.kindAvailable(kind))
|
|
57
|
-
return false;
|
|
58
|
-
this.kind = kind;
|
|
59
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store must not break the page
|
|
60
|
-
try {
|
|
61
|
-
localStorage.setItem(WpModeState.KIND_STORAGE_KEY, kind);
|
|
62
|
-
// webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
//const error = toError(err);
|
|
66
|
-
void err;
|
|
67
|
-
}
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
kindAvailable(kind) {
|
|
71
|
-
return this.scans.some((scan) => scan.kind === kind && scan.available);
|
|
72
|
-
}
|
|
73
|
-
initialKind() {
|
|
74
|
-
const stored = this.read(WpModeState.KIND_STORAGE_KEY);
|
|
75
|
-
if (stored !== null && this.kindAvailable(stored))
|
|
76
|
-
return stored;
|
|
77
|
-
const preferred = window.__WP_IMPACT__?.defaultKind ?? '';
|
|
78
|
-
if (this.scans.some((scan) => scan.kind === preferred))
|
|
79
|
-
return preferred;
|
|
80
|
-
return this.scans[0]?.kind ?? '';
|
|
81
|
-
}
|
|
82
|
-
available(mode) {
|
|
83
|
-
if (!WpModeState.MODES.includes(mode))
|
|
84
|
-
return false;
|
|
85
|
-
if (mode === 'impact')
|
|
86
|
-
return this.impactReason() === '';
|
|
87
|
-
if (mode === 'product')
|
|
88
|
-
return this.productReason() === '';
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
name(mode) {
|
|
92
|
-
return WpModeState.NAMES[WpModeState.MODES.indexOf(mode)] ?? mode;
|
|
93
|
-
}
|
|
94
|
-
subtitle(mode) {
|
|
95
|
-
return WpModeState.SUBTITLES[WpModeState.MODES.indexOf(mode)] ?? '';
|
|
96
|
-
}
|
|
97
|
-
/** False (and nothing changes) for an unknown or unavailable mode. */
|
|
98
|
-
set(mode) {
|
|
99
|
-
if (!this.available(mode))
|
|
100
|
-
return false;
|
|
101
|
-
this.mode = mode;
|
|
102
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store must not break the page
|
|
103
|
-
try {
|
|
104
|
-
localStorage.setItem(WpModeState.STORAGE_KEY, mode);
|
|
105
|
-
if (location.hash !== `#${mode}`)
|
|
106
|
-
history.replaceState(null, '', `#${mode}`);
|
|
107
|
-
// webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
//const error = toError(err);
|
|
111
|
-
void err;
|
|
112
|
-
}
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
/** Which impact shade a box gets, from the ACTIVE scan. */
|
|
116
|
-
status(id) {
|
|
117
|
-
const impact = this.impact;
|
|
118
|
-
if (impact?.touched.includes(id))
|
|
119
|
-
return 'touched';
|
|
120
|
-
if (impact?.affected.includes(id))
|
|
121
|
-
return 'affected';
|
|
122
|
-
if (impact?.buildInputs.includes(id))
|
|
123
|
-
return 'buildInput';
|
|
124
|
-
return 'untouched';
|
|
125
|
-
}
|
|
126
|
-
initial() {
|
|
127
|
-
const hashed = location.hash.slice(1);
|
|
128
|
-
if (this.available(hashed))
|
|
129
|
-
return hashed;
|
|
130
|
-
const stored = this.read(WpModeState.STORAGE_KEY);
|
|
131
|
-
if (stored !== null && this.available(stored))
|
|
132
|
-
return stored;
|
|
133
|
-
return 'runtime';
|
|
134
|
-
}
|
|
135
|
-
read(key) {
|
|
136
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store reads as "nothing stored"
|
|
137
|
-
try {
|
|
138
|
-
return localStorage.getItem(key);
|
|
139
|
-
// webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
//const error = toError(err);
|
|
143
|
-
void err;
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
2
|
/**
|
|
149
3
|
* One state of the Filter popover: a change scope plus the selected Runtime, Role and Product chips.
|
|
150
4
|
* The page holds the APPLIED one; the popover edits a copy until "Show N projects".
|
|
@@ -207,14 +61,14 @@ class WpProjectFilter {
|
|
|
207
61
|
/** How many drawn projects a scope keeps on its own: the count beside each option. */
|
|
208
62
|
scopeCount(scope) {
|
|
209
63
|
const set = this.scopeSet(scope);
|
|
210
|
-
return this.model.nodes.filter((node) => set === null || set.has(node.id)).length;
|
|
64
|
+
return this.model.nodes.filter((node) => this.isService(node) && (set === null || set.has(node.id))).length;
|
|
211
65
|
}
|
|
212
66
|
/** `base` (everything, or the Hide-unconnected chain) narrowed by `choice`. */
|
|
213
67
|
apply(base, choice) {
|
|
214
68
|
const scope = this.scopeSet(choice.scope);
|
|
215
69
|
const kept = new Set();
|
|
216
70
|
for (const node of this.model.nodes) {
|
|
217
|
-
if (!base.has(node.id))
|
|
71
|
+
if (!base.has(node.id) || !this.isService(node))
|
|
218
72
|
continue;
|
|
219
73
|
if (scope !== null && !scope.has(node.id))
|
|
220
74
|
continue;
|
|
@@ -226,8 +80,17 @@ class WpProjectFilter {
|
|
|
226
80
|
continue;
|
|
227
81
|
kept.add(node.id);
|
|
228
82
|
}
|
|
83
|
+
if (this.model.viewer === 'runtime') {
|
|
84
|
+
for (const node of this.model.nodes) {
|
|
85
|
+
if (node.tags !== null || !base.has(node.id))
|
|
86
|
+
continue;
|
|
87
|
+
if (this.model.edges.some(edge => (edge.from === node.id && kept.has(edge.to)) || (edge.to === node.id && kept.has(edge.from))))
|
|
88
|
+
kept.add(node.id);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
229
91
|
return kept;
|
|
230
92
|
}
|
|
93
|
+
isService(node) { return this.model.viewer !== 'runtime' || node.tags !== null; }
|
|
231
94
|
/** An empty chip group matches every box; otherwise the box must carry one of the chips. */
|
|
232
95
|
anyOf(selected, carried) {
|
|
233
96
|
return selected.size === 0 || carried.some((value) => selected.has(value));
|
|
@@ -307,14 +170,17 @@ class GraphHighlighter {
|
|
|
307
170
|
const designs = new Map();
|
|
308
171
|
for (const link of __DESIGN_LINKS__)
|
|
309
172
|
designs.set(link.nodeId, link.href);
|
|
310
|
-
WpNodeMenu.wire(this.svg, (name) => {
|
|
173
|
+
WpNodeMenu.wire(this.svg, (name, node) => {
|
|
311
174
|
const items = [];
|
|
312
175
|
const href = designs.get(name);
|
|
313
176
|
if (href !== undefined)
|
|
314
177
|
items.push(new WpNodeMenuItem('View Design', () => window.open(href, '_blank')));
|
|
315
178
|
const locked = this.page.lockSelection() === name;
|
|
316
|
-
items.push(new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () => this.page.
|
|
179
|
+
items.push(new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () => locked ? this.page.clearSelection() : this.page.setLock(name)));
|
|
317
180
|
items.push(this.page.filterItem(name));
|
|
181
|
+
const implemented = window.__WP_RUNTIME__?.implementsItem(name, node);
|
|
182
|
+
if (implemented)
|
|
183
|
+
items.push(implemented);
|
|
318
184
|
return items;
|
|
319
185
|
});
|
|
320
186
|
}
|
|
@@ -414,7 +280,7 @@ class GraphFilterPopover {
|
|
|
414
280
|
const apply = this.byId('wp-filter-apply');
|
|
415
281
|
if (apply !== null) {
|
|
416
282
|
const shown = this.page.preview(this.draft);
|
|
417
|
-
apply.textContent = `Show ${shown}
|
|
283
|
+
apply.textContent = `Show ${shown} ${this.page.noun()}${shown === 1 ? '' : 's'}`;
|
|
418
284
|
}
|
|
419
285
|
}
|
|
420
286
|
byId(id) {
|
|
@@ -428,6 +294,7 @@ class GraphFilterPopover {
|
|
|
428
294
|
class GraphDrawer {
|
|
429
295
|
page;
|
|
430
296
|
popover;
|
|
297
|
+
openers = new Map();
|
|
431
298
|
constructor(page) {
|
|
432
299
|
this.page = page;
|
|
433
300
|
this.popover = new GraphFilterPopover(page);
|
|
@@ -437,6 +304,7 @@ class GraphDrawer {
|
|
|
437
304
|
this.popover.wire();
|
|
438
305
|
this.wireImpactKinds();
|
|
439
306
|
this.wireLock();
|
|
307
|
+
this.byId('wp-lock-toggle')?.addEventListener('click', () => this.page.toggleLock());
|
|
440
308
|
this.byId('wp-filter-toggle')?.addEventListener('click', () => this.page.toggleFilter());
|
|
441
309
|
this.toggles('wp-collapse', 'wp-shell', true);
|
|
442
310
|
this.toggles('wp-help-btn', 'wp-help', false);
|
|
@@ -453,6 +321,7 @@ class GraphDrawer {
|
|
|
453
321
|
if (target instanceof Node && this.byId('wp-mode-menu')?.parentElement?.contains(target))
|
|
454
322
|
return;
|
|
455
323
|
this.closeModeMenu(false);
|
|
324
|
+
this.dismissOutside(target);
|
|
456
325
|
});
|
|
457
326
|
}
|
|
458
327
|
/** "Color by": the pill trigger opens the menu of four; picking one switches and closes it. */
|
|
@@ -523,7 +392,7 @@ class GraphDrawer {
|
|
|
523
392
|
input.addEventListener('input', () => {
|
|
524
393
|
const value = input.value.trim();
|
|
525
394
|
if (value === '')
|
|
526
|
-
this.page.
|
|
395
|
+
this.page.clearSelection();
|
|
527
396
|
else if (this.page.hasNode(value))
|
|
528
397
|
this.page.setLock(value);
|
|
529
398
|
});
|
|
@@ -539,7 +408,7 @@ class GraphDrawer {
|
|
|
539
408
|
return;
|
|
540
409
|
ev.preventDefault();
|
|
541
410
|
input.value = '';
|
|
542
|
-
this.page.
|
|
411
|
+
this.page.clearSelection();
|
|
543
412
|
input.blur();
|
|
544
413
|
});
|
|
545
414
|
}
|
|
@@ -559,11 +428,15 @@ class GraphDrawer {
|
|
|
559
428
|
this.popover.close();
|
|
560
429
|
return;
|
|
561
430
|
}
|
|
431
|
+
if (ev.key === 'Escape' && this.dismissOverlay())
|
|
432
|
+
return;
|
|
562
433
|
const target = ev.target;
|
|
563
434
|
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)
|
|
564
435
|
return;
|
|
565
|
-
const menuOpen = document.getElementById('wp-node-menu') !== null;
|
|
436
|
+
const menuOpen = document.getElementById('wp-node-menu') !== null || document.querySelector('.wp-graph-details:not([hidden])') !== null;
|
|
566
437
|
if (ev.key === '/') {
|
|
438
|
+
this.byId('wp-shell')?.classList.remove('wp-collapsed');
|
|
439
|
+
this.byId('wp-collapse')?.setAttribute('aria-expanded', 'true');
|
|
567
440
|
ev.preventDefault();
|
|
568
441
|
this.lockInput()?.focus();
|
|
569
442
|
return;
|
|
@@ -591,10 +464,42 @@ class GraphDrawer {
|
|
|
591
464
|
else {
|
|
592
465
|
target.hidden = !target.hidden;
|
|
593
466
|
expanded = !target.hidden;
|
|
467
|
+
if (expanded)
|
|
468
|
+
this.openers.set(targetId, button);
|
|
469
|
+
else
|
|
470
|
+
this.openers.get(targetId)?.focus({ preventScroll: true });
|
|
594
471
|
}
|
|
595
472
|
document.querySelectorAll(`[aria-controls="${targetId}"], #${buttonId}[aria-expanded]`).forEach((el) => el.setAttribute('aria-expanded', String(expanded)));
|
|
596
473
|
});
|
|
597
474
|
}
|
|
475
|
+
dismissOverlay() {
|
|
476
|
+
for (const id of ['wp-help', 'wp-legend-popout', 'wp-snapshot', 'wp-resp-panel']) {
|
|
477
|
+
const panel = this.byId(id);
|
|
478
|
+
if (!panel || panel.hidden)
|
|
479
|
+
continue;
|
|
480
|
+
panel.hidden = true;
|
|
481
|
+
this.openers.get(id)?.focus({ preventScroll: true });
|
|
482
|
+
this.openers.get(id)?.setAttribute('aria-expanded', 'false');
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
dismissOutside(target) {
|
|
488
|
+
if (!(target instanceof Node))
|
|
489
|
+
return;
|
|
490
|
+
const filter = this.byId('wp-filter-pop');
|
|
491
|
+
if (this.popover.isOpen() && !filter?.contains(target) && !this.byId('wp-filter-open')?.contains(target))
|
|
492
|
+
this.popover.close();
|
|
493
|
+
for (const entry of this.openers) {
|
|
494
|
+
const id = entry[0];
|
|
495
|
+
const opener = entry[1];
|
|
496
|
+
const panel = this.byId(id);
|
|
497
|
+
if (panel && !panel.hidden && !panel.contains(target) && !opener.contains(target)) {
|
|
498
|
+
panel.hidden = true;
|
|
499
|
+
opener.setAttribute('aria-expanded', 'false');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
598
503
|
/** Repaint every control from the page's state. */
|
|
599
504
|
sync(modes, locked, filtering, filter) {
|
|
600
505
|
const reason = modes.impactReason();
|
|
@@ -626,7 +531,13 @@ class GraphDrawer {
|
|
|
626
531
|
title.textContent = `Legend · ${modes.name(modes.mode)}`;
|
|
627
532
|
const input = this.lockInput();
|
|
628
533
|
if (input !== null && document.activeElement !== input)
|
|
629
|
-
input.value =
|
|
534
|
+
input.value = this.page.focusSelection() ?? '';
|
|
535
|
+
const lockToggle = this.byId('wp-lock-toggle');
|
|
536
|
+
if (lockToggle !== null) {
|
|
537
|
+
lockToggle.setAttribute('aria-pressed', String(locked !== null));
|
|
538
|
+
lockToggle.disabled = locked === null && this.page.focusSelection() === null;
|
|
539
|
+
lockToggle.title = lockToggle.disabled ? 'Select a node in Focus first' : '';
|
|
540
|
+
}
|
|
630
541
|
this.syncFilterToggle(locked, filtering);
|
|
631
542
|
this.syncFilter(filter);
|
|
632
543
|
this.crumb(modes.name(modes.mode), locked);
|
|
@@ -738,7 +649,8 @@ class GraphDrawer {
|
|
|
738
649
|
}
|
|
739
650
|
class GraphPage extends WpFilterPage {
|
|
740
651
|
highlighter = null;
|
|
741
|
-
|
|
652
|
+
selected = null;
|
|
653
|
+
modes = new WpModeState(this.model.nodes.some((node) => node.products.length > 0), this.model.viewer ?? 'architecture');
|
|
742
654
|
projects = new WpProjectFilter(this.model, this.modes);
|
|
743
655
|
applied = new WpFilterChoice();
|
|
744
656
|
drawer = new GraphDrawer(this);
|
|
@@ -776,6 +688,7 @@ class GraphPage extends WpFilterPage {
|
|
|
776
688
|
keepsEmptyLevels() {
|
|
777
689
|
return true;
|
|
778
690
|
}
|
|
691
|
+
prepareSvg(svg) { window.__WP_RUNTIME__?.prepare(svg); }
|
|
779
692
|
wireSvg(svg) {
|
|
780
693
|
this.highlighter = new GraphHighlighter(svg, this.chain, this.model, this);
|
|
781
694
|
this.highlighter.wire();
|
|
@@ -792,8 +705,14 @@ class GraphPage extends WpFilterPage {
|
|
|
792
705
|
hasNode(id) {
|
|
793
706
|
return this.model.nodes.some((node) => node.id === id);
|
|
794
707
|
}
|
|
795
|
-
|
|
796
|
-
|
|
708
|
+
noun() { return this.model.viewer === 'runtime' ? 'service' : 'project'; }
|
|
709
|
+
projectCount() { return this.model.nodes.filter(node => this.projects.isService(node)).length; }
|
|
710
|
+
focusSelection() { return this.selected; }
|
|
711
|
+
toggleLock() {
|
|
712
|
+
if (this.locked !== null)
|
|
713
|
+
this.setLock(null);
|
|
714
|
+
else if (this.selected !== null)
|
|
715
|
+
this.setLock(this.selected);
|
|
797
716
|
}
|
|
798
717
|
/** An exact id, else the one id whose short name (after any scope) is `text`; null if none or several. */
|
|
799
718
|
resolveName(text) {
|
|
@@ -806,10 +725,13 @@ class GraphPage extends WpFilterPage {
|
|
|
806
725
|
}
|
|
807
726
|
setLock(name) {
|
|
808
727
|
this.locked = name;
|
|
728
|
+
if (name !== null)
|
|
729
|
+
this.selected = name;
|
|
809
730
|
this.highlighter?.relight();
|
|
810
731
|
this.filterCards();
|
|
811
732
|
this.syncControls();
|
|
812
733
|
}
|
|
734
|
+
clearSelection() { this.selected = null; this.setLock(null); }
|
|
813
735
|
setMode(mode) {
|
|
814
736
|
if (mode === this.modes.mode || !this.modes.set(mode))
|
|
815
737
|
return;
|
|
@@ -838,14 +760,19 @@ class GraphPage extends WpFilterPage {
|
|
|
838
760
|
return this.modes.impactReason();
|
|
839
761
|
}
|
|
840
762
|
scopeCount(scope) {
|
|
841
|
-
|
|
763
|
+
if (this.model.viewer !== 'runtime')
|
|
764
|
+
return this.projects.scopeCount(scope);
|
|
765
|
+
const choice = new WpFilterChoice();
|
|
766
|
+
choice.scope = scope;
|
|
767
|
+
return this.preview(choice);
|
|
842
768
|
}
|
|
843
769
|
/** How many boxes `choice` would show, with the current Hide-unconnected chain. */
|
|
844
770
|
preview(choice) {
|
|
845
771
|
const base = this.anchor === null
|
|
846
772
|
? new Set(this.model.nodes.map((node) => node.id))
|
|
847
773
|
: this.chain.nodes(this.anchor);
|
|
848
|
-
|
|
774
|
+
const kept = this.projects.apply(base, choice);
|
|
775
|
+
return this.model.nodes.filter(node => kept.has(node.id) && this.projects.isService(node)).length;
|
|
849
776
|
}
|
|
850
777
|
applyFilter(choice) {
|
|
851
778
|
if (this.scopeReason() !== '')
|
|
@@ -865,6 +792,22 @@ class GraphPage extends WpFilterPage {
|
|
|
865
792
|
}
|
|
866
793
|
syncControls() {
|
|
867
794
|
this.drawer.sync(this.modes, this.locked, this.anchor !== null, this.applied);
|
|
795
|
+
const count = this.model.nodes.filter(node => this.retained.has(node.id) && this.projects.isService(node)).length;
|
|
796
|
+
const empty = document.getElementById('wp-empty');
|
|
797
|
+
if (empty !== null)
|
|
798
|
+
empty.hidden = count !== 0;
|
|
799
|
+
const context = document.getElementById('wp-context-count');
|
|
800
|
+
if (context !== null)
|
|
801
|
+
context.textContent = `${count} services · ${this.retained.size - count} contextual nodes`;
|
|
802
|
+
this.syncLegend();
|
|
803
|
+
}
|
|
804
|
+
syncLegend() {
|
|
805
|
+
document.querySelectorAll('[data-wp-legend-value]').forEach(row => {
|
|
806
|
+
const group = row.dataset['wpLegendGroup'];
|
|
807
|
+
const value = row.dataset['wpLegendValue'] ?? '';
|
|
808
|
+
row.hidden = !this.model.nodes.some(node => this.retained.has(node.id) &&
|
|
809
|
+
(group === 'runtime' ? (value === 'unknown' ? node.tags !== null && node.tags.frameworks.length === 0 : node.tags?.frameworks.includes(value)) : group === 'role' ? node.tags?.role === value : node.products.includes(value)));
|
|
810
|
+
});
|
|
868
811
|
}
|
|
869
812
|
filterCards() {
|
|
870
813
|
const lit = this.locked === null ? this.retained : this.chain.nodes(this.locked);
|