@webpieces/nx-webpieces-rules 0.4.852 → 0.4.854
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 +14 -0
- package/package.json +7 -7
- package/src/index.d.ts +1 -0
- package/src/index.js +5 -1
- package/src/index.js.map +1 -1
- package/src/lib/graph-filter.client.d.ts +40 -0
- package/src/lib/graph-filter.client.js +190 -0
- package/src/lib/graph-filter.client.js.map +1 -0
- package/src/lib/graph-level-bands.d.ts +0 -18
- package/src/lib/graph-level-bands.js +4 -3
- package/src/lib/graph-level-bands.js.map +1 -1
- package/src/lib/graph-node-menu.d.ts +1 -0
- package/src/lib/graph-node-menu.js +29 -0
- package/src/lib/graph-node-menu.js.map +1 -1
- package/src/lib/graph-render-model.d.ts +32 -0
- package/src/lib/graph-render-model.js +76 -0
- package/src/lib/graph-render-model.js.map +1 -0
- package/src/lib/graph-visualizer.client.d.ts +14 -73
- package/src/lib/graph-visualizer.client.js +71 -243
- package/src/lib/graph-visualizer.client.js.map +1 -1
- package/src/lib/graph-visualizer.d.ts +8 -4
- package/src/lib/graph-visualizer.js +38 -20
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-html-page.d.ts +6 -3
- package/src/lib/runtime-html-page.js +14 -6
- package/src/lib/runtime-html-page.js.map +1 -1
- package/src/lib/runtime-visualizer.client.d.ts +8 -19
- package/src/lib/runtime-visualizer.client.js +28 -41
- package/src/lib/runtime-visualizer.client.js.map +1 -1
- package/src/lib/runtime-visualizer.d.ts +6 -2
- package/src/lib/runtime-visualizer.js +55 -43
- package/src/lib/runtime-visualizer.js.map +1 -1
package/README.md
CHANGED
|
@@ -105,3 +105,17 @@ as a fallback. A failed generation remains a failure, and viewing any saved file
|
|
|
105
105
|
afterward does not establish that refresh succeeded. Browser-open failures print
|
|
106
106
|
the HTML path for manual opening. Architecture/API validation and CI still run
|
|
107
107
|
their existing checks independently of visualization.
|
|
108
|
+
|
|
109
|
+
### Compact graph filtering
|
|
110
|
+
|
|
111
|
+
Architecture and runtime node menus offer **Filter Unconnected** alongside
|
|
112
|
+
Lock/Unlock. Filtering retains the selected node and its separate transitive
|
|
113
|
+
incoming and outgoing chains, then reruns Graphviz. Architecture L-number rows keep
|
|
114
|
+
their original identities; runtime chains include rendered queues, clocks, external
|
|
115
|
+
APIs and systems. The anchor indicator and all surviving node menus offer **Turn off
|
|
116
|
+
Filter**, restoring the full layout and independent Lock state. Responsibilities
|
|
117
|
+
follow the retained set intersected with architecture Lock. Hover and menu dismissal
|
|
118
|
+
do not clear filtering, and no filter is persisted across reloads. Saved graph facts
|
|
119
|
+
and visibility options remain untouched. See
|
|
120
|
+
[dependency graphs](../../../docs/architecture/dependency-graphs.md#filter-unconnected)
|
|
121
|
+
for traversal examples and browser verification.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.854",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules, graph tooling, executors, and inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@webpieces/api-doc-model": "0.4.
|
|
24
|
-
"@webpieces/core-util": "0.4.
|
|
25
|
-
"@webpieces/eslint-rules": "0.4.
|
|
26
|
-
"@webpieces/rules-config": "0.4.
|
|
23
|
+
"@webpieces/api-doc-model": "0.4.854",
|
|
24
|
+
"@webpieces/core-util": "0.4.854",
|
|
25
|
+
"@webpieces/eslint-rules": "0.4.854",
|
|
26
|
+
"@webpieces/rules-config": "0.4.854",
|
|
27
27
|
"madge": "8.0.0",
|
|
28
|
-
"@webpieces/rules-sdk": "0.4.
|
|
28
|
+
"@webpieces/rules-sdk": "0.4.854"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@nx/devkit": ">=18.0.0"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@webpieces/tooling-testkit": "0.4.
|
|
44
|
+
"@webpieces/tooling-testkit": "0.4.854"
|
|
45
45
|
},
|
|
46
46
|
"types": "./src/index.d.ts",
|
|
47
47
|
"bin": {
|
package/src/index.d.ts
CHANGED
|
@@ -25,3 +25,4 @@ export { NoRootUnionApiTypeConfig } from './configs/no-root-union-config';
|
|
|
25
25
|
export { API_DOC_MODES, ApiRulesForOpenApiConfig, ApiRulesForMcpConfig } from './configs/api-doc-rules-config';
|
|
26
26
|
export type { ApiDocMode } from './configs/api-doc-rules-config';
|
|
27
27
|
export { ApiClientPackagesEntry, ApiLibDependenciesConfig, ApiLibPathConfig, FrameworkFolderEntry, FrameworkFolderConfig } from './configs/tag-truth-configs';
|
|
28
|
+
export { GraphRenderModel, RenderNode, RenderEdge } from './lib/graph-render-model';
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FrameworkFolderConfig = exports.FrameworkFolderEntry = exports.ApiLibPathConfig = exports.ApiLibDependenciesConfig = exports.ApiClientPackagesEntry = exports.ApiRulesForMcpConfig = exports.ApiRulesForOpenApiConfig = exports.API_DOC_MODES = exports.NoRootUnionApiTypeConfig = exports.ValidateEslintSyncConfig = exports.ValidateVersionsLockedConfig = exports.ValidatePackageJsonConfig = exports.ValidateNoArchitectureCyclesConfig = exports.ValidateArchitectureUnchangedConfig = exports.MissingDesignAnnotationConfig = exports.DiGraphConfig = exports.NxWiringConfig = exports.RuntimeArchitectureConfig = exports.NoFileImportCyclesConfig = exports.STRUCTURAL_MODES = exports.createNodesV2 = void 0;
|
|
3
|
+
exports.RenderEdge = exports.RenderNode = exports.GraphRenderModel = exports.FrameworkFolderConfig = exports.FrameworkFolderEntry = exports.ApiLibPathConfig = exports.ApiLibDependenciesConfig = exports.ApiClientPackagesEntry = exports.ApiRulesForMcpConfig = exports.ApiRulesForOpenApiConfig = exports.API_DOC_MODES = exports.NoRootUnionApiTypeConfig = exports.ValidateEslintSyncConfig = exports.ValidateVersionsLockedConfig = exports.ValidatePackageJsonConfig = exports.ValidateNoArchitectureCyclesConfig = exports.ValidateArchitectureUnchangedConfig = exports.MissingDesignAnnotationConfig = exports.DiGraphConfig = exports.NxWiringConfig = exports.RuntimeArchitectureConfig = exports.NoFileImportCyclesConfig = exports.STRUCTURAL_MODES = exports.createNodesV2 = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
tslib_1.__exportStar(require("./lib/graph-generator"), exports);
|
|
6
6
|
tslib_1.__exportStar(require("./lib/graph-sorter"), exports);
|
|
@@ -43,4 +43,8 @@ Object.defineProperty(exports, "ApiLibDependenciesConfig", { enumerable: true, g
|
|
|
43
43
|
Object.defineProperty(exports, "ApiLibPathConfig", { enumerable: true, get: function () { return tag_truth_configs_1.ApiLibPathConfig; } });
|
|
44
44
|
Object.defineProperty(exports, "FrameworkFolderEntry", { enumerable: true, get: function () { return tag_truth_configs_1.FrameworkFolderEntry; } });
|
|
45
45
|
Object.defineProperty(exports, "FrameworkFolderConfig", { enumerable: true, get: function () { return tag_truth_configs_1.FrameworkFolderConfig; } });
|
|
46
|
+
var graph_render_model_1 = require("./lib/graph-render-model");
|
|
47
|
+
Object.defineProperty(exports, "GraphRenderModel", { enumerable: true, get: function () { return graph_render_model_1.GraphRenderModel; } });
|
|
48
|
+
Object.defineProperty(exports, "RenderNode", { enumerable: true, get: function () { return graph_render_model_1.RenderNode; } });
|
|
49
|
+
Object.defineProperty(exports, "RenderEdge", { enumerable: true, get: function () { return graph_render_model_1.RenderEdge; } });
|
|
46
50
|
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/nx-webpieces-rules/src/index.ts"],"names":[],"mappings":";;;;AAAA,gEAAsC;AACtC,6DAAmC;AACnC,iEAAuC;AACvC,kEAAwC;AACxC,6DAAmC;AACnC,iEAAuC;AACvC,+DAAqC;AACrC,mEAAyC;AACzC,6EAAmD;AACnD,6DAAmC;AACnC,iEAAuC;AACvC,6DAAmC;AACnC,2EAAiD;AACjD,uEAA6C;AAC7C,qCAAyC;AAChC,8FADA,sBAAa,OACA;AACtB,kBAAe,EAAE,IAAI,EAAE,+BAA+B,EAAE,aAAa,EAAb,sBAAa,EAAE,CAAC;AAExE,uDAAyU;AAAhU,gHAAA,gBAAgB,OAAA;AAAE,wHAAA,wBAAwB,OAAA;AAAE,yHAAA,yBAAyB,OAAA;AAAE,8GAAA,cAAc,OAAA;AAAE,6GAAA,aAAa,OAAA;AAAE,6HAAA,6BAA6B,OAAA;AAAE,mIAAA,mCAAmC,OAAA;AAAE,kIAAA,kCAAkC,OAAA;AAAE,yHAAA,yBAAyB,OAAA;AAAE,4HAAA,4BAA4B,OAAA;AAAE,wHAAA,wBAAwB,OAAA;AAExS,uEAA0E;AAAjE,gIAAA,wBAAwB,OAAA;AACjC,uEAA+G;AAAtG,qHAAA,aAAa,OAAA;AAAE,gIAAA,wBAAwB,OAAA;AAAE,4HAAA,oBAAoB,OAAA;AAEtE,iEAA8J;AAArJ,2HAAA,sBAAsB,OAAA;AAAE,6HAAA,wBAAwB,OAAA;AAAE,qHAAA,gBAAgB,OAAA;AAAE,yHAAA,oBAAoB,OAAA;AAAE,0HAAA,qBAAqB,OAAA","sourcesContent":["export * from './lib/graph-generator';\nexport * from './lib/graph-sorter';\nexport * from './lib/graph-comparator';\nexport * from './lib/package-validator';\nexport * from './lib/graph-loader';\nexport * from './lib/graph-visualizer';\nexport * from './lib/graph-metadata';\nexport * from './lib/framework-resolver';\nexport * from './lib/runtime-participant-resolver';\nexport * from './lib/project-info';\nexport * from './lib/responsibilities';\nexport * from './lib/di-graph/dot';\nexport * from './lib/di-graph/design-visualizer';\nexport * from './lib/di-graph/design-finder';\nimport { createNodesV2 } from './plugin';\nexport { createNodesV2 };\nexport default { name: '@webpieces/nx-webpieces-rules', createNodesV2 };\n\nexport { STRUCTURAL_MODES, NoFileImportCyclesConfig, RuntimeArchitectureConfig, NxWiringConfig, DiGraphConfig, MissingDesignAnnotationConfig, ValidateArchitectureUnchangedConfig, ValidateNoArchitectureCyclesConfig, ValidatePackageJsonConfig, ValidateVersionsLockedConfig, ValidateEslintSyncConfig } from './configs/rule-configs';\nexport type { StructuralMode } from './configs/rule-configs';\nexport { NoRootUnionApiTypeConfig } from './configs/no-root-union-config';\nexport { API_DOC_MODES, ApiRulesForOpenApiConfig, ApiRulesForMcpConfig } from './configs/api-doc-rules-config';\nexport type { ApiDocMode } from './configs/api-doc-rules-config';\nexport { ApiClientPackagesEntry, ApiLibDependenciesConfig, ApiLibPathConfig, FrameworkFolderEntry, FrameworkFolderConfig } from './configs/tag-truth-configs';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/nx-webpieces-rules/src/index.ts"],"names":[],"mappings":";;;;AAAA,gEAAsC;AACtC,6DAAmC;AACnC,iEAAuC;AACvC,kEAAwC;AACxC,6DAAmC;AACnC,iEAAuC;AACvC,+DAAqC;AACrC,mEAAyC;AACzC,6EAAmD;AACnD,6DAAmC;AACnC,iEAAuC;AACvC,6DAAmC;AACnC,2EAAiD;AACjD,uEAA6C;AAC7C,qCAAyC;AAChC,8FADA,sBAAa,OACA;AACtB,kBAAe,EAAE,IAAI,EAAE,+BAA+B,EAAE,aAAa,EAAb,sBAAa,EAAE,CAAC;AAExE,uDAAyU;AAAhU,gHAAA,gBAAgB,OAAA;AAAE,wHAAA,wBAAwB,OAAA;AAAE,yHAAA,yBAAyB,OAAA;AAAE,8GAAA,cAAc,OAAA;AAAE,6GAAA,aAAa,OAAA;AAAE,6HAAA,6BAA6B,OAAA;AAAE,mIAAA,mCAAmC,OAAA;AAAE,kIAAA,kCAAkC,OAAA;AAAE,yHAAA,yBAAyB,OAAA;AAAE,4HAAA,4BAA4B,OAAA;AAAE,wHAAA,wBAAwB,OAAA;AAExS,uEAA0E;AAAjE,gIAAA,wBAAwB,OAAA;AACjC,uEAA+G;AAAtG,qHAAA,aAAa,OAAA;AAAE,gIAAA,wBAAwB,OAAA;AAAE,4HAAA,oBAAoB,OAAA;AAEtE,iEAA8J;AAArJ,2HAAA,sBAAsB,OAAA;AAAE,6HAAA,wBAAwB,OAAA;AAAE,qHAAA,gBAAgB,OAAA;AAAE,yHAAA,oBAAoB,OAAA;AAAE,0HAAA,qBAAqB,OAAA;AAExH,+DAAoF;AAA3E,sHAAA,gBAAgB,OAAA;AAAE,gHAAA,UAAU,OAAA;AAAE,gHAAA,UAAU,OAAA","sourcesContent":["export * from './lib/graph-generator';\nexport * from './lib/graph-sorter';\nexport * from './lib/graph-comparator';\nexport * from './lib/package-validator';\nexport * from './lib/graph-loader';\nexport * from './lib/graph-visualizer';\nexport * from './lib/graph-metadata';\nexport * from './lib/framework-resolver';\nexport * from './lib/runtime-participant-resolver';\nexport * from './lib/project-info';\nexport * from './lib/responsibilities';\nexport * from './lib/di-graph/dot';\nexport * from './lib/di-graph/design-visualizer';\nexport * from './lib/di-graph/design-finder';\nimport { createNodesV2 } from './plugin';\nexport { createNodesV2 };\nexport default { name: '@webpieces/nx-webpieces-rules', createNodesV2 };\n\nexport { STRUCTURAL_MODES, NoFileImportCyclesConfig, RuntimeArchitectureConfig, NxWiringConfig, DiGraphConfig, MissingDesignAnnotationConfig, ValidateArchitectureUnchangedConfig, ValidateNoArchitectureCyclesConfig, ValidatePackageJsonConfig, ValidateVersionsLockedConfig, ValidateEslintSyncConfig } from './configs/rule-configs';\nexport type { StructuralMode } from './configs/rule-configs';\nexport { NoRootUnionApiTypeConfig } from './configs/no-root-union-config';\nexport { API_DOC_MODES, ApiRulesForOpenApiConfig, ApiRulesForMcpConfig } from './configs/api-doc-rules-config';\nexport type { ApiDocMode } from './configs/api-doc-rules-config';\nexport { ApiClientPackagesEntry, ApiLibDependenciesConfig, ApiLibPathConfig, FrameworkFolderEntry, FrameworkFolderConfig } from './configs/tag-truth-configs';\n\nexport { GraphRenderModel, RenderNode, RenderEdge } from './lib/graph-render-model';\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */
|
|
2
|
+
declare class WpGraphChain {
|
|
3
|
+
private readonly model;
|
|
4
|
+
readonly inNodes: Map<string, Set<string>>;
|
|
5
|
+
readonly outNodes: Map<string, Set<string>>;
|
|
6
|
+
constructor(model: RenderModelJson);
|
|
7
|
+
private add;
|
|
8
|
+
nodes(anchor: string): Set<string>;
|
|
9
|
+
}
|
|
10
|
+
declare class WpRenderBand {
|
|
11
|
+
readonly level: number;
|
|
12
|
+
readonly nodeNames: string[];
|
|
13
|
+
constructor(level: number, nodeNames: string[]);
|
|
14
|
+
}
|
|
15
|
+
/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */
|
|
16
|
+
declare class WpFilteredDot {
|
|
17
|
+
private readonly model;
|
|
18
|
+
constructor(model: RenderModelJson);
|
|
19
|
+
render(retained: Set<string>): string;
|
|
20
|
+
private bandDot;
|
|
21
|
+
}
|
|
22
|
+
/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */
|
|
23
|
+
declare abstract class WpFilterPage {
|
|
24
|
+
protected readonly model: RenderModelJson;
|
|
25
|
+
protected readonly chain: WpGraphChain;
|
|
26
|
+
protected locked: string | null;
|
|
27
|
+
protected anchor: string | null;
|
|
28
|
+
protected retained: Set<string>;
|
|
29
|
+
private viz;
|
|
30
|
+
constructor(model: RenderModelJson);
|
|
31
|
+
render(): void;
|
|
32
|
+
protected captureBinding(_svg: SVGSVGElement | null): () => void;
|
|
33
|
+
protected wireControls(): void;
|
|
34
|
+
protected abstract wireSvg(svg: SVGSVGElement): void;
|
|
35
|
+
protected prepareSvg(_svg: SVGSVGElement): void;
|
|
36
|
+
filterItem(name: string): WpNodeMenuItem;
|
|
37
|
+
filter(anchor: string | null): void;
|
|
38
|
+
private indicator;
|
|
39
|
+
private error;
|
|
40
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */
|
|
3
|
+
class WpGraphChain {
|
|
4
|
+
model;
|
|
5
|
+
inNodes = new Map();
|
|
6
|
+
outNodes = new Map();
|
|
7
|
+
constructor(model) {
|
|
8
|
+
this.model = model;
|
|
9
|
+
const ids = new Set(model.nodes.map((node) => node.id));
|
|
10
|
+
for (const edge of model.edges) {
|
|
11
|
+
if (!ids.has(edge.from) || !ids.has(edge.to))
|
|
12
|
+
continue;
|
|
13
|
+
this.add(this.outNodes, edge.from, edge.to);
|
|
14
|
+
this.add(this.inNodes, edge.to, edge.from);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
add(map, from, to) {
|
|
18
|
+
if (!map.has(from))
|
|
19
|
+
map.set(from, new Set());
|
|
20
|
+
map.get(from)?.add(to);
|
|
21
|
+
}
|
|
22
|
+
nodes(anchor) {
|
|
23
|
+
const retained = new Set();
|
|
24
|
+
if (!this.model.nodes.some((node) => node.id === anchor))
|
|
25
|
+
return retained;
|
|
26
|
+
for (const direction of [this.inNodes, this.outNodes]) {
|
|
27
|
+
const visited = new Set([anchor]);
|
|
28
|
+
const stack = [anchor];
|
|
29
|
+
while (stack.length > 0) {
|
|
30
|
+
const id = stack.pop();
|
|
31
|
+
retained.add(id);
|
|
32
|
+
for (const next of direction.get(id) ?? []) {
|
|
33
|
+
if (visited.has(next))
|
|
34
|
+
continue;
|
|
35
|
+
visited.add(next);
|
|
36
|
+
stack.push(next);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return retained;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
class WpRenderBand {
|
|
44
|
+
level;
|
|
45
|
+
nodeNames;
|
|
46
|
+
constructor(level, nodeNames) {
|
|
47
|
+
this.level = level;
|
|
48
|
+
this.nodeNames = nodeNames;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */
|
|
52
|
+
class WpFilteredDot {
|
|
53
|
+
model;
|
|
54
|
+
constructor(model) {
|
|
55
|
+
this.model = model;
|
|
56
|
+
}
|
|
57
|
+
render(retained) {
|
|
58
|
+
const nodes = this.model.nodes.filter((node) => retained.has(node.id));
|
|
59
|
+
const edges = this.model.edges.filter((edge) => retained.has(edge.from) && retained.has(edge.to));
|
|
60
|
+
const bands = this.model.bands
|
|
61
|
+
.map((band) => new WpRenderBand(band.level, band.nodeNames.filter((id) => retained.has(id))))
|
|
62
|
+
.filter((band) => band.nodeNames.length > 0);
|
|
63
|
+
return (this.model.header +
|
|
64
|
+
nodes.map((node) => node.dot).join('') +
|
|
65
|
+
this.bandDot(bands) +
|
|
66
|
+
edges.map((edge) => edge.dot).join('') +
|
|
67
|
+
this.model.footer);
|
|
68
|
+
}
|
|
69
|
+
bandDot(bands) {
|
|
70
|
+
const nodeAttrs = '[style=invis, shape=point, width=0.01, height=0.01, label="", class="wp-layout"]';
|
|
71
|
+
const edgeAttrs = '[style=invis, class="wp-layout"]';
|
|
72
|
+
let dot = '';
|
|
73
|
+
for (const band of bands) {
|
|
74
|
+
const anchor = `__wp_layout_L${band.level}`;
|
|
75
|
+
dot += ` "${anchor}" ${nodeAttrs};\n { rank=same; "${anchor}"; `;
|
|
76
|
+
dot += band.nodeNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\n';
|
|
77
|
+
}
|
|
78
|
+
for (let i = 0; i + 1 < bands.length; i++) {
|
|
79
|
+
const upper = bands[i];
|
|
80
|
+
const lower = bands[i + 1];
|
|
81
|
+
const from = `__wp_layout_L${upper.level}`;
|
|
82
|
+
const to = `__wp_layout_L${lower.level}`;
|
|
83
|
+
if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {
|
|
84
|
+
dot += ` "${from}" -> "${to}" ${edgeAttrs};\n`;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;
|
|
88
|
+
dot += ` "${spacer}" ${nodeAttrs};\n { rank=same; "${spacer}"; }\n`;
|
|
89
|
+
dot += ` "${from}" -> "${spacer}" ${edgeAttrs};\n "${spacer}" -> "${to}" ${edgeAttrs};\n`;
|
|
90
|
+
}
|
|
91
|
+
return dot;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */
|
|
95
|
+
class WpFilterPage {
|
|
96
|
+
model;
|
|
97
|
+
chain;
|
|
98
|
+
locked = null;
|
|
99
|
+
anchor = null;
|
|
100
|
+
retained;
|
|
101
|
+
viz = null;
|
|
102
|
+
constructor(model) {
|
|
103
|
+
this.model = model;
|
|
104
|
+
this.chain = new WpGraphChain(model);
|
|
105
|
+
this.retained = new Set(model.nodes.map((node) => node.id));
|
|
106
|
+
}
|
|
107
|
+
render() {
|
|
108
|
+
document
|
|
109
|
+
.getElementById('wp-filter-off')
|
|
110
|
+
?.addEventListener('click', () => this.filter(null));
|
|
111
|
+
this.wireControls();
|
|
112
|
+
Viz.instance()
|
|
113
|
+
.then((viz) => {
|
|
114
|
+
this.viz = viz;
|
|
115
|
+
this.filter(null);
|
|
116
|
+
})
|
|
117
|
+
// webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value
|
|
118
|
+
.catch((err) => this.error(err instanceof Error ? err : new Error(String(err))));
|
|
119
|
+
}
|
|
120
|
+
captureBinding(_svg) { return () => { }; }
|
|
121
|
+
wireControls() { }
|
|
122
|
+
prepareSvg(_svg) { }
|
|
123
|
+
filterItem(name) {
|
|
124
|
+
return new WpNodeMenuItem(this.anchor === null ? 'Filter Unconnected' : 'Turn off Filter', () => this.filter(this.anchor === null ? name : null));
|
|
125
|
+
}
|
|
126
|
+
filter(anchor) {
|
|
127
|
+
if (this.viz === null)
|
|
128
|
+
return;
|
|
129
|
+
const host = document.getElementById('graph');
|
|
130
|
+
if (host === null)
|
|
131
|
+
return;
|
|
132
|
+
const retained = anchor === null
|
|
133
|
+
? new Set(this.model.nodes.map((node) => node.id))
|
|
134
|
+
: this.chain.nodes(anchor);
|
|
135
|
+
const previousAnchor = this.anchor;
|
|
136
|
+
const previousRetained = this.retained;
|
|
137
|
+
const previousSvg = host.querySelector('svg');
|
|
138
|
+
const rollback = this.captureBinding(previousSvg);
|
|
139
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure
|
|
140
|
+
try {
|
|
141
|
+
const dot = anchor === null
|
|
142
|
+
? this.model.fullDot
|
|
143
|
+
: new WpFilteredDot(this.model).render(retained);
|
|
144
|
+
const svg = this.viz.renderSVGElement(dot);
|
|
145
|
+
this.prepareSvg(svg);
|
|
146
|
+
WpNodeMenu.close();
|
|
147
|
+
this.anchor = anchor;
|
|
148
|
+
this.retained = retained;
|
|
149
|
+
host.replaceChildren(svg);
|
|
150
|
+
this.wireSvg(svg);
|
|
151
|
+
this.indicator();
|
|
152
|
+
this.error(null);
|
|
153
|
+
// webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
//const error = toError(err);
|
|
157
|
+
this.anchor = previousAnchor;
|
|
158
|
+
this.retained = previousRetained;
|
|
159
|
+
if (previousSvg !== null)
|
|
160
|
+
host.replaceChildren(previousSvg);
|
|
161
|
+
else
|
|
162
|
+
host.replaceChildren();
|
|
163
|
+
rollback();
|
|
164
|
+
this.error(err instanceof Error ? err : new Error(String(err)));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
indicator() {
|
|
168
|
+
const status = document.getElementById('wp-filter-status');
|
|
169
|
+
const label = document.getElementById('wp-filter-anchor');
|
|
170
|
+
if (status !== null)
|
|
171
|
+
status.hidden = this.anchor === null;
|
|
172
|
+
if (label !== null)
|
|
173
|
+
label.textContent = this.anchor;
|
|
174
|
+
if (this.anchor !== null)
|
|
175
|
+
document.getElementById('wp-filter-off')?.focus();
|
|
176
|
+
else
|
|
177
|
+
document.querySelector('#graph g.wp-node-clickable')?.focus();
|
|
178
|
+
}
|
|
179
|
+
error(err) {
|
|
180
|
+
const el = document.getElementById('wp-graph-error');
|
|
181
|
+
if (el === null)
|
|
182
|
+
return;
|
|
183
|
+
el.hidden = err === null;
|
|
184
|
+
el.textContent =
|
|
185
|
+
err === null
|
|
186
|
+
? ''
|
|
187
|
+
: `Graph rendering failed: ${err.message}. Try Turn off Filter or reload the page.`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=graph-filter.client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-filter.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-filter.client.ts"],"names":[],"mappings":";AAAA,qFAAqF;AACrF,MAAM,YAAY;IAIe;IAHpB,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IACzC,QAAQ,GAAG,IAAI,GAAG,EAAuB,CAAC;IAEnD,YAA6B,KAAsB;QAAtB,UAAK,GAAL,KAAK,CAAiB;QAC/C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QACxD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAAE,SAAS;YACvD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;YAC5C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,CAAC;IACL,CAAC;IAEO,GAAG,CAAC,GAA6B,EAAE,IAAY,EAAE,EAAU;QAC/D,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QAC7C,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,MAAc;QAChB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,MAAM,CAAC;YAAE,OAAO,QAAQ,CAAC;QAC1E,KAAK,MAAM,SAAS,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YAClC,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,CAAC;YACvB,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,EAAY,CAAC;gBACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACjB,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;oBACzC,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;wBAAE,SAAS;oBAChC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBAClB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACrB,CAAC;YACL,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IACpB,CAAC;CACJ;AAED,MAAM,YAAY;IAED;IACA;IAFb,YACa,KAAa,EACb,SAAmB;QADnB,UAAK,GAAL,KAAK,CAAQ;QACb,cAAS,GAAT,SAAS,CAAU;IAC7B,CAAC;CACP;AAED,wFAAwF;AACxF,MAAM,aAAa;IACc;IAA7B,YAA6B,KAAsB;QAAtB,UAAK,GAAL,KAAK,CAAiB;IAAG,CAAC;IAEvD,MAAM,CAAC,QAAqB;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QACvE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CACjC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAC7D,CAAC;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK;aACzB,GAAG,CACA,CAAC,IAAI,EAAE,EAAE,CACL,IAAI,YAAY,CACZ,IAAI,CAAC,KAAK,EACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAClD,CACR;aACA,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACjD,OAAO,CACH,IAAI,CAAC,KAAK,CAAC,MAAM;YACjB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;YACnB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,IAAI,CAAC,KAAK,CAAC,MAAM,CACpB,CAAC;IACN,CAAC;IAEO,OAAO,CAAC,KAAqB;QACjC,MAAM,SAAS,GACX,kFAAkF,CAAC;QACvF,MAAM,SAAS,GAAG,kCAAkC,CAAC;QACrD,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,gBAAgB,IAAI,CAAC,KAAK,EAAE,CAAC;YAC5C,GAAG,IAAI,MAAM,MAAM,KAAK,SAAS,sBAAsB,MAAM,KAAK,CAAC;YACnE,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC;QAClF,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACvB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC3B,MAAM,IAAI,GAAG,gBAAgB,KAAK,CAAC,KAAK,EAAE,CAAC;YAC3C,MAAM,EAAE,GAAG,gBAAgB,KAAK,CAAC,KAAK,EAAE,CAAC;YACzC,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,IAAI,EAAE,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;gBAC/D,GAAG,IAAI,MAAM,IAAI,SAAS,EAAE,KAAK,SAAS,KAAK,CAAC;gBAChD,SAAS;YACb,CAAC;YACD,MAAM,MAAM,GAAG,uBAAuB,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;YACpE,GAAG,IAAI,MAAM,MAAM,KAAK,SAAS,sBAAsB,MAAM,QAAQ,CAAC;YACtE,GAAG,IAAI,MAAM,IAAI,SAAS,MAAM,KAAK,SAAS,SAAS,MAAM,SAAS,EAAE,KAAK,SAAS,KAAK,CAAC;QAChG,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;CACJ;AAED,8FAA8F;AAC9F,MAAe,YAAY;IAOQ;IANZ,KAAK,CAAe;IAC7B,MAAM,GAAkB,IAAI,CAAC;IAC7B,MAAM,GAAkB,IAAI,CAAC;IAC7B,QAAQ,CAAc;IACxB,GAAG,GAAuB,IAAI,CAAC;IAEvC,YAA+B,KAAsB;QAAtB,UAAK,GAAL,KAAK,CAAiB;QACjD,IAAI,CAAC,KAAK,GAAG,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IAChE,CAAC;IAED,MAAM;QACF,QAAQ;aACH,cAAc,CAAC,eAAe,CAAC;YAChC,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QACzD,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,GAAG,CAAC,QAAQ,EAAE;aACT,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;YACV,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC,CAAC;YACF,wFAAwF;aACvF,KAAK,CAAC,CAAC,GAAY,EAAQ,EAAE,CAC1B,IAAI,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAClE,CAAC;IACV,CAAC;IAES,cAAc,CAAC,IAA0B,IAAgB,OAAO,GAAS,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC;IACjF,YAAY,KAAU,CAAC;IAEvB,UAAU,CAAC,IAAmB,IAAS,CAAC;IAElD,UAAU,CAAC,IAAY;QACnB,OAAO,IAAI,cAAc,CACrB,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,iBAAiB,EAC/D,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CACxD,CAAC;IACN,CAAC;IAED,MAAM,CAAC,MAAqB;QACxB,IAAI,IAAI,CAAC,GAAG,KAAK,IAAI;YAAE,OAAO;QAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QAC9C,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO;QAC1B,MAAM,QAAQ,GACV,MAAM,KAAK,IAAI;YACX,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAClD,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACnC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;QACnC,MAAM,gBAAgB,GAAG,IAAI,CAAC,QAAQ,CAAC;QACvC,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QAClD,mIAAmI;QACnI,IAAI,CAAC;YACD,MAAM,GAAG,GACL,MAAM,KAAK,IAAI;gBACX,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO;gBACpB,CAAC,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACzD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;YAC3C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YACrB,UAAU,CAAC,KAAK,EAAE,CAAC;YACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;YACzB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAClB,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACjB,8FAA8F;QAClG,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,6BAA6B;YAC7B,IAAI,CAAC,MAAM,GAAG,cAAc,CAAC;YAC7B,IAAI,CAAC,QAAQ,GAAG,gBAAgB,CAAC;YACjC,IAAI,WAAW,KAAK,IAAI;gBAAE,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;;gBACvD,IAAI,CAAC,eAAe,EAAE,CAAC;YAC5B,QAAQ,EAAE,CAAC;YACX,IAAI,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACpE,CAAC;IACL,CAAC;IAEO,SAAS;QACb,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;QAC1D,IAAI,MAAM,KAAK,IAAI;YAAE,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC;QAC1D,IAAI,KAAK,KAAK,IAAI;YAAE,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC;QACpD,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;YAAE,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,CAAC;;YACvE,QAAQ,CAAC,aAAa,CAAc,4BAA4B,CAAC,EAAE,KAAK,EAAE,CAAC;IACpF,CAAC;IAEO,KAAK,CAAC,GAAiB;QAC3B,MAAM,EAAE,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;QACrD,IAAI,EAAE,KAAK,IAAI;YAAE,OAAO;QACxB,EAAE,CAAC,MAAM,GAAG,GAAG,KAAK,IAAI,CAAC;QACzB,EAAE,CAAC,WAAW;YACV,GAAG,KAAK,IAAI;gBACR,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,2BAA2B,GAAG,CAAC,OAAO,2CAA2C,CAAC;IAChG,CAAC;CACJ","sourcesContent":["/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */\nclass WpGraphChain {\n readonly inNodes = new Map<string, Set<string>>();\n readonly outNodes = new Map<string, Set<string>>();\n\n constructor(private readonly model: RenderModelJson) {\n const ids = new Set(model.nodes.map((node) => node.id));\n for (const edge of model.edges) {\n if (!ids.has(edge.from) || !ids.has(edge.to)) continue;\n this.add(this.outNodes, edge.from, edge.to);\n this.add(this.inNodes, edge.to, edge.from);\n }\n }\n\n private add(map: Map<string, Set<string>>, from: string, to: string): void {\n if (!map.has(from)) map.set(from, new Set());\n map.get(from)?.add(to);\n }\n\n nodes(anchor: string): Set<string> {\n const retained = new Set<string>();\n if (!this.model.nodes.some((node) => node.id === anchor)) return retained;\n for (const direction of [this.inNodes, this.outNodes]) {\n const visited = new Set([anchor]);\n const stack = [anchor];\n while (stack.length > 0) {\n const id = stack.pop() as string;\n retained.add(id);\n for (const next of direction.get(id) ?? []) {\n if (visited.has(next)) continue;\n visited.add(next);\n stack.push(next);\n }\n }\n }\n return retained;\n }\n}\n\nclass WpRenderBand {\n constructor(\n readonly level: number,\n readonly nodeNames: string[],\n ) {}\n}\n\n/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */\nclass WpFilteredDot {\n constructor(private readonly model: RenderModelJson) {}\n\n render(retained: Set<string>): string {\n const nodes = this.model.nodes.filter((node) => retained.has(node.id));\n const edges = this.model.edges.filter(\n (edge) => retained.has(edge.from) && retained.has(edge.to),\n );\n const bands = this.model.bands\n .map(\n (band) =>\n new WpRenderBand(\n band.level,\n band.nodeNames.filter((id) => retained.has(id)),\n ),\n )\n .filter((band) => band.nodeNames.length > 0);\n return (\n this.model.header +\n nodes.map((node) => node.dot).join('') +\n this.bandDot(bands) +\n edges.map((edge) => edge.dot).join('') +\n this.model.footer\n );\n }\n\n private bandDot(bands: WpRenderBand[]): string {\n const nodeAttrs =\n '[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"wp-layout\"]';\n const edgeAttrs = '[style=invis, class=\"wp-layout\"]';\n let dot = '';\n for (const band of bands) {\n const anchor = `__wp_layout_L${band.level}`;\n dot += ` \"${anchor}\" ${nodeAttrs};\\n { rank=same; \"${anchor}\"; `;\n dot += band.nodeNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n const upper = bands[i];\n const lower = bands[i + 1];\n const from = `__wp_layout_L${upper.level}`;\n const to = `__wp_layout_L${lower.level}`;\n if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {\n dot += ` \"${from}\" -> \"${to}\" ${edgeAttrs};\\n`;\n continue;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n dot += ` \"${spacer}\" ${nodeAttrs};\\n { rank=same; \"${spacer}\"; }\\n`;\n dot += ` \"${from}\" -> \"${spacer}\" ${edgeAttrs};\\n \"${spacer}\" -> \"${to}\" ${edgeAttrs};\\n`;\n }\n return dot;\n }\n}\n\n/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */\nabstract class WpFilterPage {\n protected readonly chain: WpGraphChain;\n protected locked: string | null = null;\n protected anchor: string | null = null;\n protected retained: Set<string>;\n private viz: VizInstance | null = null;\n\n constructor(protected readonly model: RenderModelJson) {\n this.chain = new WpGraphChain(model);\n this.retained = new Set(model.nodes.map((node) => node.id));\n }\n\n render(): void {\n document\n .getElementById('wp-filter-off')\n ?.addEventListener('click', () => this.filter(null));\n this.wireControls();\n Viz.instance()\n .then((viz) => {\n this.viz = viz;\n this.filter(null);\n })\n // webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value\n .catch((err: unknown): void =>\n this.error(err instanceof Error ? err : new Error(String(err))),\n );\n }\n\n protected captureBinding(_svg: SVGSVGElement | null): () => void { return (): void => {}; }\n protected wireControls(): void {}\n protected abstract wireSvg(svg: SVGSVGElement): void;\n protected prepareSvg(_svg: SVGSVGElement): void {}\n\n filterItem(name: string): WpNodeMenuItem {\n return new WpNodeMenuItem(\n this.anchor === null ? 'Filter Unconnected' : 'Turn off Filter',\n () => this.filter(this.anchor === null ? name : null),\n );\n }\n\n filter(anchor: string | null): void {\n if (this.viz === null) return;\n const host = document.getElementById('graph');\n if (host === null) return;\n const retained =\n anchor === null\n ? new Set(this.model.nodes.map((node) => node.id))\n : this.chain.nodes(anchor);\n const previousAnchor = this.anchor;\n const previousRetained = this.retained;\n const previousSvg = host.querySelector('svg');\n const rollback = this.captureBinding(previousSvg);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure\n try {\n const dot =\n anchor === null\n ? this.model.fullDot\n : new WpFilteredDot(this.model).render(retained);\n const svg = this.viz.renderSVGElement(dot);\n this.prepareSvg(svg);\n WpNodeMenu.close();\n this.anchor = anchor;\n this.retained = retained;\n host.replaceChildren(svg);\n this.wireSvg(svg);\n this.indicator();\n this.error(null);\n // webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary\n } catch (err: unknown) {\n //const error = toError(err);\n this.anchor = previousAnchor;\n this.retained = previousRetained;\n if (previousSvg !== null) host.replaceChildren(previousSvg);\n else host.replaceChildren();\n rollback();\n this.error(err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n private indicator(): void {\n const status = document.getElementById('wp-filter-status');\n const label = document.getElementById('wp-filter-anchor');\n if (status !== null) status.hidden = this.anchor === null;\n if (label !== null) label.textContent = this.anchor;\n if (this.anchor !== null) document.getElementById('wp-filter-off')?.focus();\n else document.querySelector<SVGGElement>('#graph g.wp-node-clickable')?.focus();\n }\n\n private error(err: Error | null): void {\n const el = document.getElementById('wp-graph-error');\n if (el === null) return;\n el.hidden = err === null;\n el.textContent =\n err === null\n ? ''\n : `Graph rendering failed: ${err.message}. Try Turn off Filter or reload the page.`;\n }\n}\n"]}
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Level bands for the architecture graph.
|
|
3
|
-
*
|
|
4
|
-
* Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,
|
|
5
|
-
* descending as you read down, L0 — the foundation everything is built on — always last.
|
|
6
|
-
*
|
|
7
|
-
* `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a
|
|
8
|
-
* level's boxes to one row but says nothing about where that row goes, so graphviz derived each
|
|
9
|
-
* row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an
|
|
10
|
-
* api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and
|
|
11
|
-
* dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in
|
|
12
|
-
* the top row, and what parked an L0 lib in the same row as the L6 servers).
|
|
13
|
-
*
|
|
14
|
-
* So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in
|
|
15
|
-
* that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of
|
|
16
|
-
* level and nothing else. The anchors — never the real boxes — carry the chain, so no fake
|
|
17
|
-
* dependency is ever drawn between two projects.
|
|
18
|
-
*/
|
|
19
1
|
/**
|
|
20
2
|
* One horizontal band of the graph: every visible project sharing a dependency level, plus the
|
|
21
3
|
* invisible anchor that pins the band's vertical position.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LevelBandLayout = exports.LevelBand = void 0;
|
|
4
|
+
const dot_syntax_1 = require("./dot-syntax");
|
|
2
5
|
/**
|
|
3
6
|
* Level bands for the architecture graph.
|
|
4
7
|
*
|
|
@@ -17,8 +20,6 @@
|
|
|
17
20
|
* level and nothing else. The anchors — never the real boxes — carry the chain, so no fake
|
|
18
21
|
* dependency is ever drawn between two projects.
|
|
19
22
|
*/
|
|
20
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.LevelBandLayout = exports.LevelBand = void 0;
|
|
22
23
|
/**
|
|
23
24
|
* A band bigger than this gets a blank spacer band inserted next to it, so the long edges leaving a
|
|
24
25
|
* crowded row have vertical room to fan out instead of cutting straight through the row below.
|
|
@@ -66,7 +67,7 @@ class LevelBandLayout {
|
|
|
66
67
|
dot += ` "${band.anchorName()}" ${LAYOUT_NODE_ATTRS};\n`;
|
|
67
68
|
dot += ` { rank=same; "${band.anchorName()}"; `;
|
|
68
69
|
for (const name of band.nodeNames)
|
|
69
|
-
dot += `"${name}"; `;
|
|
70
|
+
dot += `"${(0, dot_syntax_1.dotValue)(name)}"; `;
|
|
70
71
|
dot += '}\n';
|
|
71
72
|
}
|
|
72
73
|
for (let i = 0; i + 1 < bands.length; i++) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-level-bands.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-level-bands.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"graph-level-bands.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-level-bands.ts"],"names":[],"mappings":";;;AAAA,6CAAwC;AAExC;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,YAAY,GAAG,WAAW,CAAC;AAEjC,wFAAwF;AACxF,MAAM,iBAAiB,GACnB,wEAAwE,YAAY,IAAI,CAAC;AAE7F,sFAAsF;AACtF,MAAM,iBAAiB,GAAG,wBAAwB,YAAY,IAAI,CAAC;AAEnE;;;GAGG;AACH,MAAa,SAAS;IAClB,KAAK,CAAS;IACd,0FAA0F;IAC1F,SAAS,CAAW;IAEpB,YAAY,KAAa,EAAE,SAAmB;QAC1C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED,UAAU;QACN,OAAO,gBAAgB,IAAI,CAAC,KAAK,EAAE,CAAC;IACxC,CAAC;CACJ;AAbD,8BAaC;AAED,oFAAoF;AACpF,MAAa,eAAe;IACxB;;;;OAIG;IACH,GAAG,CAAC,KAAkB;QAClB,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,GAAG,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;YAC1D,GAAG,IAAI,mBAAmB,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC;YACjD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS;gBAAE,GAAG,IAAI,IAAI,IAAA,qBAAQ,EAAC,IAAI,CAAC,KAAK,CAAC;YAClE,GAAG,IAAI,KAAK,CAAC;QACjB,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;OAKG;IACK,OAAO,CAAC,KAAgB,EAAE,KAAgB;QAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB;eACtD,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB,CAAC;QACrD,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;QAC1F,CAAC;QACD,MAAM,MAAM,GAAG,uBAAuB,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;QACpE,OAAO,MAAM,MAAM,KAAK,iBAAiB,KAAK;cACxC,mBAAmB,MAAM,QAAQ;cACjC,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,MAAM,KAAK,iBAAiB,KAAK;cAClE,MAAM,MAAM,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;IAC7E,CAAC;CACJ;AAtCD,0CAsCC","sourcesContent":["import { dotValue } from './dot-syntax';\n\n/**\n * Level bands for the architecture graph.\n *\n * Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,\n * descending as you read down, L0 — the foundation everything is built on — always last.\n *\n * `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a\n * level's boxes to one row but says nothing about where that row goes, so graphviz derived each\n * row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an\n * api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and\n * dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in\n * the top row, and what parked an L0 lib in the same row as the L6 servers).\n *\n * So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in\n * that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of\n * level and nothing else. The anchors — never the real boxes — carry the chain, so no fake\n * dependency is ever drawn between two projects.\n */\n\n/**\n * A band bigger than this gets a blank spacer band inserted next to it, so the long edges leaving a\n * crowded row have vertical room to fan out instead of cutting straight through the row below.\n * Eleven-plus boxes in a row is where that became unreadable in practice.\n */\nconst SPACER_MIN_BAND_SIZE = 10;\n\n/**\n * The class stamped on every node and edge that exists ONLY to pin the layout. Graphviz drops\n * `style=invis` elements from its SVG entirely, so nothing carrying this class reaches the page\n * today; the class is how the DOT tells the browser client which elements are scaffolding, and the\n * client skips them when indexing so an anchor can never be walked as a dependency.\n */\nconst LAYOUT_CLASS = 'wp-layout';\n\n/** Attributes shared by every invisible layout node (rank anchors and spacer bands). */\nconst LAYOUT_NODE_ATTRS =\n `[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"${LAYOUT_CLASS}\"]`;\n\n/** Attributes for the invisible edges that force one band strictly above the next. */\nconst LAYOUT_EDGE_ATTRS = `[style=invis, class=\"${LAYOUT_CLASS}\"]`;\n\n/**\n * One horizontal band of the graph: every visible project sharing a dependency level, plus the\n * invisible anchor that pins the band's vertical position.\n */\nexport class LevelBand {\n level: number;\n /** Short names of the visible projects on this level, sorted for deterministic output. */\n nodeNames: string[];\n\n constructor(level: number, nodeNames: string[]) {\n this.level = level;\n this.nodeNames = nodeNames;\n }\n\n anchorName(): string {\n return `__wp_layout_L${this.level}`;\n }\n}\n\n/** Emits the DOT that pins each band to its own rank, in descending level order. */\nexport class LevelBandLayout {\n /**\n * @param bands the visible projects grouped by level, HIGHEST LEVEL FIRST. Levels need not be\n * contiguous — only the levels actually present get a band, and the chain links\n * whichever ones exist.\n */\n dot(bands: LevelBand[]): string {\n let dot = '';\n for (const band of bands) {\n dot += ` \"${band.anchorName()}\" ${LAYOUT_NODE_ATTRS};\\n`;\n dot += ` { rank=same; \"${band.anchorName()}\"; `;\n for (const name of band.nodeNames) dot += `\"${dotValue(name)}\"; `;\n dot += '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n dot += this.bandGap(bands[i], bands[i + 1]);\n }\n return dot;\n }\n\n /**\n * The ordering chain between two adjacent bands. When either band is crowded\n * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a\n * wide row then have a whole rank of vertical room to fan out in, instead of being drawn\n * straight through the row below.\n */\n private bandGap(upper: LevelBand, lower: LevelBand): string {\n const crowded = upper.nodeNames.length > SPACER_MIN_BAND_SIZE\n || lower.nodeNames.length > SPACER_MIN_BAND_SIZE;\n if (!crowded) {\n return ` \"${upper.anchorName()}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n return ` \"${spacer}\" ${LAYOUT_NODE_ATTRS};\\n`\n + ` { rank=same; \"${spacer}\"; }\\n`\n + ` \"${upper.anchorName()}\" -> \"${spacer}\" ${LAYOUT_EDGE_ATTRS};\\n`\n + ` \"${spacer}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n}\n"]}
|
|
@@ -45,6 +45,7 @@ export declare class GraphNodeMenu {
|
|
|
45
45
|
* menu. Opening a menu closes any other, so at most one is ever on screen.
|
|
46
46
|
*/
|
|
47
47
|
private menuScript;
|
|
48
|
+
private keyboardScript;
|
|
48
49
|
/**
|
|
49
50
|
* The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities
|
|
50
51
|
* list, so locking a box dims every other box in that graph and lights the locked one.
|
|
@@ -151,13 +151,18 @@ class GraphNodeMenu {
|
|
|
151
151
|
menu.style.left = left + 'px';
|
|
152
152
|
menu.style.top = (box.bottom + window.scrollY + 4) + 'px';
|
|
153
153
|
}
|
|
154
|
+
${this.keyboardScript()}
|
|
154
155
|
static wire(svg, itemsFor) {
|
|
155
156
|
svg.querySelectorAll('g.node').forEach(function (node) {
|
|
156
157
|
const title = node.querySelector('title');
|
|
157
158
|
const name = title === null || title.textContent === null
|
|
158
159
|
? '' : title.textContent.trim();
|
|
159
160
|
if (name === '') return;
|
|
161
|
+
if (node.classList.contains('wp-layout')) return;
|
|
160
162
|
node.classList.add('wp-node-clickable');
|
|
163
|
+
node.setAttribute('tabindex', '0');
|
|
164
|
+
node.setAttribute('role', 'button');
|
|
165
|
+
WpNodeMenu.wireKeyboard(node, name, itemsFor);
|
|
161
166
|
node.addEventListener('click', function (ev) {
|
|
162
167
|
ev.preventDefault();
|
|
163
168
|
ev.stopPropagation();
|
|
@@ -167,6 +172,17 @@ class GraphNodeMenu {
|
|
|
167
172
|
}
|
|
168
173
|
}`;
|
|
169
174
|
}
|
|
175
|
+
keyboardScript() {
|
|
176
|
+
return `static wireKeyboard(node, name, itemsFor) {
|
|
177
|
+
node.addEventListener('keydown', function (ev) {
|
|
178
|
+
if (ev.key !== 'Enter' && ev.key !== ' ') return;
|
|
179
|
+
ev.preventDefault();
|
|
180
|
+
ev.stopPropagation();
|
|
181
|
+
WpNodeMenu.open(node, name, itemsFor(name, node));
|
|
182
|
+
document.querySelector('#wp-node-menu button')?.focus();
|
|
183
|
+
});
|
|
184
|
+
}`;
|
|
185
|
+
}
|
|
170
186
|
/**
|
|
171
187
|
* The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities
|
|
172
188
|
* list, so locking a box dims every other box in that graph and lights the locked one.
|
|
@@ -175,6 +191,19 @@ class GraphNodeMenu {
|
|
|
175
191
|
return `
|
|
176
192
|
class WpNodeLock {
|
|
177
193
|
constructor(svg) { this.svg = svg; this.locked = null; }
|
|
194
|
+
rebind(svg) {
|
|
195
|
+
const name = this.locked;
|
|
196
|
+
this.svg = svg;
|
|
197
|
+
this.clear();
|
|
198
|
+
this.locked = name;
|
|
199
|
+
if (name === null) return;
|
|
200
|
+
for (const node of svg.querySelectorAll('g.node')) {
|
|
201
|
+
if (node.querySelector('title')?.textContent === name) {
|
|
202
|
+
this.lock(name, node);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
178
207
|
isLocked(name) { return this.locked === name; }
|
|
179
208
|
toggle(name, nodeEl) {
|
|
180
209
|
if (this.locked === name) this.clear();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-node-menu.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-node-menu.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,aAAa;IACtB;;;;OAIG;IACH,MAAM;QACF,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8CL,CAAC;IACP,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,KAAa;QACnB,OAAO;UACL,KAAK,WAAW,KAAK;UACrB,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK,yCAAyC,CAAC;IACrD,CAAC;IAED,+FAA+F;IAC/F,MAAM;QACF,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;IACjF,CAAC;IAED;;;;OAIG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA2DL,CAAC;IACP,CAAC;IAED;;;OAGG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;UAqBL,CAAC;IACP,CAAC;IAED,2EAA2E;IACnE,aAAa;QACjB,OAAO;;;;YAIH,CAAC;IACT,CAAC;CACJ;AAvLD,sCAuLC","sourcesContent":["/**\n * The floating per-node menu, shared by ALL THREE generated graphs.\n *\n * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the\n * click wiring that opens it — and `architecture/dependencies.html` (graph-visualizer.ts), every\n * project's `design.html` (di-graph/design-visualizer.ts) and `tmp/webpieces/runtime-architecture.html`\n * (runtime-visualizer.ts) all inline these same bytes. Duplicating it into the three emitters is what\n * this module exists to prevent.\n *\n * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`\n * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the\n * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a\n * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one\n * copy reachable by every emitter with no build-order coupling. The two pages that DO have a compiled\n * client (`graph-visualizer.client.ts`, `runtime-visualizer.client.ts`) call into these classes as\n * ambient globals — see viz-client-globals.d.ts.\n *\n * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:\n * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page and the runtime page have neither dropdown nor responsibilities, so they use\n * `WpNodeLock` below — dim every other box in that graph, light the locked one.\n * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.\n */\nexport class GraphNodeMenu {\n /**\n * Menu chrome + the \"this box is clickable\" affordance. Every node is clickable now (the menu\n * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with\n * a design page.\n */\n styles(): string {\n return `\n .wp-node-menu {\n position: absolute;\n z-index: 1000;\n min-width: 150px;\n padding: 4px 0;\n background: white;\n border: 1px solid #cfcfcf;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0,0,0,0.22);\n font-family: Arial, sans-serif;\n font-size: 14px;\n }\n .wp-node-menu-title {\n padding: 4px 14px 6px;\n color: #777;\n font-size: 12px;\n font-family: monospace;\n border-bottom: 1px solid #eee;\n margin-bottom: 4px;\n }\n .wp-node-menu-item {\n display: block;\n width: 100%;\n padding: 7px 14px;\n border: 0;\n background: none;\n color: #1565C0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n }\n .wp-node-menu-item:hover { background: #E3F2FD; }\n g.wp-node-clickable { cursor: pointer; }\n /* Every shape Graphviz (or the runtime page's own queue-cylinder redraw) can emit for a node\n * body: box/record -> polygon, circle -> ellipse, cylinder and the redrawn queue -> path. A\n * shape left out of this list would be a box that is clickable but never looks it. */\n g.wp-node-clickable polygon,\n g.wp-node-clickable ellipse,\n g.wp-node-clickable path { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n g.wp-node-clickable:hover polygon,\n g.wp-node-clickable:hover ellipse,\n g.wp-node-clickable:hover path {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }`;\n }\n\n /**\n * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered\n * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).\n *\n * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat\n * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.\n */\n dimStyles(scope: string): string {\n return `\n ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }\n ${scope} svg.wp-dim .node,\n ${scope} svg.wp-dim .edge { opacity: 0.15; }\n ${scope} svg.wp-dim .node.wp-focus,\n ${scope} svg.wp-dim .node.wp-neighbor,\n ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;\n }\n\n /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */\n script(): string {\n return `${this.menuScript()}\\n${this.lockScript()}\\n${this.dismissScript()}`;\n }\n\n /**\n * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,\n * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole\n * menu. Opening a menu closes any other, so at most one is ever on screen.\n */\n private menuScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }\n }\n class WpNodeMenu {\n static close() {\n const open = document.getElementById('wp-node-menu');\n if (open !== null) open.remove();\n }\n static open(nodeEl, name, items) {\n WpNodeMenu.close();\n const menu = document.createElement('div');\n menu.id = 'wp-node-menu';\n menu.className = 'wp-node-menu';\n menu.addEventListener('click', function (ev) { ev.stopPropagation(); });\n const heading = document.createElement('div');\n heading.className = 'wp-node-menu-title';\n heading.textContent = name;\n menu.appendChild(heading);\n for (const item of items) menu.appendChild(WpNodeMenu.button(item));\n document.body.appendChild(menu);\n WpNodeMenu.place(menu, nodeEl);\n }\n static button(item) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n button.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.close();\n item.onSelect();\n });\n return button;\n }\n static place(menu, nodeEl) {\n const box = nodeEl.getBoundingClientRect();\n const own = menu.getBoundingClientRect();\n const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;\n let left = box.left + window.scrollX;\n if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);\n menu.style.left = left + 'px';\n menu.style.top = (box.bottom + window.scrollY + 4) + 'px';\n }\n static wire(svg, itemsFor) {\n svg.querySelectorAll('g.node').forEach(function (node) {\n const title = node.querySelector('title');\n const name = title === null || title.textContent === null\n ? '' : title.textContent.trim();\n if (name === '') return;\n node.classList.add('wp-node-clickable');\n node.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n });\n }\n }`;\n }\n\n /**\n * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities\n * list, so locking a box dims every other box in that graph and lights the locked one.\n */\n private lockScript(): string {\n return `\n class WpNodeLock {\n constructor(svg) { this.svg = svg; this.locked = null; }\n isLocked(name) { return this.locked === name; }\n toggle(name, nodeEl) {\n if (this.locked === name) this.clear();\n else this.lock(name, nodeEl);\n }\n lock(name, nodeEl) {\n this.clear();\n this.locked = name;\n this.svg.classList.add('wp-dim');\n nodeEl.classList.add('wp-focus');\n }\n clear() {\n this.locked = null;\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus').forEach(function (el) {\n el.classList.remove('wp-focus');\n });\n }\n }`;\n }\n\n /** Outside click and Escape both dismiss — wired once, on the document. */\n private dismissScript(): string {\n return `\n document.addEventListener('click', function () { WpNodeMenu.close(); });\n document.addEventListener('keydown', function (ev) {\n if (ev.key === 'Escape') WpNodeMenu.close();\n });`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-node-menu.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-node-menu.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,aAAa;IACtB;;;;OAIG;IACH,MAAM;QACF,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8CL,CAAC;IACP,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,KAAa;QACnB,OAAO;UACL,KAAK,WAAW,KAAK;UACrB,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK,yCAAyC,CAAC;IACrD,CAAC;IAED,+FAA+F;IAC/F,MAAM;QACF,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;IACjF,CAAC;IAED;;;;OAIG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA6CD,IAAI,CAAC,cAAc,EAAE;;;;;;;;;;;;;;;;;;;UAmBzB,CAAC;IACP,CAAC;IAEO,cAAc;QAClB,OAAO;;;;;;;;UAQL,CAAC;IACP,CAAC;IAED;;;OAGG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAkCL,CAAC;IACP,CAAC;IAED,2EAA2E;IACnE,aAAa;QACjB,OAAO;;;;YAIH,CAAC;IACT,CAAC;CACJ;AArND,sCAqNC","sourcesContent":["/**\n * The floating per-node menu, shared by ALL THREE generated graphs.\n *\n * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the\n * click wiring that opens it — and `architecture/dependencies.html` (graph-visualizer.ts), every\n * project's `design.html` (di-graph/design-visualizer.ts) and `tmp/webpieces/runtime-architecture.html`\n * (runtime-visualizer.ts) all inline these same bytes. Duplicating it into the three emitters is what\n * this module exists to prevent.\n *\n * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`\n * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the\n * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a\n * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one\n * copy reachable by every emitter with no build-order coupling. The two pages that DO have a compiled\n * client (`graph-visualizer.client.ts`, `runtime-visualizer.client.ts`) call into these classes as\n * ambient globals — see viz-client-globals.d.ts.\n *\n * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:\n * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page and the runtime page have neither dropdown nor responsibilities, so they use\n * `WpNodeLock` below — dim every other box in that graph, light the locked one.\n * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.\n */\nexport class GraphNodeMenu {\n /**\n * Menu chrome + the \"this box is clickable\" affordance. Every node is clickable now (the menu\n * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with\n * a design page.\n */\n styles(): string {\n return `\n .wp-node-menu {\n position: absolute;\n z-index: 1000;\n min-width: 150px;\n padding: 4px 0;\n background: white;\n border: 1px solid #cfcfcf;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0,0,0,0.22);\n font-family: Arial, sans-serif;\n font-size: 14px;\n }\n .wp-node-menu-title {\n padding: 4px 14px 6px;\n color: #777;\n font-size: 12px;\n font-family: monospace;\n border-bottom: 1px solid #eee;\n margin-bottom: 4px;\n }\n .wp-node-menu-item {\n display: block;\n width: 100%;\n padding: 7px 14px;\n border: 0;\n background: none;\n color: #1565C0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n }\n .wp-node-menu-item:hover { background: #E3F2FD; }\n g.wp-node-clickable { cursor: pointer; }\n /* Every shape Graphviz (or the runtime page's own queue-cylinder redraw) can emit for a node\n * body: box/record -> polygon, circle -> ellipse, cylinder and the redrawn queue -> path. A\n * shape left out of this list would be a box that is clickable but never looks it. */\n g.wp-node-clickable polygon,\n g.wp-node-clickable ellipse,\n g.wp-node-clickable path { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n g.wp-node-clickable:hover polygon,\n g.wp-node-clickable:hover ellipse,\n g.wp-node-clickable:hover path {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }`;\n }\n\n /**\n * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered\n * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).\n *\n * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat\n * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.\n */\n dimStyles(scope: string): string {\n return `\n ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }\n ${scope} svg.wp-dim .node,\n ${scope} svg.wp-dim .edge { opacity: 0.15; }\n ${scope} svg.wp-dim .node.wp-focus,\n ${scope} svg.wp-dim .node.wp-neighbor,\n ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;\n }\n\n /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */\n script(): string {\n return `${this.menuScript()}\\n${this.lockScript()}\\n${this.dismissScript()}`;\n }\n\n /**\n * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,\n * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole\n * menu. Opening a menu closes any other, so at most one is ever on screen.\n */\n private menuScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }\n }\n class WpNodeMenu {\n static close() {\n const open = document.getElementById('wp-node-menu');\n if (open !== null) open.remove();\n }\n static open(nodeEl, name, items) {\n WpNodeMenu.close();\n const menu = document.createElement('div');\n menu.id = 'wp-node-menu';\n menu.className = 'wp-node-menu';\n menu.addEventListener('click', function (ev) { ev.stopPropagation(); });\n const heading = document.createElement('div');\n heading.className = 'wp-node-menu-title';\n heading.textContent = name;\n menu.appendChild(heading);\n for (const item of items) menu.appendChild(WpNodeMenu.button(item));\n document.body.appendChild(menu);\n WpNodeMenu.place(menu, nodeEl);\n }\n static button(item) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n button.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.close();\n item.onSelect();\n });\n return button;\n }\n static place(menu, nodeEl) {\n const box = nodeEl.getBoundingClientRect();\n const own = menu.getBoundingClientRect();\n const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;\n let left = box.left + window.scrollX;\n if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);\n menu.style.left = left + 'px';\n menu.style.top = (box.bottom + window.scrollY + 4) + 'px';\n }\n ${this.keyboardScript()}\n static wire(svg, itemsFor) {\n svg.querySelectorAll('g.node').forEach(function (node) {\n const title = node.querySelector('title');\n const name = title === null || title.textContent === null\n ? '' : title.textContent.trim();\n if (name === '') return;\n if (node.classList.contains('wp-layout')) return;\n node.classList.add('wp-node-clickable');\n node.setAttribute('tabindex', '0');\n node.setAttribute('role', 'button');\n WpNodeMenu.wireKeyboard(node, name, itemsFor);\n node.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n });\n }\n }`;\n }\n\n private keyboardScript(): string {\n return `static wireKeyboard(node, name, itemsFor) {\n node.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Enter' && ev.key !== ' ') return;\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n document.querySelector('#wp-node-menu button')?.focus();\n });\n }`;\n }\n\n /**\n * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities\n * list, so locking a box dims every other box in that graph and lights the locked one.\n */\n private lockScript(): string {\n return `\n class WpNodeLock {\n constructor(svg) { this.svg = svg; this.locked = null; }\n rebind(svg) {\n const name = this.locked;\n this.svg = svg;\n this.clear();\n this.locked = name;\n if (name === null) return;\n for (const node of svg.querySelectorAll('g.node')) {\n if (node.querySelector('title')?.textContent === name) {\n this.lock(name, node);\n break;\n }\n }\n }\n isLocked(name) { return this.locked === name; }\n toggle(name, nodeEl) {\n if (this.locked === name) this.clear();\n else this.lock(name, nodeEl);\n }\n lock(name, nodeEl) {\n this.clear();\n this.locked = name;\n this.svg.classList.add('wp-dim');\n nodeEl.classList.add('wp-focus');\n }\n clear() {\n this.locked = null;\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus').forEach(function (el) {\n el.classList.remove('wp-focus');\n });\n }\n }`;\n }\n\n /** Outside click and Escape both dismiss — wired once, on the document. */\n private dismissScript(): string {\n return `\n document.addEventListener('click', function () { WpNodeMenu.close(); });\n document.addEventListener('keydown', function (ev) {\n if (ev.key === 'Escape') WpNodeMenu.close();\n });`;\n }\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { LevelBand } from './graph-level-bands';
|
|
2
|
+
/** Renderer-owned records, captured while emitting DOT, never recovered by parsing labels. */
|
|
3
|
+
export declare class RenderNode {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly dot: string;
|
|
6
|
+
constructor(id: string, dot: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class RenderEdge {
|
|
9
|
+
readonly from: string;
|
|
10
|
+
readonly to: string;
|
|
11
|
+
readonly dot: string;
|
|
12
|
+
readonly id: string;
|
|
13
|
+
constructor(from: string, to: string, dot: string, id: string);
|
|
14
|
+
}
|
|
15
|
+
/** The immutable drawable snapshot carried by a generated page. */
|
|
16
|
+
export declare class GraphRenderModel {
|
|
17
|
+
readonly nodes: RenderNode[];
|
|
18
|
+
readonly edges: RenderEdge[];
|
|
19
|
+
bands: LevelBand[];
|
|
20
|
+
fullDot: string;
|
|
21
|
+
header: string;
|
|
22
|
+
footer: string;
|
|
23
|
+
node(id: string, dot: string): string;
|
|
24
|
+
completeEndpoints(): void;
|
|
25
|
+
edge(from: string, to: string, dot: string): string;
|
|
26
|
+
}
|
|
27
|
+
/** Shared filter chrome and script-safe JSON for pages opened directly from disk. */
|
|
28
|
+
export declare class GraphFilterAssets {
|
|
29
|
+
json<T>(value: T): string;
|
|
30
|
+
html(): string;
|
|
31
|
+
styles(): string;
|
|
32
|
+
}
|