@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/NEXT-STEPS.md
CHANGED
|
@@ -17,16 +17,23 @@ unresolved dependencies (red stroke). Color nodes by state instead:
|
|
|
17
17
|
This surfaces the most common diagnostic question — "which plugins did not
|
|
18
18
|
load successfully" — at a glance.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
(module specifier), but the UI never shows them. Clicking a node could open a
|
|
24
|
-
small popover with state, injected services, module path, and degree
|
|
25
|
-
centrality. Requires switching the SVG from an `<img>` to inline rendering
|
|
26
|
-
with pointer hit-testing — a medium-sized change.
|
|
20
|
+
Worth noting the boundary: when DSH cannot boot at all, this panel cannot
|
|
21
|
+
render either, so pre-boot diagnosis belongs to a CLI tool. State coloring
|
|
22
|
+
targets the other case — DSH runs, but some plugin misbehaved.
|
|
27
23
|
|
|
28
24
|
## Deferred ideas
|
|
29
25
|
|
|
30
26
|
- Show an instance count (`timer ×3`) instead of the ordinal list. Deferred:
|
|
31
27
|
the ordinal list doubles as a startup-order hint, and adding the count
|
|
32
28
|
crowds the label.
|
|
29
|
+
- Transitively affected plugins ("if I remove X, what breaks?"). Deferred:
|
|
30
|
+
direct in/out degree already covers the common question, and transitive
|
|
31
|
+
closure is a query better served by the JSON export than by the picture.
|
|
32
|
+
|
|
33
|
+
## Done
|
|
34
|
+
|
|
35
|
+
- **Click a node for details** — shipped in `0.5.0` as a node-anchored popover
|
|
36
|
+
(state, module source, drawn in/out degree, npm link) with `deg⁺`/`deg⁻`
|
|
37
|
+
toggles that light one dependency direction at a time. The SVG moved from an
|
|
38
|
+
`<img>` to inline rendering; highlight classes are applied to the Graphviz DOM
|
|
39
|
+
and never baked into the exported SVG.
|
package/README.md
CHANGED
|
@@ -67,6 +67,26 @@ centered window. Same-named plugin instances merge into one node; each node's
|
|
|
67
67
|
label carries the instance creation ordinals in brackets (`timer [1,9,23]`),
|
|
68
68
|
assigned at startup and meaningful only within that run.
|
|
69
69
|
|
|
70
|
+
### Node inspection
|
|
71
|
+
|
|
72
|
+
The SVG renders inline, so the graph is interactive without baking anything
|
|
73
|
+
into the export — a downloaded SVG still opens clean in any viewer.
|
|
74
|
+
|
|
75
|
+
- **Hover** raises a light ring on the node, nothing else.
|
|
76
|
+
- **Click** opens a card anchored to that node (it tracks the node through pan
|
|
77
|
+
and zoom). The card shows only what needs a running process to know: the
|
|
78
|
+
fiber `state`, the module `source`, and the drawn in/out degree. The name
|
|
79
|
+
links to the npm page for everything that does not (version, description,
|
|
80
|
+
license, repository).
|
|
81
|
+
- **`deg⁺` / `deg⁻`** are pill toggles for the graph-theory out-/in-degree as
|
|
82
|
+
drawn in the collapsed projection. Tapping one lights exactly that
|
|
83
|
+
direction's dependency edges and their far ends while the rest fades; tapping
|
|
84
|
+
it again clears. They are disabled at zero.
|
|
85
|
+
|
|
86
|
+
Degree counts are read from the rendered SVG rather than the raw snapshot, so a
|
|
87
|
+
merged node reports the edges actually drawn after the projection collapses
|
|
88
|
+
same-named instances.
|
|
89
|
+
|
|
70
90
|
The client injects `remote` (the gateway ClientRemote service) and self-mounts
|
|
71
91
|
its own `pluginTopology` Remote contribution, so it does not require editing
|
|
72
92
|
the host assembly's contribution list.
|
|
@@ -79,7 +99,7 @@ version on their own schedule, independent of which DSH release it targets.
|
|
|
79
99
|
The table below maps each plugin version to the DSH release it was validated
|
|
80
100
|
against, so pick the plugin version whose target DSH matches your harness.
|
|
81
101
|
|
|
82
|
-
Current release targets DeepSeek Harness `0.
|
|
102
|
+
Current release targets DeepSeek Harness `0.2.0-rc.2`; the `peerDependencies`
|
|
83
103
|
pin the client packages and `@deepseek-ai/cordis@^4.0.4` the snapshot reads
|
|
84
104
|
through. The 0.1.2 rc line removed the old `dsh-client-runtime` browser runtime:
|
|
85
105
|
the client now runs on the Cordis `Context` augmented by the shell baseline
|
|
@@ -101,6 +121,7 @@ releases only and skips the fast-moving `alpha` line.
|
|
|
101
121
|
| `0.3.0` | `0.1.5-rc.1` | Dependency refresh for DSH 0.1.5-rc.1; no code changes required. |
|
|
102
122
|
| `0.3.1` | `0.1.5-rc.3` | Peer refresh to DSH 0.1.5-rc.3 (same 0.1.5 rc line). |
|
|
103
123
|
| `0.4.0` | `0.1.7-rc.2` | TypertCodec API migration (`schema` → `create`); cordis ^4.0.4. |
|
|
124
|
+
| `0.5.0` | `0.2.0-rc.2` | Node detail popover + direction-scoped dependency highlight; panel colors rebound to the real DSH theme tokens (dark theme); peer refresh to DSH 0.2.0-rc.2 (no code changes required). |
|
|
104
125
|
|
|
105
126
|
## Known Limitations
|
|
106
127
|
|
|
@@ -121,8 +142,8 @@ pnpm run typecheck # noEmit check
|
|
|
121
142
|
```
|
|
122
143
|
|
|
123
144
|
The `dsh.client` browser bundle inlines everything except the shell's frozen
|
|
124
|
-
platform-module rows (`react`,
|
|
125
|
-
|
|
145
|
+
platform-module rows (`react`, `react/jsx-runtime`, and
|
|
146
|
+
`@deepseek-ai/dsh-client-store`), which every supported harness shell serves.
|
|
126
147
|
|
|
127
148
|
See [NEXT-STEPS.md](NEXT-STEPS.md) for planned renderer improvements.
|
|
128
149
|
|
|
@@ -147,8 +168,8 @@ store avoids the npm cache entirely:
|
|
|
147
168
|
pnpm publish # same prepublishOnly gate, pnpm store
|
|
148
169
|
```
|
|
149
170
|
|
|
150
|
-
`files` ships `lib/`, `cordis.patch.yml`,
|
|
151
|
-
README and LICENSE automatically.
|
|
171
|
+
`files` ships `lib/`, `cordis.patch.yml`, `overlay.example.yml`, and
|
|
172
|
+
`NEXT-STEPS.md`; npm adds README and LICENSE automatically.
|
|
152
173
|
|
|
153
174
|
## License
|
|
154
175
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { PluginNode, TopologyAnalysis } from '../types.ts';
|
|
3
|
+
import type { PluginTopologyLocaleKey } from './locales.ts';
|
|
4
|
+
/** The currently selected node's detail data, derived from the analysis. */
|
|
5
|
+
export interface NodeDetail {
|
|
6
|
+
readonly node: PluginNode;
|
|
7
|
+
readonly dependsOnCount: number;
|
|
8
|
+
readonly dependedByCount: number;
|
|
9
|
+
readonly unresolvedCount: number;
|
|
10
|
+
}
|
|
11
|
+
export interface NodeDetailPanelProps {
|
|
12
|
+
readonly detail: NodeDetail;
|
|
13
|
+
readonly analysis: TopologyAnalysis;
|
|
14
|
+
/** Which dependency direction is currently highlighted on the graph, if any. */
|
|
15
|
+
readonly highlightDirection: 'depends-on' | 'depended-by' | null;
|
|
16
|
+
readonly onHighlight: (direction: 'depends-on' | 'depended-by' | null) => void;
|
|
17
|
+
readonly onClose: () => void;
|
|
18
|
+
readonly t: (key: PluginTopologyLocaleKey) => string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Side panel showing runtime-only information about the selected plugin node.
|
|
22
|
+
* Displays name (linked to npm), state badge, source path, dependency counts
|
|
23
|
+
* (clickable to highlight edges on the graph), and unresolved dependency count.
|
|
24
|
+
*/
|
|
25
|
+
export declare function NodeDetailPanel({ detail, highlightDirection, onHighlight, onClose, t, }: NodeDetailPanelProps): ReactNode;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconCloseOutline16 } from "./icons.js";
|
|
3
|
+
import css from './NodeDetailPanel.module.css';
|
|
4
|
+
/** Build an npm registry URL from a source specifier, or null for local paths. */
|
|
5
|
+
function npmUrl(source) {
|
|
6
|
+
if (source === undefined)
|
|
7
|
+
return null;
|
|
8
|
+
// npm package names: @scope/name or bare-name (no path separators beyond scope)
|
|
9
|
+
if (source.startsWith('/') || source.startsWith('.') || source.includes(':\\'))
|
|
10
|
+
return null;
|
|
11
|
+
return `https://www.npmjs.com/package/${encodeURIComponent(source)}`;
|
|
12
|
+
}
|
|
13
|
+
function stateBadgeClass(state) {
|
|
14
|
+
if (state === 'ACTIVE')
|
|
15
|
+
return `${css.stateBadge} ${css.stateActive}`;
|
|
16
|
+
if (state === 'FAILED')
|
|
17
|
+
return `${css.stateBadge} ${css.stateFailed}`;
|
|
18
|
+
return `${css.stateBadge} ${css.stateOther}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Side panel showing runtime-only information about the selected plugin node.
|
|
22
|
+
* Displays name (linked to npm), state badge, source path, dependency counts
|
|
23
|
+
* (clickable to highlight edges on the graph), and unresolved dependency count.
|
|
24
|
+
*/
|
|
25
|
+
export function NodeDetailPanel({ detail, highlightDirection, onHighlight, onClose, t, }) {
|
|
26
|
+
const { node, dependsOnCount, dependedByCount, unresolvedCount } = detail;
|
|
27
|
+
const url = npmUrl(node.source);
|
|
28
|
+
const toggleHighlight = (direction) => {
|
|
29
|
+
onHighlight(highlightDirection === direction ? null : direction);
|
|
30
|
+
};
|
|
31
|
+
return (_jsxs("aside", { className: css.panel, "aria-label": t('detailClose'), children: [_jsxs("div", { className: css.header, children: [url !== null ? (_jsx("a", { className: css.nameLink, href: url, target: "_blank", rel: "noopener noreferrer", children: node.label })) : (_jsx("span", { className: css.nameLink, style: { color: 'var(--dsw-text-primary, #0f172a)' }, children: node.label })), _jsx("button", { type: "button", className: css.closeButton, onClick: onClose, "aria-label": t('detailClose'), children: _jsx(IconCloseOutline16, { size: 14 }) })] }), _jsx("span", { className: stateBadgeClass(node.state), children: node.state }), node.source !== undefined && (_jsxs("div", { className: css.row, children: [_jsx("span", { className: css.rowLabel, children: t('detailSource') }), _jsx("span", { className: `${css.rowValue} ${css.sourceValue}`, children: node.source })] })), _jsxs("div", { className: css.row, children: [_jsx("span", { className: css.rowLabel, children: t('detailDependsOn') }), _jsx("button", { type: "button", className: highlightDirection === 'depends-on' ? `${css.countButton} ${css.countButtonActive}` : css.countButton, onClick: () => { toggleHighlight('depends-on'); }, disabled: dependsOnCount === 0, children: dependsOnCount })] }), _jsxs("div", { className: css.row, children: [_jsx("span", { className: css.rowLabel, children: t('detailDependedBy') }), _jsx("button", { type: "button", className: highlightDirection === 'depended-by' ? `${css.countButton} ${css.countButtonActive}` : css.countButton, onClick: () => { toggleHighlight('depended-by'); }, disabled: dependedByCount === 0, children: dependedByCount })] }), unresolvedCount > 0 && (_jsxs("div", { className: css.row, children: [_jsx("span", { className: css.rowLabel, children: t('detailUnresolved') }), _jsx("span", { className: `${css.rowValue} ${css.unresolvedValue}`, children: unresolvedCount })] }))] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { HighlightDirection } from './TopologyGraphView.tsx';
|
|
3
|
+
import type { TopologyAnalysis } from '../types.ts';
|
|
4
|
+
import type { PluginTopologyLocaleKey } from './locales.ts';
|
|
5
|
+
export interface PopoverData {
|
|
6
|
+
/** Stable element id of the selected node in the SVG. */
|
|
7
|
+
readonly key: string;
|
|
8
|
+
/** DOT node id (`p:3` / `m:0`). */
|
|
9
|
+
readonly nodeId: string;
|
|
10
|
+
readonly label: string;
|
|
11
|
+
/** Tap point in graph coordinates; the durable anchor across pan and zoom. */
|
|
12
|
+
readonly anchorX: number;
|
|
13
|
+
readonly anchorY: number;
|
|
14
|
+
/** Viewport-relative x derived from the live transform. */
|
|
15
|
+
readonly x: number;
|
|
16
|
+
/** Viewport-relative y derived from the live transform. */
|
|
17
|
+
readonly y: number;
|
|
18
|
+
/** Outgoing direct dependencies (deg⁺) as drawn. */
|
|
19
|
+
readonly outCount: number;
|
|
20
|
+
/** Incoming direct dependents (deg⁻) as drawn. */
|
|
21
|
+
readonly inCount: number;
|
|
22
|
+
}
|
|
23
|
+
export interface NodePopoverProps {
|
|
24
|
+
readonly popover: PopoverData;
|
|
25
|
+
readonly analysis: TopologyAnalysis;
|
|
26
|
+
/** Currently highlighted direction, or null when only the node is selected. */
|
|
27
|
+
readonly direction: HighlightDirection | null;
|
|
28
|
+
/** Toggle one direction's edge highlight on or off. */
|
|
29
|
+
readonly onToggleDirection: (direction: HighlightDirection) => void;
|
|
30
|
+
readonly onClose: () => void;
|
|
31
|
+
readonly t: (key: PluginTopologyLocaleKey) => string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Floating detail card anchored to a clicked graph node. It shows only facts
|
|
35
|
+
* that need the running process to know (state, load source, drawn in/out
|
|
36
|
+
* degree) and links the name out to npm for everything else. The deg⁺/deg⁻
|
|
37
|
+
* pills drive the graph highlight, so the panel itself stays a one-screen read.
|
|
38
|
+
*/
|
|
39
|
+
export declare function NodePopover({ popover, analysis, direction, onToggleDirection, onClose, t, }: NodePopoverProps): ReactNode;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { IconCloseOutline16 } from "./icons.js";
|
|
4
|
+
import css from './NodePopover.module.css';
|
|
5
|
+
/** Gap between the anchor and the card's top-left corner, in px. */
|
|
6
|
+
const OFFSET = 8;
|
|
7
|
+
/** Build an npm registry URL from a source specifier, or null for local paths. */
|
|
8
|
+
function npmUrl(source) {
|
|
9
|
+
if (source === undefined)
|
|
10
|
+
return null;
|
|
11
|
+
if (source.startsWith('/') || source.startsWith('.') || source.includes(':\\'))
|
|
12
|
+
return null;
|
|
13
|
+
return `https://www.npmjs.com/package/${encodeURIComponent(source)}`;
|
|
14
|
+
}
|
|
15
|
+
function stateBadgeClass(state) {
|
|
16
|
+
if (state === 'ACTIVE')
|
|
17
|
+
return `${css.stateBadge} ${css.stateActive}`;
|
|
18
|
+
if (state === 'FAILED')
|
|
19
|
+
return `${css.stateBadge} ${css.stateFailed}`;
|
|
20
|
+
return `${css.stateBadge} ${css.stateOther}`;
|
|
21
|
+
}
|
|
22
|
+
/** The worst lifecycle state in a group, so a broken instance is never hidden. */
|
|
23
|
+
function worstState(states) {
|
|
24
|
+
if (states.includes('FAILED'))
|
|
25
|
+
return 'FAILED';
|
|
26
|
+
if (states.includes('PENDING'))
|
|
27
|
+
return 'PENDING';
|
|
28
|
+
return states[0] ?? 'ACTIVE';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the runtime facts the panel shows. Single instances map straight to a
|
|
32
|
+
* snapshot node; merged render nodes (`m:N`, several same-named instances drawn
|
|
33
|
+
* as one) are recovered from the ordinals in the rendered label.
|
|
34
|
+
*/
|
|
35
|
+
function resolveNode(nodeId, svgLabel, analysis) {
|
|
36
|
+
const direct = analysis.collapsed.nodes.find(node => node.id === nodeId);
|
|
37
|
+
if (direct !== undefined) {
|
|
38
|
+
return {
|
|
39
|
+
node: direct,
|
|
40
|
+
unresolved: analysis.graph.unresolved.filter(dep => dep.plugin === nodeId).length,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const bracket = svgLabel.match(/\[([^\]]*)\]\s*$/);
|
|
44
|
+
const baseName = svgLabel.replace(/\s*\[[^\]]*\]\s*$/, '').trim();
|
|
45
|
+
const ordinals = bracket === null
|
|
46
|
+
? []
|
|
47
|
+
: bracket[1].split(',').map(part => part.trim()).filter(part => part !== '' && part !== '…');
|
|
48
|
+
let members = analysis.collapsed.nodes.filter(node => node.label === baseName);
|
|
49
|
+
if (ordinals.length > 0) {
|
|
50
|
+
const wanted = new Set(ordinals.map(ordinal => `p:${ordinal}`));
|
|
51
|
+
const matched = members.filter(node => wanted.has(node.id));
|
|
52
|
+
if (matched.length > 0)
|
|
53
|
+
members = matched;
|
|
54
|
+
}
|
|
55
|
+
const source = members.find(member => member.source !== undefined)?.source;
|
|
56
|
+
const node = {
|
|
57
|
+
id: nodeId,
|
|
58
|
+
kind: 'plugin',
|
|
59
|
+
label: baseName === '' ? svgLabel : baseName,
|
|
60
|
+
state: worstState(members.map(member => member.state)),
|
|
61
|
+
parent: null,
|
|
62
|
+
inject: [],
|
|
63
|
+
...(source === undefined ? {} : { source }),
|
|
64
|
+
};
|
|
65
|
+
const unresolved = members.reduce((total, member) => total + analysis.graph.unresolved.filter(dep => dep.plugin === member.id).length, 0);
|
|
66
|
+
return { node, unresolved };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Floating detail card anchored to a clicked graph node. It shows only facts
|
|
70
|
+
* that need the running process to know (state, load source, drawn in/out
|
|
71
|
+
* degree) and links the name out to npm for everything else. The deg⁺/deg⁻
|
|
72
|
+
* pills drive the graph highlight, so the panel itself stays a one-screen read.
|
|
73
|
+
*/
|
|
74
|
+
export function NodePopover({ popover, analysis, direction, onToggleDirection, onClose, t, }) {
|
|
75
|
+
const ref = useRef(null);
|
|
76
|
+
const { node, unresolved } = resolveNode(popover.nodeId, popover.label, analysis);
|
|
77
|
+
const url = npmUrl(node.source);
|
|
78
|
+
// Track the anchor on every pan/zoom. The card sits just off the tap point,
|
|
79
|
+
// clamped inside the viewport so it stays readable and slides along the edge
|
|
80
|
+
// once its node scrolls out instead of being left behind.
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const el = ref.current;
|
|
83
|
+
if (el === null)
|
|
84
|
+
return;
|
|
85
|
+
const parent = el.parentElement;
|
|
86
|
+
if (parent === null)
|
|
87
|
+
return;
|
|
88
|
+
const bounds = parent.getBoundingClientRect();
|
|
89
|
+
const own = el.getBoundingClientRect();
|
|
90
|
+
const maxLeft = Math.max(OFFSET, bounds.width - own.width - OFFSET);
|
|
91
|
+
const maxTop = Math.max(OFFSET, bounds.height - own.height - OFFSET);
|
|
92
|
+
el.style.left = `${Math.min(Math.max(OFFSET, popover.x + OFFSET), maxLeft)}px`;
|
|
93
|
+
el.style.top = `${Math.min(Math.max(OFFSET, popover.y + OFFSET), maxTop)}px`;
|
|
94
|
+
}, [popover.x, popover.y]);
|
|
95
|
+
return (_jsxs("div", { ref: ref, className: css.popover, style: { left: popover.x + OFFSET, top: popover.y + OFFSET }, role: "dialog", "aria-label": node.label, children: [_jsxs("div", { className: css.header, children: [url !== null ? (_jsx("a", { className: css.nameLink, href: url, target: "_blank", rel: "noopener noreferrer", children: node.label })) : (_jsx("span", { className: css.namePlain, children: node.label })), _jsx("button", { type: "button", className: css.closeButton, onClick: onClose, "aria-label": t('popoverClose'), children: _jsx(IconCloseOutline16, { size: 12 }) })] }), _jsxs("div", { className: css.metaRow, children: [_jsx("span", { className: stateBadgeClass(node.state), children: node.state }), unresolved > 0 && (_jsxs("span", { className: css.unresolvedBadge, children: [t('popoverUnresolved'), " ", unresolved] }))] }), node.source !== undefined && (_jsxs("div", { className: css.row, children: [_jsx("span", { className: css.rowLabel, children: t('popoverSource') }), _jsx("span", { className: `${css.rowValue} ${css.sourceValue}`, children: node.source })] })), _jsxs("div", { className: css.depPills, children: [_jsxs("button", { type: "button", className: direction === 'out' ? `${css.depPill} ${css.depPillActive}` : css.depPill, "aria-pressed": direction === 'out', "aria-label": t('popoverOutgoing'), title: t('popoverOutgoing'), disabled: popover.outCount === 0, onClick: () => { onToggleDirection('out'); }, children: [_jsx("span", { className: css.degMark, children: "deg\u207A" }), _jsx("span", { className: css.degValue, children: popover.outCount })] }), _jsxs("button", { type: "button", className: direction === 'in' ? `${css.depPill} ${css.depPillActive}` : css.depPill, "aria-pressed": direction === 'in', "aria-label": t('popoverIncoming'), title: t('popoverIncoming'), disabled: popover.inCount === 0, onClick: () => { onToggleDirection('in'); }, children: [_jsx("span", { className: css.degMark, children: "deg\u207B" }), _jsx("span", { className: css.degValue, children: popover.inCount })] })] })] }));
|
|
96
|
+
}
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { TopologyTransform } from './stores.ts';
|
|
3
|
+
/** Dependency direction highlighted from the selected node. */
|
|
4
|
+
export type HighlightDirection = 'out' | 'in';
|
|
5
|
+
/** A node resolved from the composed SVG DOM. */
|
|
6
|
+
export interface GraphNodeRef {
|
|
7
|
+
/** Stable element id (`node-p:3` / `iso-node-m:0`), unique across the composed SVG. */
|
|
8
|
+
readonly key: string;
|
|
9
|
+
/** DOT node id from the `<title>` text (`p:3` / `m:0`), used to match edge endpoints. */
|
|
10
|
+
readonly dotId: string;
|
|
11
|
+
/** Display label (`ApprovalService [60,96]`). */
|
|
12
|
+
readonly label: string;
|
|
13
|
+
}
|
|
14
|
+
/** Data passed when a graph node is tapped. */
|
|
15
|
+
export interface NodeClickInfo {
|
|
16
|
+
/** Stable element id, echoed back as the selection key. */
|
|
17
|
+
readonly key: string;
|
|
18
|
+
/** DOT node id (`p:3` / `m:0`). */
|
|
19
|
+
readonly nodeId: string;
|
|
20
|
+
/** Display label as rendered, including the instance ordinals. */
|
|
21
|
+
readonly label: string;
|
|
22
|
+
/**
|
|
23
|
+
* Tap point in graph coordinates. Anchoring here rather than in viewport px
|
|
24
|
+
* keeps the popover attached to its node across pan and zoom.
|
|
25
|
+
*/
|
|
26
|
+
readonly anchorX: number;
|
|
27
|
+
readonly anchorY: number;
|
|
28
|
+
/** Outgoing direct dependencies (deg⁺) as drawn in the current projection. */
|
|
29
|
+
readonly outCount: number;
|
|
30
|
+
/** Incoming direct dependents (deg⁻) as drawn in the current projection. */
|
|
31
|
+
readonly inCount: number;
|
|
32
|
+
}
|
|
3
33
|
/** Zoom/pan props plus the localized control labels and the shared transform. */
|
|
4
34
|
export interface TopologyGraphViewProps {
|
|
5
35
|
svg: string;
|
|
@@ -16,12 +46,25 @@ export interface TopologyGraphViewProps {
|
|
|
16
46
|
resetViewLabel: string;
|
|
17
47
|
/** Accessible name for the clickable zoom-level toggle. */
|
|
18
48
|
zoomLevelLabel: string;
|
|
49
|
+
/** Called when a graph node is tapped; receives id, label, counts, and viewport coords. */
|
|
50
|
+
onNodeClick?: (info: NodeClickInfo) => void;
|
|
51
|
+
/** Called when empty space is tapped (to close the popover and drop the highlight). */
|
|
52
|
+
onEmptyClick?: () => void;
|
|
53
|
+
/** Element id of the selected node, or null when nothing is selected. */
|
|
54
|
+
selectedKey?: string | null;
|
|
55
|
+
/** DOT id of the selected node, used to match edge endpoints. */
|
|
56
|
+
selectedDotId?: string | null;
|
|
57
|
+
/** Active dependency direction to highlight; null means selection only. */
|
|
58
|
+
direction?: HighlightDirection | null;
|
|
19
59
|
}
|
|
20
60
|
/**
|
|
21
|
-
* Pan/zoom SVG viewer
|
|
22
|
-
* drag pans, double-click zooms toward the
|
|
23
|
-
* (fit / zoom-out / zoom-level / zoom-in)
|
|
24
|
-
*
|
|
25
|
-
*
|
|
61
|
+
* Pan/zoom SVG viewer with drill-down interaction. Fit-and-center on load,
|
|
62
|
+
* wheel zooms toward the cursor, drag pans, double-click zooms toward the
|
|
63
|
+
* cursor, and a compact floating pill (fit / zoom-out / zoom-level / zoom-in)
|
|
64
|
+
* collapses to just the fit control at rest.
|
|
65
|
+
*
|
|
66
|
+
* Hovering a node raises a ring; tapping it fires onNodeClick with viewport
|
|
67
|
+
* coordinates and the drawn deg⁺/deg⁻ counts. Tapping empty space fires
|
|
68
|
+
* onEmptyClick. `direction` lights only the chosen dependency direction.
|
|
26
69
|
*/
|
|
27
|
-
export declare function TopologyGraphView({ svg, alt, height, transform, onTransformChange, zoomInLabel, zoomOutLabel, resetViewLabel, zoomLevelLabel, }: TopologyGraphViewProps): ReactNode;
|
|
70
|
+
export declare function TopologyGraphView({ svg, alt, height, transform, onTransformChange, zoomInLabel, zoomOutLabel, resetViewLabel, zoomLevelLabel, onNodeClick, onEmptyClick, selectedKey, selectedDotId, direction, }: TopologyGraphViewProps): ReactNode;
|