@sleetdrop/dsh-plugin-topology 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NEXT-STEPS.md +14 -7
- package/README.md +26 -5
- package/lib/client/NodeDetailPanel.d.ts +25 -0
- package/lib/client/NodeDetailPanel.js +32 -0
- package/lib/client/NodePopover.d.ts +39 -0
- package/lib/client/NodePopover.js +96 -0
- package/lib/client/TopologyGraphView.d.ts +49 -6
- package/lib/client/TopologyGraphView.js +244 -27
- package/lib/client/TopologyView.d.ts +2 -2
- package/lib/client/TopologyView.js +84 -10
- package/lib/client/index.js +2 -2
- package/lib/client/locales.d.ts +1 -1
- package/lib/client/locales.js +10 -0
- package/lib/client/remote-client.js +12 -0
- package/lib/client.js +586 -70
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +7 -6
- package/lib/render.d.ts +22 -5
- package/lib/render.js +48 -19
- package/lib/typert.d.ts +12 -0
- package/lib/typert.js +11 -0
- package/package.json +20 -20
package/lib/client.js
CHANGED
|
@@ -4392,6 +4392,7 @@ const render_parameter_0$schema = union([
|
|
|
4392
4392
|
literal("svg")
|
|
4393
4393
|
]);
|
|
4394
4394
|
const render_parameter_1$schema = union([literal("TB"), literal("LR")]);
|
|
4395
|
+
const render_parameter_2$schema = union([literal("light"), literal("dark")]).optional();
|
|
4395
4396
|
const render_result$schema = string();
|
|
4396
4397
|
const TYPERT_REMOTE = {
|
|
4397
4398
|
package: "@sleetdrop/dsh-plugin-topology",
|
|
@@ -4413,25 +4414,39 @@ const TYPERT_REMOTE = {
|
|
|
4413
4414
|
namespace: "pluginTopology",
|
|
4414
4415
|
method: "render",
|
|
4415
4416
|
invocation: { kind: "direct" },
|
|
4416
|
-
parameters: [
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4417
|
+
parameters: [
|
|
4418
|
+
{
|
|
4419
|
+
name: "format",
|
|
4420
|
+
wire: "format",
|
|
4421
|
+
source: "json",
|
|
4422
|
+
codec: {
|
|
4423
|
+
mode: "strict",
|
|
4424
|
+
typeSymbol: "@sleetdrop/dsh-plugin-topology/types#RenderFormat",
|
|
4425
|
+
create: () => render_parameter_0$schema
|
|
4426
|
+
}
|
|
4427
|
+
},
|
|
4428
|
+
{
|
|
4429
|
+
name: "rankdir",
|
|
4430
|
+
wire: "rankdir",
|
|
4431
|
+
source: "json",
|
|
4432
|
+
codec: {
|
|
4433
|
+
mode: "strict",
|
|
4434
|
+
typeSymbol: "@sleetdrop/dsh-plugin-topology/types#Rankdir",
|
|
4435
|
+
create: () => render_parameter_1$schema
|
|
4436
|
+
}
|
|
4437
|
+
},
|
|
4438
|
+
{
|
|
4439
|
+
name: "theme",
|
|
4440
|
+
wire: "theme",
|
|
4441
|
+
source: "json",
|
|
4442
|
+
acceptsUndefined: true,
|
|
4443
|
+
codec: {
|
|
4444
|
+
mode: "strict",
|
|
4445
|
+
typeSymbol: "@sleetdrop/dsh-plugin-topology/types#Theme",
|
|
4446
|
+
create: () => render_parameter_2$schema
|
|
4447
|
+
}
|
|
4433
4448
|
}
|
|
4434
|
-
|
|
4449
|
+
],
|
|
4435
4450
|
result: {
|
|
4436
4451
|
mode: "strict",
|
|
4437
4452
|
typeSymbol: "@sleetdrop/dsh-plugin-topology#pluginTopology/render:result",
|
|
@@ -4627,13 +4642,13 @@ function OutlineButton({ children, onClick, title, icon }) {
|
|
|
4627
4642
|
|
|
4628
4643
|
//#endregion
|
|
4629
4644
|
//#region \0dsh-css:/Users/jiangyuan/Documents/side-work/dsh-plugin-topology/src/client/TopologyGraphView.module.css.mjs
|
|
4630
|
-
const css$
|
|
4631
|
-
const tagId$
|
|
4632
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$
|
|
4645
|
+
const css$3 = ".s3EGUG_graphBox{flex-direction:column;min-height:0;display:flex;position:relative}.s3EGUG_graphViewport{cursor:grab;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#0000001a);border-radius:8px;flex:1;min-height:0;position:relative;overflow:hidden}.s3EGUG_graphViewport:active{cursor:grabbing}.s3EGUG_graphImg{transform-origin:0 0;user-select:none;width:max-content;position:absolute;top:0;left:0}.s3EGUG_graphImg .topo-hover-node>ellipse,.s3EGUG_graphImg .topo-hover-node>polygon{stroke:var(--dsw-alias-state-business-primary,#4176e6);stroke-opacity:.5;stroke-width:1.6px}.s3EGUG_graphImg .topo-selected-node>ellipse,.s3EGUG_graphImg .topo-selected-node>polygon,.s3EGUG_graphImg .topo-hl-node>ellipse,.s3EGUG_graphImg .topo-hl-node>polygon{stroke:var(--dsw-alias-state-business-primary,#4176e6);stroke-opacity:1;stroke-width:2.2px}.s3EGUG_graphImg .topo-hl-neighbor>ellipse,.s3EGUG_graphImg .topo-hl-neighbor>polygon{stroke:var(--dsw-alias-state-business-primary,#4176e6);stroke-opacity:.45;stroke-width:1.4px}.s3EGUG_graphImg .topo-hl-edge path{fill:none;stroke:var(--dsw-alias-state-business-primary,#4176e6);stroke-opacity:1;stroke-width:1.4px}.s3EGUG_graphImg .topo-hl-edge polygon{stroke:var(--dsw-alias-state-business-primary,#4176e6);stroke-opacity:1;fill:var(--dsw-alias-state-business-primary,#4176e6);fill-opacity:1}.s3EGUG_graphImg .topo-hl-edge text{fill:var(--dsw-alias-state-business-primary,#4176e6);fill-opacity:1}.s3EGUG_graphImg .topo-dimmed{opacity:.12}.s3EGUG_graphImg .topo-hover-node,.s3EGUG_graphImg .topo-selected-node,.s3EGUG_graphImg .topo-hl-node,.s3EGUG_graphImg .topo-hl-neighbor,.s3EGUG_graphImg .topo-hl-edge,.s3EGUG_graphImg .topo-dimmed{transition-property:opacity,stroke,stroke-opacity,stroke-width,fill,fill-opacity;transition-duration:.12s;transition-timing-function:ease}.s3EGUG_zoomControls{border:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:8px;align-items:center;gap:2px;padding:4px;display:flex;position:absolute;bottom:12px;right:12px;box-shadow:0 6px 16px #00000014}.s3EGUG_zoomControls button{color:var(--dsw-alias-label-secondary,#61666b);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}.s3EGUG_zoomControls button:hover{color:var(--dsw-alias-label-primary,#0f1115);border-color:var(--dsw-alias-border-l2,#0000001a)}.s3EGUG_zoomControls .s3EGUG_iconButton{width:24px;height:24px}.s3EGUG_zoomControls .s3EGUG_zoomButton{transition:opacity .15s,width .15s}.s3EGUG_zoomLevel{text-align:center;min-width:46px;height:22px;color:var(--dsw-alias-label-secondary,#61666b);padding:0 4px;font-size:11px;line-height:1;transition:opacity .15s,width .15s}.s3EGUG_zoomLevel:hover{color:var(--dsw-alias-label-primary,#0f1115)}.s3EGUG_zoomControls.s3EGUG_collapsed .s3EGUG_zoomButton,.s3EGUG_zoomControls.s3EGUG_collapsed .s3EGUG_zoomLevel{opacity:0;pointer-events:none;border:0;width:0;min-width:0;height:0;margin:0;padding:0;overflow:hidden}";
|
|
4646
|
+
const tagId$3 = "dsh-plugin-topology/TopologyGraphView.module.css";
|
|
4647
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
|
|
4633
4648
|
const tag = document.createElement("style");
|
|
4634
4649
|
tag.dataset.plugin = "dsh-plugin-topology";
|
|
4635
|
-
tag.dataset.pluginCss = tagId$
|
|
4636
|
-
tag.textContent = css$
|
|
4650
|
+
tag.dataset.pluginCss = tagId$3;
|
|
4651
|
+
tag.textContent = css$3;
|
|
4637
4652
|
document.head.appendChild(tag);
|
|
4638
4653
|
}
|
|
4639
4654
|
var TopologyGraphView_module_css_default = {
|
|
@@ -4649,6 +4664,178 @@ var TopologyGraphView_module_css_default = {
|
|
|
4649
4664
|
|
|
4650
4665
|
//#endregion
|
|
4651
4666
|
//#region src/client/TopologyGraphView.tsx
|
|
4667
|
+
/**
|
|
4668
|
+
* Classes owned and toggled by the highlight layer. The hover ring is managed
|
|
4669
|
+
* separately by the pointer listener so a selection repaint cannot drop it.
|
|
4670
|
+
*/
|
|
4671
|
+
const HIGHLIGHT_CLASSES = [
|
|
4672
|
+
"topo-selected-node",
|
|
4673
|
+
"topo-hl-node",
|
|
4674
|
+
"topo-hl-neighbor",
|
|
4675
|
+
"topo-hl-edge",
|
|
4676
|
+
"topo-dimmed"
|
|
4677
|
+
];
|
|
4678
|
+
/** Read the DOT node id and display label from a Graphviz node group. */
|
|
4679
|
+
function nodeRefFromElement(element) {
|
|
4680
|
+
if (!(element instanceof Element)) return null;
|
|
4681
|
+
const group = element.closest("g.node");
|
|
4682
|
+
if (group === null) return null;
|
|
4683
|
+
const dotId = group.querySelector(":scope > title")?.textContent;
|
|
4684
|
+
if (dotId === null || dotId === void 0) return null;
|
|
4685
|
+
const label = group.querySelector(":scope > text")?.textContent ?? dotId;
|
|
4686
|
+
return {
|
|
4687
|
+
ref: {
|
|
4688
|
+
key: group.id,
|
|
4689
|
+
dotId,
|
|
4690
|
+
label
|
|
4691
|
+
},
|
|
4692
|
+
group
|
|
4693
|
+
};
|
|
4694
|
+
}
|
|
4695
|
+
/**
|
|
4696
|
+
* The nearest ancestor `<g>` that wraps one whole Graphviz layout (main or
|
|
4697
|
+
* isolated). `composeGraphvizSvgs` places each layout in its own translated
|
|
4698
|
+
* group, so this is also the isolation boundary: `m:N` ids are only unique
|
|
4699
|
+
* inside one layout and must not be matched across both.
|
|
4700
|
+
*/
|
|
4701
|
+
function graphScope(node) {
|
|
4702
|
+
let current = node;
|
|
4703
|
+
while (current.parentElement !== null && current.parentElement.tagName.toLowerCase() !== "svg") current = current.parentElement;
|
|
4704
|
+
return current;
|
|
4705
|
+
}
|
|
4706
|
+
/** Split a Graphviz edge `<title>` (`source->target`) into its endpoints. */
|
|
4707
|
+
function edgeEndpoints(edge) {
|
|
4708
|
+
const title = edge.querySelector(":scope > title")?.textContent;
|
|
4709
|
+
if (title === null || title === void 0) return null;
|
|
4710
|
+
const arrowIdx = title.indexOf("->");
|
|
4711
|
+
if (arrowIdx < 0) return null;
|
|
4712
|
+
return {
|
|
4713
|
+
source: title.slice(0, arrowIdx),
|
|
4714
|
+
target: title.slice(arrowIdx + 2)
|
|
4715
|
+
};
|
|
4716
|
+
}
|
|
4717
|
+
/** Count the drawn outgoing/incoming edges of a node inside one layout scope. */
|
|
4718
|
+
function edgeStats(scope, dotId) {
|
|
4719
|
+
let out = 0;
|
|
4720
|
+
let incoming = 0;
|
|
4721
|
+
for (const edge of scope.querySelectorAll("g.edge")) {
|
|
4722
|
+
const endpoints = edgeEndpoints(edge);
|
|
4723
|
+
if (endpoints === null) continue;
|
|
4724
|
+
if (endpoints.source === dotId) out += 1;
|
|
4725
|
+
if (endpoints.target === dotId) incoming += 1;
|
|
4726
|
+
}
|
|
4727
|
+
return {
|
|
4728
|
+
out,
|
|
4729
|
+
in: incoming
|
|
4730
|
+
};
|
|
4731
|
+
}
|
|
4732
|
+
/** Find the node group with the given stable element id. */
|
|
4733
|
+
function findNodeGroup(container, key) {
|
|
4734
|
+
for (const group of container.querySelectorAll("g.node")) if (group.id === key) return group;
|
|
4735
|
+
return null;
|
|
4736
|
+
}
|
|
4737
|
+
/** Drop every highlight class this module owns. */
|
|
4738
|
+
function clearHighlight(container) {
|
|
4739
|
+
const selector = HIGHLIGHT_CLASSES.map((name) => `.${name}`).join(",");
|
|
4740
|
+
for (const element of container.querySelectorAll(selector)) element.classList.remove(...HIGHLIGHT_CLASSES);
|
|
4741
|
+
}
|
|
4742
|
+
/**
|
|
4743
|
+
* Paint the selection: the selected node always gets a marker ring; when a
|
|
4744
|
+
* direction is active, only that direction's edges and their far-end nodes stay
|
|
4745
|
+
* lit while the rest of the same layout dims. The other layout (isolated
|
|
4746
|
+
* plugins) is left untouched, since it has no edges to follow.
|
|
4747
|
+
*/
|
|
4748
|
+
function applyHighlight(container, selection) {
|
|
4749
|
+
clearHighlight(container);
|
|
4750
|
+
if (selection === null) return;
|
|
4751
|
+
const group = findNodeGroup(container, selection.key);
|
|
4752
|
+
if (group === null) return;
|
|
4753
|
+
group.classList.add("topo-selected-node");
|
|
4754
|
+
if (selection.direction === null) return;
|
|
4755
|
+
const scope = graphScope(group);
|
|
4756
|
+
const lit = new Set([selection.dotId]);
|
|
4757
|
+
const connectedEdges = /* @__PURE__ */ new Set();
|
|
4758
|
+
for (const edge of scope.querySelectorAll("g.edge")) {
|
|
4759
|
+
const endpoints = edgeEndpoints(edge);
|
|
4760
|
+
if (endpoints === null) continue;
|
|
4761
|
+
if (!(selection.direction === "out" ? endpoints.source === selection.dotId : endpoints.target === selection.dotId)) continue;
|
|
4762
|
+
connectedEdges.add(edge);
|
|
4763
|
+
lit.add(endpoints.source);
|
|
4764
|
+
lit.add(endpoints.target);
|
|
4765
|
+
}
|
|
4766
|
+
group.classList.add("topo-hl-node");
|
|
4767
|
+
for (const node of scope.querySelectorAll("g.node")) {
|
|
4768
|
+
if (node === group) continue;
|
|
4769
|
+
const dotId = node.querySelector(":scope > title")?.textContent;
|
|
4770
|
+
if (dotId !== null && dotId !== void 0 && lit.has(dotId)) node.classList.add("topo-hl-neighbor");
|
|
4771
|
+
else node.classList.add("topo-dimmed");
|
|
4772
|
+
}
|
|
4773
|
+
for (const edge of scope.querySelectorAll("g.edge")) edge.classList.add(connectedEdges.has(edge) ? "topo-hl-edge" : "topo-dimmed");
|
|
4774
|
+
}
|
|
4775
|
+
/**
|
|
4776
|
+
* Inline SVG renderer. Encapsulates the raw HTML injection behind a meaningful
|
|
4777
|
+
* component name so readers are not startled by the React API at call sites.
|
|
4778
|
+
* Hover and highlight are applied as classes on the Graphviz DOM, which React
|
|
4779
|
+
* does not own; the exported SVG string itself stays free of listeners.
|
|
4780
|
+
*/
|
|
4781
|
+
function SvgCanvas({ svg, alt, fitted, view, selectedKey, selectedDotId, direction }) {
|
|
4782
|
+
const containerRef = (0, react.useRef)(null);
|
|
4783
|
+
/** The group currently carrying the hover ring, to avoid redundant DOM work. */
|
|
4784
|
+
const hoveredRef = (0, react.useRef)(null);
|
|
4785
|
+
(0, react.useEffect)(() => {
|
|
4786
|
+
const container = containerRef.current;
|
|
4787
|
+
if (container === null) return;
|
|
4788
|
+
applyHighlight(container, selectedKey !== null && selectedDotId !== null ? {
|
|
4789
|
+
key: selectedKey,
|
|
4790
|
+
dotId: selectedDotId,
|
|
4791
|
+
direction
|
|
4792
|
+
} : null);
|
|
4793
|
+
}, [
|
|
4794
|
+
selectedKey,
|
|
4795
|
+
selectedDotId,
|
|
4796
|
+
direction,
|
|
4797
|
+
svg
|
|
4798
|
+
]);
|
|
4799
|
+
(0, react.useEffect)(() => {
|
|
4800
|
+
const container = containerRef.current;
|
|
4801
|
+
if (container === null) return;
|
|
4802
|
+
hoveredRef.current = null;
|
|
4803
|
+
const onMouseOver = (event) => {
|
|
4804
|
+
const found = nodeRefFromElement(event.target);
|
|
4805
|
+
if (found === null || found.group === hoveredRef.current) return;
|
|
4806
|
+
hoveredRef.current?.classList.remove("topo-hover-node");
|
|
4807
|
+
hoveredRef.current = found.group;
|
|
4808
|
+
found.group.classList.add("topo-hover-node");
|
|
4809
|
+
container.style.cursor = "pointer";
|
|
4810
|
+
};
|
|
4811
|
+
const onMouseOut = (event) => {
|
|
4812
|
+
const hovered = hoveredRef.current;
|
|
4813
|
+
if (hovered === null) return;
|
|
4814
|
+
const related = event.relatedTarget;
|
|
4815
|
+
if (related !== null && hovered.contains(related)) return;
|
|
4816
|
+
hovered.classList.remove("topo-hover-node");
|
|
4817
|
+
hoveredRef.current = null;
|
|
4818
|
+
container.style.cursor = "";
|
|
4819
|
+
};
|
|
4820
|
+
container.addEventListener("mouseover", onMouseOver);
|
|
4821
|
+
container.addEventListener("mouseout", onMouseOut);
|
|
4822
|
+
return () => {
|
|
4823
|
+
container.removeEventListener("mouseover", onMouseOver);
|
|
4824
|
+
container.removeEventListener("mouseout", onMouseOut);
|
|
4825
|
+
};
|
|
4826
|
+
}, [svg]);
|
|
4827
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4828
|
+
ref: containerRef,
|
|
4829
|
+
className: TopologyGraphView_module_css_default.graphImg,
|
|
4830
|
+
role: "img",
|
|
4831
|
+
"aria-label": alt,
|
|
4832
|
+
style: {
|
|
4833
|
+
opacity: fitted ? 1 : 0,
|
|
4834
|
+
transform: `translate(${view.x}px, ${view.y}px) scale(${view.k})`
|
|
4835
|
+
},
|
|
4836
|
+
dangerouslySetInnerHTML: { __html: svg }
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4652
4839
|
const MIN_K = .05;
|
|
4653
4840
|
/** A fit never shrinks below this scale, so the default and reset land at actual size. */
|
|
4654
4841
|
const MIN_FIT_K = 1;
|
|
@@ -4673,13 +4860,16 @@ function fitView(containerWidth, containerHeight, naturalWidth, naturalHeight) {
|
|
|
4673
4860
|
};
|
|
4674
4861
|
}
|
|
4675
4862
|
/**
|
|
4676
|
-
* Pan/zoom SVG viewer
|
|
4677
|
-
* drag pans, double-click zooms toward the
|
|
4678
|
-
* (fit / zoom-out / zoom-level / zoom-in)
|
|
4679
|
-
*
|
|
4680
|
-
*
|
|
4863
|
+
* Pan/zoom SVG viewer with drill-down interaction. Fit-and-center on load,
|
|
4864
|
+
* wheel zooms toward the cursor, drag pans, double-click zooms toward the
|
|
4865
|
+
* cursor, and a compact floating pill (fit / zoom-out / zoom-level / zoom-in)
|
|
4866
|
+
* collapses to just the fit control at rest.
|
|
4867
|
+
*
|
|
4868
|
+
* Hovering a node raises a ring; tapping it fires onNodeClick with viewport
|
|
4869
|
+
* coordinates and the drawn deg⁺/deg⁻ counts. Tapping empty space fires
|
|
4870
|
+
* onEmptyClick. `direction` lights only the chosen dependency direction.
|
|
4681
4871
|
*/
|
|
4682
|
-
function TopologyGraphView({ svg, alt, height, transform: transform$1, onTransformChange, zoomInLabel, zoomOutLabel, resetViewLabel, zoomLevelLabel }) {
|
|
4872
|
+
function TopologyGraphView({ svg, alt, height, transform: transform$1, onTransformChange, zoomInLabel, zoomOutLabel, resetViewLabel, zoomLevelLabel, onNodeClick, onEmptyClick, selectedKey, selectedDotId, direction }) {
|
|
4683
4873
|
const containerRef = (0, react.useRef)(null);
|
|
4684
4874
|
const naturalRef = (0, react.useRef)(null);
|
|
4685
4875
|
const dragRef = (0, react.useRef)(null);
|
|
@@ -4750,15 +4940,23 @@ function TopologyGraphView({ svg, alt, height, transform: transform$1, onTransfo
|
|
|
4750
4940
|
});
|
|
4751
4941
|
setAtFit(false);
|
|
4752
4942
|
}, [onTransformChange]);
|
|
4753
|
-
|
|
4754
|
-
const
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4943
|
+
(0, react.useEffect)(() => {
|
|
4944
|
+
const match = svg.match(/viewBox="[\d.]+ [\d.]+ ([\d.]+) ([\d.]+)"/);
|
|
4945
|
+
if (match !== null) {
|
|
4946
|
+
naturalRef.current = {
|
|
4947
|
+
width: parseFloat(match[1] ?? "0"),
|
|
4948
|
+
height: parseFloat(match[2] ?? "0")
|
|
4949
|
+
};
|
|
4950
|
+
if (transform$1 === null) requestAnimationFrame(() => {
|
|
4951
|
+
fit();
|
|
4952
|
+
});
|
|
4953
|
+
setFitted(true);
|
|
4954
|
+
}
|
|
4955
|
+
}, [
|
|
4956
|
+
svg,
|
|
4957
|
+
transform$1,
|
|
4958
|
+
fit
|
|
4959
|
+
]);
|
|
4762
4960
|
(0, react.useEffect)(() => {
|
|
4763
4961
|
const element = containerRef.current;
|
|
4764
4962
|
if (element === null) return;
|
|
@@ -4860,22 +5058,46 @@ function TopologyGraphView({ svg, alt, height, transform: transform$1, onTransfo
|
|
|
4860
5058
|
startX: event.clientX,
|
|
4861
5059
|
startY: event.clientY,
|
|
4862
5060
|
baseX: view.x,
|
|
4863
|
-
baseY: view.y
|
|
5061
|
+
baseY: view.y,
|
|
5062
|
+
moved: false
|
|
4864
5063
|
};
|
|
4865
5064
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
4866
5065
|
};
|
|
4867
5066
|
const onPointerMove = (event) => {
|
|
4868
5067
|
const drag = dragRef.current;
|
|
4869
5068
|
if (drag === null || drag.pointerId !== event.pointerId) return;
|
|
5069
|
+
const dx = event.clientX - drag.startX;
|
|
5070
|
+
const dy = event.clientY - drag.startY;
|
|
5071
|
+
if (!drag.moved && Math.abs(dx) < 3 && Math.abs(dy) < 3) return;
|
|
5072
|
+
drag.moved = true;
|
|
4870
5073
|
commit({
|
|
4871
5074
|
...view,
|
|
4872
|
-
x: drag.baseX +
|
|
4873
|
-
y: drag.baseY +
|
|
5075
|
+
x: drag.baseX + dx,
|
|
5076
|
+
y: drag.baseY + dy
|
|
4874
5077
|
});
|
|
4875
5078
|
setAtFit(false);
|
|
4876
5079
|
};
|
|
4877
5080
|
const onPointerEnd = (event) => {
|
|
4878
|
-
|
|
5081
|
+
const drag = dragRef.current;
|
|
5082
|
+
if (drag === null || drag.pointerId !== event.pointerId) return;
|
|
5083
|
+
if (!drag.moved) {
|
|
5084
|
+
const found = nodeRefFromElement(document.elementFromPoint(event.clientX, event.clientY));
|
|
5085
|
+
const container = containerRef.current;
|
|
5086
|
+
if (found !== null && container !== null && onNodeClick !== void 0) {
|
|
5087
|
+
const rect = container.getBoundingClientRect();
|
|
5088
|
+
const stats = edgeStats(graphScope(found.group), found.ref.dotId);
|
|
5089
|
+
onNodeClick({
|
|
5090
|
+
key: found.ref.key,
|
|
5091
|
+
nodeId: found.ref.dotId,
|
|
5092
|
+
label: found.ref.label,
|
|
5093
|
+
anchorX: (event.clientX - rect.left - view.x) / view.k,
|
|
5094
|
+
anchorY: (event.clientY - rect.top - view.y) / view.k,
|
|
5095
|
+
outCount: stats.out,
|
|
5096
|
+
inCount: stats.in
|
|
5097
|
+
});
|
|
5098
|
+
} else if (found === null && onEmptyClick !== void 0) onEmptyClick();
|
|
5099
|
+
}
|
|
5100
|
+
dragRef.current = null;
|
|
4879
5101
|
};
|
|
4880
5102
|
const onDoubleClick = (event) => {
|
|
4881
5103
|
const element = containerRef.current;
|
|
@@ -4898,7 +5120,6 @@ function TopologyGraphView({ svg, alt, height, transform: transform$1, onTransfo
|
|
|
4898
5120
|
} else fit();
|
|
4899
5121
|
};
|
|
4900
5122
|
const collapsed = atFit && !panelHovered;
|
|
4901
|
-
const src = `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
|
4902
5123
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4903
5124
|
className: TopologyGraphView_module_css_default.graphBox,
|
|
4904
5125
|
style: { height },
|
|
@@ -4916,16 +5137,14 @@ function TopologyGraphView({ svg, alt, height, transform: transform$1, onTransfo
|
|
|
4916
5137
|
onPointerLeave: () => {
|
|
4917
5138
|
setViewportHovered(false);
|
|
4918
5139
|
},
|
|
4919
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
4920
|
-
|
|
4921
|
-
src,
|
|
5140
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SvgCanvas, {
|
|
5141
|
+
svg,
|
|
4922
5142
|
alt,
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
}
|
|
5143
|
+
fitted,
|
|
5144
|
+
view,
|
|
5145
|
+
selectedKey: selectedKey ?? null,
|
|
5146
|
+
selectedDotId: selectedDotId ?? null,
|
|
5147
|
+
direction: direction ?? null
|
|
4929
5148
|
})
|
|
4930
5149
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4931
5150
|
className: collapsed ? `${TopologyGraphView_module_css_default.zoomControls} ${TopologyGraphView_module_css_default.collapsed}` : TopologyGraphView_module_css_default.zoomControls,
|
|
@@ -4977,9 +5196,215 @@ function TopologyGraphView({ svg, alt, height, transform: transform$1, onTransfo
|
|
|
4977
5196
|
});
|
|
4978
5197
|
}
|
|
4979
5198
|
|
|
5199
|
+
//#endregion
|
|
5200
|
+
//#region \0dsh-css:/Users/jiangyuan/Documents/side-work/dsh-plugin-topology/src/client/NodePopover.module.css.mjs
|
|
5201
|
+
const css$2 = ".iw1Z3a_popover{z-index:10;border:1px solid var(--dsw-alias-border-l3,#0000001f);background:var(--dsw-alias-bg-layer-2,#fff);pointer-events:auto;border-radius:8px;flex-direction:column;gap:6px;min-width:180px;max-width:280px;padding:10px 12px;font-size:12px;display:flex;position:absolute;box-shadow:0 8px 24px #0000001f}.iw1Z3a_header{justify-content:space-between;align-items:flex-start;gap:8px;display:flex}.iw1Z3a_nameLink{color:var(--dsw-alias-state-business-primary,#4176e6);word-break:break-word;font-size:13px;font-weight:600;line-height:1.3;text-decoration:none}.iw1Z3a_nameLink:hover{text-decoration:underline}.iw1Z3a_namePlain{color:var(--dsw-alias-label-primary,#0f172a);word-break:break-word;font-size:13px;font-weight:600;line-height:1.3}.iw1Z3a_closeButton{width:18px;height:18px;color:var(--dsw-alias-label-secondary,#61666b);cursor:pointer;background:0 0;border:none;border-radius:4px;flex-shrink:0;justify-content:center;align-items:center;padding:0;display:inline-flex}.iw1Z3a_closeButton:hover{background:var(--dsw-alias-interactive-bg-hover,#2631480f);color:var(--dsw-alias-label-primary,#0f1115)}.iw1Z3a_metaRow{align-items:center;gap:6px;display:flex}.iw1Z3a_stateBadge{border-radius:4px;width:fit-content;padding:1px 6px;font-size:10px;font-weight:500;line-height:1.5;display:inline-block}.iw1Z3a_stateActive{background:var(--dsw-alias-state-business-tertiary,#e4edfd);color:var(--dsw-alias-state-business-primary,#4176e6)}.iw1Z3a_stateFailed{background:var(--dsw-alias-interactive-bg-hover-danger,#ec13130d);color:var(--dsw-alias-state-error-primary,#ec1313)}.iw1Z3a_stateOther{background:var(--dsw-alias-interactive-bg-hover,#2631480f);color:var(--dsw-alias-label-secondary,#61666b)}.iw1Z3a_row{align-items:baseline;gap:6px;display:flex}.iw1Z3a_rowLabel{color:var(--dsw-alias-label-secondary,#61666b);white-space:nowrap;font-size:11px}.iw1Z3a_rowValue{color:var(--dsw-alias-label-primary,#0f1115);word-break:break-all;font-size:11px}.iw1Z3a_sourceValue{font-family:var(--ds-font-family-code,monospace);font-size:10px}.iw1Z3a_depPills{gap:6px;margin-top:2px;display:flex}.iw1Z3a_depPill{border:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-interactive-bg-hover,#2631480f);color:var(--dsw-alias-label-secondary,#61666b);cursor:pointer;border-radius:999px;align-items:center;gap:5px;padding:2px 9px;transition:background .12s,border-color .12s,color .12s;display:inline-flex}.iw1Z3a_depPill:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary,#4176e6);color:var(--dsw-alias-state-business-primary,#4176e6)}.iw1Z3a_depPill:disabled{opacity:.4;cursor:default}.iw1Z3a_depPillActive,.iw1Z3a_depPillActive:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary,#4176e6);background:color-mix(in srgb, var(--dsw-alias-state-business-primary,#4176e6) 14%, transparent);color:var(--dsw-alias-state-business-primary,#4176e6)}.iw1Z3a_degMark{font-size:11px;line-height:1}.iw1Z3a_degValue{font-variant-numeric:tabular-nums;font-size:11px;font-weight:600;line-height:1}.iw1Z3a_unresolvedBadge{background:var(--dsw-alias-interactive-bg-hover-danger,#ec13130d);color:var(--dsw-alias-state-error-primary,#ec1313);border-radius:3px;padding:1px 5px;font-size:10px;font-weight:500;display:inline-block}";
|
|
5202
|
+
const tagId$2 = "dsh-plugin-topology/NodePopover.module.css";
|
|
5203
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
|
|
5204
|
+
const tag = document.createElement("style");
|
|
5205
|
+
tag.dataset.plugin = "dsh-plugin-topology";
|
|
5206
|
+
tag.dataset.pluginCss = tagId$2;
|
|
5207
|
+
tag.textContent = css$2;
|
|
5208
|
+
document.head.appendChild(tag);
|
|
5209
|
+
}
|
|
5210
|
+
var NodePopover_module_css_default = {
|
|
5211
|
+
"closeButton": "iw1Z3a_closeButton",
|
|
5212
|
+
"degMark": "iw1Z3a_degMark",
|
|
5213
|
+
"degValue": "iw1Z3a_degValue",
|
|
5214
|
+
"depPill": "iw1Z3a_depPill",
|
|
5215
|
+
"depPillActive": "iw1Z3a_depPillActive",
|
|
5216
|
+
"depPills": "iw1Z3a_depPills",
|
|
5217
|
+
"header": "iw1Z3a_header",
|
|
5218
|
+
"metaRow": "iw1Z3a_metaRow",
|
|
5219
|
+
"nameLink": "iw1Z3a_nameLink",
|
|
5220
|
+
"namePlain": "iw1Z3a_namePlain",
|
|
5221
|
+
"popover": "iw1Z3a_popover",
|
|
5222
|
+
"row": "iw1Z3a_row",
|
|
5223
|
+
"rowLabel": "iw1Z3a_rowLabel",
|
|
5224
|
+
"rowValue": "iw1Z3a_rowValue",
|
|
5225
|
+
"sourceValue": "iw1Z3a_sourceValue",
|
|
5226
|
+
"stateActive": "iw1Z3a_stateActive",
|
|
5227
|
+
"stateBadge": "iw1Z3a_stateBadge",
|
|
5228
|
+
"stateFailed": "iw1Z3a_stateFailed",
|
|
5229
|
+
"stateOther": "iw1Z3a_stateOther",
|
|
5230
|
+
"unresolvedBadge": "iw1Z3a_unresolvedBadge"
|
|
5231
|
+
};
|
|
5232
|
+
|
|
5233
|
+
//#endregion
|
|
5234
|
+
//#region src/client/NodePopover.tsx
|
|
5235
|
+
/** Gap between the anchor and the card's top-left corner, in px. */
|
|
5236
|
+
const OFFSET = 8;
|
|
5237
|
+
/** Build an npm registry URL from a source specifier, or null for local paths. */
|
|
5238
|
+
function npmUrl(source) {
|
|
5239
|
+
if (source === void 0) return null;
|
|
5240
|
+
if (source.startsWith("/") || source.startsWith(".") || source.includes(":\\")) return null;
|
|
5241
|
+
return `https://www.npmjs.com/package/${encodeURIComponent(source)}`;
|
|
5242
|
+
}
|
|
5243
|
+
function stateBadgeClass(state) {
|
|
5244
|
+
if (state === "ACTIVE") return `${NodePopover_module_css_default.stateBadge} ${NodePopover_module_css_default.stateActive}`;
|
|
5245
|
+
if (state === "FAILED") return `${NodePopover_module_css_default.stateBadge} ${NodePopover_module_css_default.stateFailed}`;
|
|
5246
|
+
return `${NodePopover_module_css_default.stateBadge} ${NodePopover_module_css_default.stateOther}`;
|
|
5247
|
+
}
|
|
5248
|
+
/** The worst lifecycle state in a group, so a broken instance is never hidden. */
|
|
5249
|
+
function worstState(states) {
|
|
5250
|
+
if (states.includes("FAILED")) return "FAILED";
|
|
5251
|
+
if (states.includes("PENDING")) return "PENDING";
|
|
5252
|
+
return states[0] ?? "ACTIVE";
|
|
5253
|
+
}
|
|
5254
|
+
/**
|
|
5255
|
+
* Resolve the runtime facts the panel shows. Single instances map straight to a
|
|
5256
|
+
* snapshot node; merged render nodes (`m:N`, several same-named instances drawn
|
|
5257
|
+
* as one) are recovered from the ordinals in the rendered label.
|
|
5258
|
+
*/
|
|
5259
|
+
function resolveNode(nodeId, svgLabel, analysis) {
|
|
5260
|
+
const direct = analysis.collapsed.nodes.find((node) => node.id === nodeId);
|
|
5261
|
+
if (direct !== void 0) return {
|
|
5262
|
+
node: direct,
|
|
5263
|
+
unresolved: analysis.graph.unresolved.filter((dep) => dep.plugin === nodeId).length
|
|
5264
|
+
};
|
|
5265
|
+
const bracket = svgLabel.match(/\[([^\]]*)\]\s*$/);
|
|
5266
|
+
const baseName = svgLabel.replace(/\s*\[[^\]]*\]\s*$/, "").trim();
|
|
5267
|
+
const ordinals = bracket === null ? [] : bracket[1].split(",").map((part) => part.trim()).filter((part) => part !== "" && part !== "…");
|
|
5268
|
+
let members = analysis.collapsed.nodes.filter((node) => node.label === baseName);
|
|
5269
|
+
if (ordinals.length > 0) {
|
|
5270
|
+
const wanted = new Set(ordinals.map((ordinal) => `p:${ordinal}`));
|
|
5271
|
+
const matched = members.filter((node) => wanted.has(node.id));
|
|
5272
|
+
if (matched.length > 0) members = matched;
|
|
5273
|
+
}
|
|
5274
|
+
const source = members.find((member) => member.source !== void 0)?.source;
|
|
5275
|
+
return {
|
|
5276
|
+
node: {
|
|
5277
|
+
id: nodeId,
|
|
5278
|
+
kind: "plugin",
|
|
5279
|
+
label: baseName === "" ? svgLabel : baseName,
|
|
5280
|
+
state: worstState(members.map((member) => member.state)),
|
|
5281
|
+
parent: null,
|
|
5282
|
+
inject: [],
|
|
5283
|
+
...source === void 0 ? {} : { source }
|
|
5284
|
+
},
|
|
5285
|
+
unresolved: members.reduce((total, member) => total + analysis.graph.unresolved.filter((dep) => dep.plugin === member.id).length, 0)
|
|
5286
|
+
};
|
|
5287
|
+
}
|
|
5288
|
+
/**
|
|
5289
|
+
* Floating detail card anchored to a clicked graph node. It shows only facts
|
|
5290
|
+
* that need the running process to know (state, load source, drawn in/out
|
|
5291
|
+
* degree) and links the name out to npm for everything else. The deg⁺/deg⁻
|
|
5292
|
+
* pills drive the graph highlight, so the panel itself stays a one-screen read.
|
|
5293
|
+
*/
|
|
5294
|
+
function NodePopover({ popover, analysis, direction, onToggleDirection, onClose, t }) {
|
|
5295
|
+
const ref = (0, react.useRef)(null);
|
|
5296
|
+
const { node, unresolved } = resolveNode(popover.nodeId, popover.label, analysis);
|
|
5297
|
+
const url = npmUrl(node.source);
|
|
5298
|
+
(0, react.useEffect)(() => {
|
|
5299
|
+
const el = ref.current;
|
|
5300
|
+
if (el === null) return;
|
|
5301
|
+
const parent = el.parentElement;
|
|
5302
|
+
if (parent === null) return;
|
|
5303
|
+
const bounds = parent.getBoundingClientRect();
|
|
5304
|
+
const own = el.getBoundingClientRect();
|
|
5305
|
+
const maxLeft = Math.max(OFFSET, bounds.width - own.width - OFFSET);
|
|
5306
|
+
const maxTop = Math.max(OFFSET, bounds.height - own.height - OFFSET);
|
|
5307
|
+
el.style.left = `${Math.min(Math.max(OFFSET, popover.x + OFFSET), maxLeft)}px`;
|
|
5308
|
+
el.style.top = `${Math.min(Math.max(OFFSET, popover.y + OFFSET), maxTop)}px`;
|
|
5309
|
+
}, [popover.x, popover.y]);
|
|
5310
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5311
|
+
ref,
|
|
5312
|
+
className: NodePopover_module_css_default.popover,
|
|
5313
|
+
style: {
|
|
5314
|
+
left: popover.x + OFFSET,
|
|
5315
|
+
top: popover.y + OFFSET
|
|
5316
|
+
},
|
|
5317
|
+
role: "dialog",
|
|
5318
|
+
"aria-label": node.label,
|
|
5319
|
+
children: [
|
|
5320
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5321
|
+
className: NodePopover_module_css_default.header,
|
|
5322
|
+
children: [url !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
5323
|
+
className: NodePopover_module_css_default.nameLink,
|
|
5324
|
+
href: url,
|
|
5325
|
+
target: "_blank",
|
|
5326
|
+
rel: "noopener noreferrer",
|
|
5327
|
+
children: node.label
|
|
5328
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5329
|
+
className: NodePopover_module_css_default.namePlain,
|
|
5330
|
+
children: node.label
|
|
5331
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5332
|
+
type: "button",
|
|
5333
|
+
className: NodePopover_module_css_default.closeButton,
|
|
5334
|
+
onClick: onClose,
|
|
5335
|
+
"aria-label": t("popoverClose"),
|
|
5336
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCloseOutline16, { size: 12 })
|
|
5337
|
+
})]
|
|
5338
|
+
}),
|
|
5339
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5340
|
+
className: NodePopover_module_css_default.metaRow,
|
|
5341
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5342
|
+
className: stateBadgeClass(node.state),
|
|
5343
|
+
children: node.state
|
|
5344
|
+
}), unresolved > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5345
|
+
className: NodePopover_module_css_default.unresolvedBadge,
|
|
5346
|
+
children: [
|
|
5347
|
+
t("popoverUnresolved"),
|
|
5348
|
+
" ",
|
|
5349
|
+
unresolved
|
|
5350
|
+
]
|
|
5351
|
+
})]
|
|
5352
|
+
}),
|
|
5353
|
+
node.source !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5354
|
+
className: NodePopover_module_css_default.row,
|
|
5355
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5356
|
+
className: NodePopover_module_css_default.rowLabel,
|
|
5357
|
+
children: t("popoverSource")
|
|
5358
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5359
|
+
className: `${NodePopover_module_css_default.rowValue} ${NodePopover_module_css_default.sourceValue}`,
|
|
5360
|
+
children: node.source
|
|
5361
|
+
})]
|
|
5362
|
+
}),
|
|
5363
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5364
|
+
className: NodePopover_module_css_default.depPills,
|
|
5365
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5366
|
+
type: "button",
|
|
5367
|
+
className: direction === "out" ? `${NodePopover_module_css_default.depPill} ${NodePopover_module_css_default.depPillActive}` : NodePopover_module_css_default.depPill,
|
|
5368
|
+
"aria-pressed": direction === "out",
|
|
5369
|
+
"aria-label": t("popoverOutgoing"),
|
|
5370
|
+
title: t("popoverOutgoing"),
|
|
5371
|
+
disabled: popover.outCount === 0,
|
|
5372
|
+
onClick: () => {
|
|
5373
|
+
onToggleDirection("out");
|
|
5374
|
+
},
|
|
5375
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5376
|
+
className: NodePopover_module_css_default.degMark,
|
|
5377
|
+
children: "deg⁺"
|
|
5378
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5379
|
+
className: NodePopover_module_css_default.degValue,
|
|
5380
|
+
children: popover.outCount
|
|
5381
|
+
})]
|
|
5382
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5383
|
+
type: "button",
|
|
5384
|
+
className: direction === "in" ? `${NodePopover_module_css_default.depPill} ${NodePopover_module_css_default.depPillActive}` : NodePopover_module_css_default.depPill,
|
|
5385
|
+
"aria-pressed": direction === "in",
|
|
5386
|
+
"aria-label": t("popoverIncoming"),
|
|
5387
|
+
title: t("popoverIncoming"),
|
|
5388
|
+
disabled: popover.inCount === 0,
|
|
5389
|
+
onClick: () => {
|
|
5390
|
+
onToggleDirection("in");
|
|
5391
|
+
},
|
|
5392
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5393
|
+
className: NodePopover_module_css_default.degMark,
|
|
5394
|
+
children: "deg⁻"
|
|
5395
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5396
|
+
className: NodePopover_module_css_default.degValue,
|
|
5397
|
+
children: popover.inCount
|
|
5398
|
+
})]
|
|
5399
|
+
})]
|
|
5400
|
+
})
|
|
5401
|
+
]
|
|
5402
|
+
});
|
|
5403
|
+
}
|
|
5404
|
+
|
|
4980
5405
|
//#endregion
|
|
4981
5406
|
//#region \0dsh-css:/Users/jiangyuan/Documents/side-work/dsh-plugin-topology/src/client/TopologyView.module.css.mjs
|
|
4982
|
-
const css$1 = ".rve-MG_root{box-sizing:border-box;width:100%;height:100%;min-height:0;padding:
|
|
5407
|
+
const css$1 = ".rve-MG_root{box-sizing:border-box;flex-direction:column;gap:12px;width:100%;height:100%;min-height:0;padding:16px;display:flex;overflow:hidden}.rve-MG_status{color:var(--dsw-alias-label-secondary,#61666b);margin:0}.rve-MG_failure{flex-direction:column;align-items:flex-start;gap:8px;display:flex}.rve-MG_errorDetail{background:var(--dsw-alias-interactive-bg-hover,#2631480f);max-width:100%;font-family:var(--ds-font-family-code,monospace);color:var(--dsw-alias-state-error-primary,#dc2626);white-space:pre-wrap;word-break:break-all;border-radius:6px;margin:0;padding:8px 10px;font-size:11px;line-height:1.5;overflow:auto}.rve-MG_toolbar{justify-content:space-between;align-items:center;gap:12px;display:flex}.rve-MG_summaryGroup{align-items:center;gap:6px;min-width:0;display:flex}.rve-MG_actions{flex-wrap:wrap;flex-shrink:0;justify-content:flex-end;align-items:center;gap:8px;display:flex}.rve-MG_rankdirGroup{border:1px solid var(--dsw-alias-border-l3,#0000001f);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:6px;align-items:center;gap:0;padding:2px;display:inline-flex}.rve-MG_rankdirOption{min-width:32px;height:22px;color:var(--dsw-alias-label-secondary,#61666b);text-align:center;cursor:pointer;background:0 0;border:none;border-radius:4px;padding:0 8px;font-size:11px}.rve-MG_rankdirOption[aria-pressed=true]{background:var(--dsw-alias-state-business-primary,#3b82f6);color:var(--dsw-alias-label-primary-foreground,#fff);font-weight:500}.rve-MG_metricStrip{flex-wrap:wrap;align-items:center;gap:6px 20px;padding:2px 0;display:flex}.rve-MG_stat{align-items:center;gap:6px;display:inline-flex}.rve-MG_statLabel{color:var(--dsw-alias-label-secondary,#61666b);font-size:12px}.rve-MG_statValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#0f1115);font-size:15px;font-weight:600;line-height:1}.rve-MG_statAlert .rve-MG_statValue{color:var(--dsw-alias-state-error-primary,#dc2626)}.rve-MG_statToggle{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;padding:0;display:inline-flex}.rve-MG_statChevron{color:var(--dsw-alias-label-secondary,#61666b);transition:transform .15s}.rve-MG_statChevronOpen{transform:rotate(180deg)}.rve-MG_unresolvedPanel{border:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:8px;overflow:hidden}.rve-MG_unresolvedScroll{max-height:260px;overflow-y:auto}.rve-MG_unresolvedTable{table-layout:fixed;border-collapse:collapse;width:100%;font-size:12px}.rve-MG_unresolvedTable th,.rve-MG_unresolvedTable td{text-align:left;padding:8px 12px}.rve-MG_unresolvedTable thead th{z-index:1;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#61666b);border-bottom:1px solid var(--dsw-alias-border-l2,#0000001a);font-size:11px;font-weight:500;position:sticky;top:0}.rve-MG_unresolvedTable tbody tr+tr td{border-top:1px solid var(--dsw-alias-border-l2,#0000001a)}.rve-MG_unresolvedTable .rve-MG_unresolvedColPlugin{width:180px}.rve-MG_unresolvedTable .rve-MG_unresolvedColState{width:110px}.rve-MG_unresolvedPlugin{font-family:var(--ds-font-family-code,monospace);color:var(--dsw-alias-label-primary,#0f1115);text-overflow:ellipsis;white-space:nowrap;font-weight:500;overflow:hidden}.rve-MG_unresolvedState{color:var(--dsw-alias-label-secondary,#61666b);white-space:nowrap;font-size:11px}.rve-MG_unresolvedService{font-family:var(--ds-font-family-code,monospace);color:var(--dsw-alias-state-error-primary,#dc2626);overflow-wrap:anywhere}.rve-MG_legend{color:var(--dsw-alias-label-secondary,#61666b);flex-wrap:wrap;align-items:center;gap:6px 16px;font-size:11px;display:flex}.rve-MG_legendItem{align-items:center;gap:6px;display:inline-flex}.rve-MG_legendSwatch{flex:none;display:inline-block}.rve-MG_legendUnresolvedBox{border:1.5px solid var(--dsw-alias-state-error-primary,#dc2626);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:50%;width:12px;height:12px}.rve-MG_legendIsolatedBox{border:1px dashed var(--dsw-alias-label-caption,#94a3b8);background:var(--dsw-alias-bg-base,#f8fafc);border-radius:2px;width:12px;height:12px}.rve-MG_legendIdSample{font-family:var(--ds-font-family-code,monospace);color:var(--dsw-alias-label-secondary,#61666b)}.rve-MG_body{flex:1;min-height:0;position:relative}";
|
|
4983
5408
|
const tagId$1 = "dsh-plugin-topology/TopologyView.module.css";
|
|
4984
5409
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
4985
5410
|
const tag = document.createElement("style");
|
|
@@ -5039,11 +5464,22 @@ const DOWNLOAD_FORMATS = [
|
|
|
5039
5464
|
labelKey: "downloadJson"
|
|
5040
5465
|
}
|
|
5041
5466
|
];
|
|
5467
|
+
/** Detect the current DSH theme from the document root's data attribute or class. */
|
|
5468
|
+
function detectTheme() {
|
|
5469
|
+
const dataTheme = document.documentElement.getAttribute("data-theme");
|
|
5470
|
+
if (dataTheme === "dark") return "dark";
|
|
5471
|
+
if (dataTheme === "light") return "light";
|
|
5472
|
+
const bg = getComputedStyle(document.body).backgroundColor;
|
|
5473
|
+
if (bg !== "" && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
|
5474
|
+
const match = bg.match(/(\d+)/g);
|
|
5475
|
+
if (match !== null && match.length >= 3) return (parseInt(match[0] ?? "255") * 299 + parseInt(match[1] ?? "255") * 587 + parseInt(match[2] ?? "255") * 114) / 1e3 < 128 ? "dark" : "light";
|
|
5476
|
+
}
|
|
5477
|
+
if (typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches) {}
|
|
5478
|
+
return "light";
|
|
5479
|
+
}
|
|
5042
5480
|
/**
|
|
5043
5481
|
* The health-oriented metrics: scale, the broken-dependency signal, and the
|
|
5044
|
-
* three coupling/depth/fragmentation facts.
|
|
5045
|
-
* descriptive counts (average path, source/sink) are omitted — the former is
|
|
5046
|
-
* constant on an inject/provide graph, the latter carry no health signal.
|
|
5482
|
+
* three coupling/depth/fragmentation facts.
|
|
5047
5483
|
*/
|
|
5048
5484
|
function metricRows(analysis, t) {
|
|
5049
5485
|
const m = analysis.graphMetrics;
|
|
@@ -5078,8 +5514,8 @@ function metricRows(analysis, t) {
|
|
|
5078
5514
|
];
|
|
5079
5515
|
}
|
|
5080
5516
|
/** Trigger a browser download of the rendered document. */
|
|
5081
|
-
async function download(format, rankdir, render) {
|
|
5082
|
-
const content = await render(format, rankdir);
|
|
5517
|
+
async function download(format, rankdir, theme, render) {
|
|
5518
|
+
const content = await render(format, rankdir, theme);
|
|
5083
5519
|
const mime = format === "json" ? "application/json" : format === "dot" ? "text/vnd.graphviz" : "image/svg+xml";
|
|
5084
5520
|
const blob = new Blob([content], { type: mime });
|
|
5085
5521
|
const url = URL.createObjectURL(blob);
|
|
@@ -5091,7 +5527,7 @@ async function download(format, rankdir, render) {
|
|
|
5091
5527
|
anchor.remove();
|
|
5092
5528
|
URL.revokeObjectURL(url);
|
|
5093
5529
|
}
|
|
5094
|
-
/** Group unresolved dependencies per plugin
|
|
5530
|
+
/** Group unresolved dependencies per plugin. */
|
|
5095
5531
|
function unresolvedRows(analysis) {
|
|
5096
5532
|
const labels = new Map(analysis.graph.nodes.filter((node) => node.kind === "plugin").map((node) => [node.id, node.label]));
|
|
5097
5533
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -5122,10 +5558,40 @@ function TopologyView({ useStore, actions, analyze, render, t }) {
|
|
|
5122
5558
|
const [state, setState] = (0, react.useState)({ status: "loading" });
|
|
5123
5559
|
const [unresolvedOpen, setUnresolvedOpen] = (0, react.useState)(false);
|
|
5124
5560
|
const [rankdir, setRankdir] = (0, react.useState)("LR");
|
|
5561
|
+
const [theme, setTheme] = (0, react.useState)(detectTheme);
|
|
5562
|
+
const [anchor, setAnchor] = (0, react.useState)(null);
|
|
5563
|
+
const [direction, setDirection] = (0, react.useState)(null);
|
|
5125
5564
|
const transform$1 = useStore((store) => store.transform);
|
|
5565
|
+
const view = transform$1 ?? {
|
|
5566
|
+
x: 0,
|
|
5567
|
+
y: 0,
|
|
5568
|
+
k: 1
|
|
5569
|
+
};
|
|
5570
|
+
const popover = anchor === null ? null : {
|
|
5571
|
+
...anchor,
|
|
5572
|
+
x: view.x + anchor.anchorX * view.k,
|
|
5573
|
+
y: view.y + anchor.anchorY * view.k
|
|
5574
|
+
};
|
|
5575
|
+
(0, react.useEffect)(() => {
|
|
5576
|
+
const observer = new MutationObserver(() => {
|
|
5577
|
+
const next = detectTheme();
|
|
5578
|
+
setTheme((prev) => prev === next ? prev : next);
|
|
5579
|
+
});
|
|
5580
|
+
observer.observe(document.documentElement, {
|
|
5581
|
+
attributes: true,
|
|
5582
|
+
attributeFilter: [
|
|
5583
|
+
"data-theme",
|
|
5584
|
+
"class",
|
|
5585
|
+
"style"
|
|
5586
|
+
]
|
|
5587
|
+
});
|
|
5588
|
+
return () => {
|
|
5589
|
+
observer.disconnect();
|
|
5590
|
+
};
|
|
5591
|
+
}, []);
|
|
5126
5592
|
(0, react.useEffect)(() => {
|
|
5127
5593
|
let current = true;
|
|
5128
|
-
Promise.all([analyze(), render("svg", rankdir)]).then(([analysis, svg]) => {
|
|
5594
|
+
Promise.all([analyze(), render("svg", rankdir, theme)]).then(([analysis, svg]) => {
|
|
5129
5595
|
if (current) setState({
|
|
5130
5596
|
status: "ready",
|
|
5131
5597
|
analysis,
|
|
@@ -5145,6 +5611,7 @@ function TopologyView({ useStore, actions, analyze, render, t }) {
|
|
|
5145
5611
|
}, [
|
|
5146
5612
|
request,
|
|
5147
5613
|
rankdir,
|
|
5614
|
+
theme,
|
|
5148
5615
|
analyze,
|
|
5149
5616
|
render
|
|
5150
5617
|
]);
|
|
@@ -5160,6 +5627,33 @@ function TopologyView({ useStore, actions, analyze, render, t }) {
|
|
|
5160
5627
|
actions.setTransform(null);
|
|
5161
5628
|
setRankdir(next);
|
|
5162
5629
|
};
|
|
5630
|
+
const onNodeClick = (0, react.useCallback)((info) => {
|
|
5631
|
+
setDirection(null);
|
|
5632
|
+
setAnchor((prev) => {
|
|
5633
|
+
if (prev !== null && prev.nodeId === info.nodeId) return null;
|
|
5634
|
+
return {
|
|
5635
|
+
key: info.key,
|
|
5636
|
+
nodeId: info.nodeId,
|
|
5637
|
+
label: info.label,
|
|
5638
|
+
anchorX: info.anchorX,
|
|
5639
|
+
anchorY: info.anchorY,
|
|
5640
|
+
outCount: info.outCount,
|
|
5641
|
+
inCount: info.inCount
|
|
5642
|
+
};
|
|
5643
|
+
});
|
|
5644
|
+
}, []);
|
|
5645
|
+
const onEmptyClick = (0, react.useCallback)(() => {
|
|
5646
|
+
setAnchor(null);
|
|
5647
|
+
setDirection(null);
|
|
5648
|
+
}, []);
|
|
5649
|
+
const closePopover = (0, react.useCallback)(() => {
|
|
5650
|
+
setAnchor(null);
|
|
5651
|
+
setDirection(null);
|
|
5652
|
+
}, []);
|
|
5653
|
+
/** Tapping the active pill again drops back to plain selection. */
|
|
5654
|
+
const toggleDirection = (0, react.useCallback)((next) => {
|
|
5655
|
+
setDirection((current) => current === next ? null : next);
|
|
5656
|
+
}, []);
|
|
5163
5657
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5164
5658
|
className: TopologyView_module_css_default.root,
|
|
5165
5659
|
"data-plugin-topology-view": "",
|
|
@@ -5241,7 +5735,7 @@ function TopologyView({ useStore, actions, analyze, render, t }) {
|
|
|
5241
5735
|
}), DOWNLOAD_FORMATS.map(({ format, labelKey }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutlineButton, {
|
|
5242
5736
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconDownloadOutline16, { size: 14 }),
|
|
5243
5737
|
onClick: () => {
|
|
5244
|
-
download(format, rankdir, render);
|
|
5738
|
+
download(format, rankdir, theme, render);
|
|
5245
5739
|
},
|
|
5246
5740
|
children: t(labelKey)
|
|
5247
5741
|
}, format))]
|
|
@@ -5313,9 +5807,9 @@ function TopologyView({ useStore, actions, analyze, render, t }) {
|
|
|
5313
5807
|
})
|
|
5314
5808
|
]
|
|
5315
5809
|
}),
|
|
5316
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
5810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5317
5811
|
className: TopologyView_module_css_default.body,
|
|
5318
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TopologyGraphView, {
|
|
5812
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TopologyGraphView, {
|
|
5319
5813
|
svg: state.svg,
|
|
5320
5814
|
alt: t("title"),
|
|
5321
5815
|
height: "100%",
|
|
@@ -5324,8 +5818,20 @@ function TopologyView({ useStore, actions, analyze, render, t }) {
|
|
|
5324
5818
|
zoomInLabel: t("zoomIn"),
|
|
5325
5819
|
zoomOutLabel: t("zoomOut"),
|
|
5326
5820
|
resetViewLabel: t("resetView"),
|
|
5327
|
-
zoomLevelLabel: t("zoomLevel")
|
|
5328
|
-
|
|
5821
|
+
zoomLevelLabel: t("zoomLevel"),
|
|
5822
|
+
onNodeClick,
|
|
5823
|
+
onEmptyClick,
|
|
5824
|
+
selectedKey: popover?.key ?? null,
|
|
5825
|
+
selectedDotId: popover?.nodeId ?? null,
|
|
5826
|
+
direction
|
|
5827
|
+
}), popover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodePopover, {
|
|
5828
|
+
popover,
|
|
5829
|
+
analysis: state.analysis,
|
|
5830
|
+
direction,
|
|
5831
|
+
onToggleDirection: toggleDirection,
|
|
5832
|
+
onClose: closePopover,
|
|
5833
|
+
t
|
|
5834
|
+
})]
|
|
5329
5835
|
})
|
|
5330
5836
|
] }) : null
|
|
5331
5837
|
]
|
|
@@ -5334,7 +5840,7 @@ function TopologyView({ useStore, actions, analyze, render, t }) {
|
|
|
5334
5840
|
|
|
5335
5841
|
//#endregion
|
|
5336
5842
|
//#region \0dsh-css:/Users/jiangyuan/Documents/side-work/dsh-plugin-topology/src/client/panel.module.css.mjs
|
|
5337
|
-
const css = ".F01v1a_layer{align-items:center;width:100%;display:flex}.F01v1a_rail{justify-content:center}.F01v1a_trigger{box-sizing:border-box;cursor:pointer;width:calc(100% + 4px);height:42px;color:var(--dsw-alias-label-primary,#0f172a);background:0 0;border:none;border-radius:12px;flex:none;align-items:center;gap:8px;margin:4px -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}.F01v1a_trigger:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f5f9)}.F01v1a_triggerLabel{white-space:nowrap;overflow:hidden}.F01v1a_panel{border:1px solid var(--dsw-border,#
|
|
5843
|
+
const css = ".F01v1a_layer{align-items:center;width:100%;display:flex}.F01v1a_rail{justify-content:center}.F01v1a_trigger{box-sizing:border-box;cursor:pointer;width:calc(100% + 4px);height:42px;color:var(--dsw-alias-label-primary,#0f172a);background:0 0;border:none;border-radius:12px;flex:none;align-items:center;gap:8px;margin:4px -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}.F01v1a_trigger:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f5f9)}.F01v1a_triggerLabel{white-space:nowrap;overflow:hidden}.F01v1a_panel{border:1px solid var(--dsw-alias-border-l3,#0000001f);background:var(--dsw-alias-bg-layer-2,#fff);z-index:30;border-radius:12px;flex-direction:column;width:min(840px,100vw - 48px);height:min(680px,100vh - 48px);display:flex;position:fixed;top:50%;left:50%;overflow:hidden;transform:translate(-50%,-50%);box-shadow:0 18px 48px #0000002e}.F01v1a_maximized{width:calc(100vw - 32px);height:calc(100vh - 32px)}.F01v1a_header{border-bottom:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-bg-layer-2,#fff);justify-content:space-between;align-items:center;gap:8px;padding:10px 14px;display:flex}.F01v1a_headerActions{align-items:center;gap:4px;display:inline-flex}.F01v1a_title{color:var(--dsw-alias-label-primary,#0f1115);font-size:14px;font-weight:600}.F01v1a_close{width:24px;height:24px;color:var(--dsw-alias-label-secondary,#61666b);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;display:inline-flex}.F01v1a_close:hover{color:var(--dsw-alias-label-primary,#0f1115);background:var(--dsw-alias-interactive-bg-hover,#2631480f)}.F01v1a_body{flex-direction:column;flex:1;min-height:0;display:flex}.pluginTopologyButton{border:1px solid var(--dsw-alias-border-l3,#0000001f);background:var(--dsw-alias-bg-layer-2,#fff);height:26px;color:var(--dsw-alias-label-primary,#0f1115);cursor:pointer;border-radius:6px;align-items:center;gap:6px;padding:0 10px;font-family:inherit;font-size:12px;line-height:1;display:inline-flex}.pluginTopologyButton:hover{border-color:var(--dsw-alias-state-business-primary,#3b82f6)}";
|
|
5338
5844
|
const tagId = "dsh-plugin-topology/panel.module.css";
|
|
5339
5845
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
5340
5846
|
const tag = document.createElement("style");
|
|
@@ -5476,7 +5982,12 @@ const en = {
|
|
|
5476
5982
|
legendLabel: "Legend",
|
|
5477
5983
|
legendUnresolved: "Unresolved dependency",
|
|
5478
5984
|
legendIsolated: "Isolated plugin",
|
|
5479
|
-
legendId: "Instance creation ordinal within this run"
|
|
5985
|
+
legendId: "Instance creation ordinal within this run",
|
|
5986
|
+
popoverClose: "Close",
|
|
5987
|
+
popoverSource: "Source",
|
|
5988
|
+
popoverOutgoing: "Highlight outgoing dependencies (deg⁺)",
|
|
5989
|
+
popoverIncoming: "Highlight incoming dependents (deg⁻)",
|
|
5990
|
+
popoverUnresolved: "unresolved"
|
|
5480
5991
|
};
|
|
5481
5992
|
const zh = {
|
|
5482
5993
|
title: "插件拓扑",
|
|
@@ -5503,7 +6014,12 @@ const zh = {
|
|
|
5503
6014
|
legendLabel: "图例",
|
|
5504
6015
|
legendUnresolved: "存在未解析依赖",
|
|
5505
6016
|
legendIsolated: "孤立插件(无依赖边)",
|
|
5506
|
-
legendId: "本次运行内的创建序号"
|
|
6017
|
+
legendId: "本次运行内的创建序号",
|
|
6018
|
+
popoverClose: "关闭",
|
|
6019
|
+
popoverSource: "来源",
|
|
6020
|
+
popoverOutgoing: "高亮它依赖的插件(出度 deg⁺)",
|
|
6021
|
+
popoverIncoming: "高亮依赖它的插件(入度 deg⁻)",
|
|
6022
|
+
popoverUnresolved: "未解析"
|
|
5507
6023
|
};
|
|
5508
6024
|
|
|
5509
6025
|
//#endregion
|
|
@@ -5542,8 +6058,8 @@ function apply(ctx) {
|
|
|
5542
6058
|
if (!result.ok) throw new Error(`pluginTopology.analyze failed: ${result.error.code}: ${result.error.message}`);
|
|
5543
6059
|
return result.value;
|
|
5544
6060
|
},
|
|
5545
|
-
render: async (format, rankdir) => {
|
|
5546
|
-
const result = await namespaceOf().render(format, rankdir);
|
|
6061
|
+
render: async (format, rankdir, theme) => {
|
|
6062
|
+
const result = await namespaceOf().render(format, rankdir, theme);
|
|
5547
6063
|
if (!result.ok) throw new Error(`pluginTopology.render failed: ${result.error.code}: ${result.error.message}`);
|
|
5548
6064
|
return result.value;
|
|
5549
6065
|
}
|