diff --git a/frontend/console/package.json b/frontend/console/package.json
index b4b8d37924..a56d0d5a9d 100644
--- a/frontend/console/package.json
+++ b/frontend/console/package.json
@@ -43,11 +43,17 @@
"@tailwindcss/forms": "^0.5.6",
"@tanstack/react-query": "^5.51.23",
"@tanstack/react-query-devtools": "^5.51.23",
+ "@types/cytoscape": "^3.21.8",
+ "@types/cytoscape-dagre": "^2.3.3",
+ "@types/dagre": "^0.7.52",
"@uiw/codemirror-theme-atomone": "^4.22.0",
"@uiw/codemirror-theme-github": "^4.22.0",
"@vitejs/plugin-react": "^4.0.4",
"codemirror-json-schema": "0.7.0",
"codemirror-json5": "^1.0.3",
+ "cytoscape": "^3.30.3",
+ "cytoscape-dagre": "^2.5.0",
+ "dagre": "^0.8.5",
"fnv1a": "^1.1.1",
"fuse.js": "^7.0.0",
"highlight.js": "^11.8.0",
diff --git a/frontend/console/src/api/modules/use-stream-modules.ts b/frontend/console/src/api/modules/use-stream-modules.ts
index 6bfd535038..8582d1c7a7 100644
--- a/frontend/console/src/api/modules/use-stream-modules.ts
+++ b/frontend/console/src/api/modules/use-stream-modules.ts
@@ -7,7 +7,7 @@ import type { Module, Topology } from '../../protos/xyz/block/ftl/v1/console/con
const streamModulesKey = 'streamModules'
-type StreamModulesResult = {
+export type StreamModulesResult = {
modules: Module[]
topology: Topology
}
diff --git a/frontend/console/src/features/console/ConsolePage.tsx b/frontend/console/src/features/console/ConsolePage.tsx
index f0684022be..66443c6679 100644
--- a/frontend/console/src/features/console/ConsolePage.tsx
+++ b/frontend/console/src/features/console/ConsolePage.tsx
@@ -4,7 +4,8 @@ import { useModules } from '../../api/modules/use-modules'
import { Loader } from '../../components/Loader'
import { ResizablePanels } from '../../components/ResizablePanels'
import { Config, Data, Database, Enum, Module, Secret, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb'
-import { type FTLNode, GraphPane } from '../graph/GraphPane'
+import type { FTLNode } from '../graph/GraphPane'
+import { NewGraphPane } from '../graph/NewGraphPane'
import { configPanels } from '../modules/decls/config/ConfigRightPanels'
import { dataPanels } from '../modules/decls/data/DataRightPanels'
import { databasePanels } from '../modules/decls/database/DatabaseRightPanels'
@@ -33,7 +34,7 @@ export const ConsolePage = () => {
return (
}
+ mainContent={
}
rightPanelHeader={headerForNode(selectedNode)}
rightPanelPanels={panelsForNode(modules.data.modules, selectedNode, navigate)}
bottomPanelContent={
}
diff --git a/frontend/console/src/features/graph/NewGraphPane.tsx b/frontend/console/src/features/graph/NewGraphPane.tsx
new file mode 100644
index 0000000000..cab591da4c
--- /dev/null
+++ b/frontend/console/src/features/graph/NewGraphPane.tsx
@@ -0,0 +1,270 @@
+import cytoscape from 'cytoscape'
+import dagre, { type DagreLayoutOptions } from 'cytoscape-dagre'
+import { useEffect, useRef, useState } from 'react'
+import type React from 'react'
+import { useStreamModules } from '../../api/modules/use-stream-modules'
+import type { FTLNode } from './GraphPane'
+import { getGraphData } from './graph-utils'
+
+cytoscape.use(dagre)
+
+interface NewGraphPaneProps {
+ onTapped?: (item: FTLNode | null) => void
+}
+
+export const NewGraphPane: React.FC
= () => {
+ // Add state for tracking current group view
+ // const [currentGroup, setCurrentGroup] = useState(null)
+
+ const modules = useStreamModules()
+
+ // Modify static data to include children
+ const staticData = {
+ modules: [
+ {
+ id: 'mod1',
+ title: 'Module 1',
+ type: 'groupNode',
+ children: [
+ { id: 'mod1-1', title: 'Sub Module 1.1', type: 'node' },
+ { id: 'mod1-2', title: 'Sub Module 1.2', type: 'node' },
+ ],
+ childConnections: [{ source: 'mod1-1', target: 'mod1-2' }],
+ },
+ {
+ id: 'mod2',
+ title: 'Module 2',
+ type: 'groupNode',
+ children: [
+ { id: 'mod2-1', title: 'Sub Module 2.1', type: 'node' },
+ { id: 'mod2-2', title: 'Sub Module 2.2', type: 'node' },
+ { id: 'mod2-3', title: 'Sub Module 2.3', type: 'node' },
+ ],
+ childConnections: [{ source: 'mod2-1', target: 'mod2-2' }],
+ },
+ {
+ id: 'mod3',
+ title: 'Module 3',
+ type: 'groupNode',
+ children: [
+ { id: 'mod3-1', title: 'Sub Module 3.1', type: 'node' },
+ { id: 'mod3-2', title: 'Sub Module 3.2', type: 'node' },
+ ],
+ childConnections: [{ source: 'mod3-1', target: 'mod3-2' }],
+ },
+ {
+ id: 'mod4',
+ title: 'Module 4',
+ type: 'groupNode',
+ children: [
+ { id: 'mod4-1', title: 'Sub Module 4.1', type: 'node' },
+ { id: 'mod4-2', title: 'Sub Module 4.2', type: 'node' },
+ ],
+ childConnections: [{ source: 'mod4-1', target: 'mod4-2' }],
+ },
+ ],
+ topology: {
+ connections: [
+ // { source: 'mod1', target: 'mod2' },
+ { source: 'mod2', target: 'mod4' },
+ { source: 'mod3', target: 'mod4' },
+ { source: 'mod2-1', target: 'mod1-1' },
+ { source: 'mod2-2', target: 'mod1-1' },
+ ],
+ },
+ }
+
+ const cyRef = useRef(null)
+ const cyInstance = useRef(null)
+ // const [, setSelectedNode] = React.useState(null)
+ const [nodePositions, setNodePositions] = useState>({})
+
+ // Initialize Cytoscape
+ useEffect(() => {
+ if (!cyRef.current) return
+
+ cyInstance.current = cytoscape({
+ container: cyRef.current,
+ userZoomingEnabled: true,
+ userPanningEnabled: true,
+ boxSelectionEnabled: false,
+ style: [
+ {
+ selector: 'node',
+ style: {
+ 'background-color': '#64748b',
+ label: 'data(label)',
+ 'text-valign': 'center',
+ 'text-halign': 'center',
+ shape: 'round-rectangle',
+ width: '120px',
+ height: '40px',
+ 'text-wrap': 'wrap',
+ 'text-max-width': '100px',
+ 'text-overflow-wrap': 'anywhere',
+ 'font-size': '12px',
+ },
+ },
+ {
+ selector: 'edge',
+ style: {
+ width: 2,
+ 'line-color': '#6366f1',
+ 'curve-style': 'bezier',
+ 'target-arrow-shape': 'triangle',
+ 'target-arrow-color': '#6366f1',
+ 'arrow-scale': 1,
+ },
+ },
+ {
+ selector: '$node > node',
+ style: {
+ 'padding-top': '10px',
+ 'padding-left': '10px',
+ 'padding-bottom': '10px',
+ 'padding-right': '10px',
+ 'text-valign': 'top',
+ 'text-halign': 'center',
+ 'background-color': '#94a3b8',
+ },
+ },
+ {
+ selector: 'node[type="groupNode"]',
+ style: {
+ 'background-color': '#6366f1',
+ 'background-opacity': 0.8,
+ shape: 'round-rectangle',
+ width: '120px',
+ height: '120px',
+ 'text-valign': 'top', // Default position at top
+ 'text-halign': 'center',
+ 'text-wrap': 'wrap',
+ 'text-max-width': '100px',
+ 'text-overflow-wrap': 'anywhere',
+ 'font-size': '14px',
+ },
+ },
+ {
+ selector: ':parent',
+ style: {
+ 'text-valign': 'top',
+ 'text-halign': 'center',
+ 'background-opacity': 0.3,
+ },
+ },
+ {
+ selector: '.selected',
+ style: {
+ 'background-color': '#3b82f6',
+ 'border-width': 2,
+ 'border-color': '#60a5fa',
+ },
+ },
+ {
+ selector: 'node[type="node"]',
+ style: {
+ 'background-color': 'data(backgroundColor)',
+ shape: 'round-rectangle',
+ width: '100px',
+ height: '30px',
+ 'border-width': '1px',
+ 'border-color': '#475569',
+ 'text-wrap': 'wrap',
+ 'text-max-width': '80px',
+ 'text-overflow-wrap': 'anywhere',
+ 'font-size': '11px',
+ },
+ },
+ ],
+ })
+
+ // Update zoom level event handler
+ cyInstance.current.on('zoom', (evt) => {
+ const zoom = evt.target.zoom()
+ const elements = evt.target.elements()
+
+ if (zoom < 1) {
+ // Hide child nodes
+ elements.nodes('node[type != "groupNode"]').style('opacity', 0)
+
+ // Show only module-level edges (type="moduleConnection")
+ elements.edges('[type = "moduleConnection"]').style('opacity', 1)
+ elements.edges('[type = "childConnection"]').style('opacity', 0)
+
+ // Move text inside and make it larger when zoomed out
+ elements.nodes('node[type = "groupNode"]').style({
+ 'text-valign': 'center',
+ 'text-halign': 'center',
+ 'font-size': '18px',
+ 'text-max-width': '100px',
+ })
+ } else {
+ // Show all nodes
+ elements.nodes().style('opacity', 1)
+
+ // Show only verb-level edges (type="childConnection")
+ elements.edges('[type = "moduleConnection"]').style('opacity', 0)
+ elements.edges('[type = "childConnection"]').style('opacity', 1)
+
+ // Move text to top when zoomed in
+ elements.nodes('node[type = "groupNode"]').style({
+ 'text-valign': 'top', // Move text to top
+ 'text-halign': 'center', // Keep text centered horizontally
+ 'font-size': '14px', // Original font size
+ 'text-max-width': '100px',
+ })
+ }
+ })
+
+ return () => {
+ cyInstance.current?.destroy()
+ }
+ }, [])
+
+ // Modify the data loading effect
+ useEffect(() => {
+ if (!cyInstance.current) return
+
+ const elements = getGraphData(modules.data, nodePositions)
+ cyInstance.current.elements().remove()
+ cyInstance.current.add(elements)
+
+ // Run layout if needed
+ if (staticData.modules.some((module) => !nodePositions[module.id])) {
+ const layoutOptions: DagreLayoutOptions = {
+ name: 'dagre',
+ rankDir: 'TB',
+ // ranker: 'network-simplex',
+ nodeDimensionsIncludeLabels: true,
+ rankSep: 5,
+ nodeSep: 5,
+ edgeSep: 20,
+ padding: 5,
+ ranker: 'tight-tree',
+ spacingFactor: 2,
+ }
+
+ const layout = cyInstance.current.layout(layoutOptions)
+ layout.run()
+
+ // Save positions after layout is complete
+ layout.on('layoutstop', () => {
+ const newPositions = { ...nodePositions }
+ for (const node of cyInstance.current?.nodes() || []) {
+ newPositions[node.id()] = node.position()
+ }
+ setNodePositions(newPositions)
+ })
+ }
+
+ cyInstance.current.fit()
+ }, [nodePositions, modules.data])
+
+ // Modify event handlers
+
+ return (
+
+ )
+}
diff --git a/frontend/console/src/features/graph/graph-utils.ts b/frontend/console/src/features/graph/graph-utils.ts
new file mode 100644
index 0000000000..ae8603b605
--- /dev/null
+++ b/frontend/console/src/features/graph/graph-utils.ts
@@ -0,0 +1,139 @@
+import type { EdgeDefinition, ElementDefinition } from 'cytoscape'
+import type { StreamModulesResult } from '../../api/modules/use-stream-modules'
+import type { Config, Module, Secret, Subscription, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb'
+
+const createParentNode = (module: Module, nodePositions: Record) => ({
+ group: 'nodes' as const,
+ data: {
+ id: module.name,
+ label: module.name,
+ type: 'groupNode',
+ item: module,
+ },
+ ...(nodePositions[module.name] && {
+ position: nodePositions[module.name],
+ }),
+})
+
+const createChildNode = (
+ parentName: string,
+ childId: string,
+ childLabel: string,
+ childType: string,
+ nodePositions: Record,
+) => ({
+ group: 'nodes' as const,
+ data: {
+ id: childId,
+ label: childLabel,
+ type: 'node',
+ nodeType: childType,
+ parent: parentName,
+ item: { id: childId, name: childLabel },
+ backgroundColor: childType === 'verb' ? '#e3f2fd' : childType === 'config' ? '#e8f5e9' : childType === 'secret' ? '#fce4ec' : '#ffffff', // default white
+ },
+ ...(nodePositions[childId] && {
+ position: nodePositions[childId],
+ }),
+})
+
+const createModuleChildren = (module: Module, nodePositions: Record) => {
+ const children = [
+ // Create nodes for verbs
+ ...(module.verbs || []).map((verb: Verb) =>
+ createChildNode(module.name, `${module.name}-verb-${verb.verb?.name}`, verb.verb?.name || '', 'verb', nodePositions),
+ ),
+ // Create nodes for configs
+ ...(module.configs || []).map((config: Config) =>
+ createChildNode(module.name, `${module.name}-config-${config.config?.name}`, config.config?.name || '', 'config', nodePositions),
+ ),
+ // Create nodes for secrets
+ ...(module.secrets || []).map((secret: Secret) =>
+ createChildNode(module.name, `${module.name}-secret-${secret.secret?.name}`, secret.secret?.name || '', 'secret', nodePositions),
+ ),
+ // Create nodes for subscriptions
+ ...(module.subscriptions || []).map((subscription: Subscription) =>
+ createChildNode(
+ module.name,
+ `${module.name}-subscription-${subscription.subscription?.name}`,
+ subscription.subscription?.name || '',
+ 'subscription',
+ nodePositions,
+ ),
+ ),
+ ]
+ return children
+}
+
+const createChildEdge = (sourceModule: string, sourceVerb: string, targetModule: string, targetVerb: string) => ({
+ group: 'edges' as const,
+ data: {
+ id: `${sourceModule}-${sourceVerb}-to-${targetModule}-${targetVerb}`,
+ source: `${sourceModule}-verb-${sourceVerb}`,
+ target: `${targetModule}-verb-${targetVerb}`,
+ type: 'childConnection',
+ },
+})
+
+const createModuleEdge = (sourceModule: string, targetModule: string) => ({
+ group: 'edges' as const,
+ data: {
+ id: `module-${sourceModule}-to-${targetModule}`,
+ source: sourceModule,
+ target: targetModule,
+ type: 'moduleConnection',
+ },
+})
+
+const createVerbEdges = (modules: Module[]) => {
+ const edges: EdgeDefinition[] = []
+ const moduleConnections = new Set() // Track unique module connections
+
+ for (const module of modules) {
+ // For each verb in the module
+ for (const verb of module.verbs || []) {
+ // For each reference in the verb
+ for (const ref of verb.references || []) {
+ // Create verb-to-verb edge
+ edges.push(createChildEdge(ref.module, ref.name, module.name, verb.verb?.name || ''))
+
+ // Track module-to-module connection
+ // Sort module names to ensure consistent edge IDs
+ const [sourceModule, targetModule] = [module.name, ref.module].sort()
+ moduleConnections.add(`${sourceModule}-${targetModule}`)
+ }
+ }
+
+ for (const config of module.configs || []) {
+ for (const ref of config.references || []) {
+ edges.push(createChildEdge(ref.module, ref.name, module.name, config.config?.name || ''))
+
+ // Track module-to-module connection
+ // Sort module names to ensure consistent edge IDs
+ const [sourceModule, targetModule] = [module.name, ref.module].sort()
+ moduleConnections.add(`${sourceModule}-${targetModule}`)
+ }
+ }
+ }
+
+ // Create module-level edges for each unique module connection
+ for (const connection of moduleConnections) {
+ const [sourceModule, targetModule] = connection.split('-')
+ edges.push(createModuleEdge(sourceModule, targetModule))
+ }
+
+ return edges
+}
+
+export const getGraphData = (modules: StreamModulesResult | undefined, nodePositions: Record = {}): ElementDefinition[] => {
+ if (!modules) return []
+
+ return [
+ // Add parent nodes (modules)
+ ...modules.modules.map((module) => createParentNode(module, nodePositions)),
+ // Add all child nodes
+ ...modules.modules.flatMap((module) => createModuleChildren(module, nodePositions)),
+ // Add both verb-level and module-level edges
+ ...createVerbEdges(modules.modules),
+ ]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b5a895b11a..169218fdf8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -50,6 +50,15 @@ importers:
'@tanstack/react-query-devtools':
specifier: ^5.51.23
version: 5.60.5(@tanstack/react-query@5.60.5(react@18.3.1))(react@18.3.1)
+ '@types/cytoscape':
+ specifier: ^3.21.8
+ version: 3.21.8
+ '@types/cytoscape-dagre':
+ specifier: ^2.3.3
+ version: 2.3.3
+ '@types/dagre':
+ specifier: ^0.7.52
+ version: 0.7.52
'@uiw/codemirror-theme-atomone':
specifier: ^4.22.0
version: 4.23.6(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.3)
@@ -65,6 +74,18 @@ importers:
codemirror-json5:
specifier: ^1.0.3
version: 1.0.3
+ cytoscape:
+ specifier: ^3.30.3
+ version: 3.30.3
+ cytoscape-cola:
+ specifier: ^2.5.1
+ version: 2.5.1(cytoscape@3.30.3)
+ cytoscape-dagre:
+ specifier: ^2.5.0
+ version: 2.5.0(cytoscape@3.30.3)
+ dagre:
+ specifier: ^0.8.5
+ version: 0.8.5
fnv1a:
specifier: ^1.1.1
version: 1.1.1
@@ -2046,6 +2067,12 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+ '@types/cytoscape-dagre@2.3.3':
+ resolution: {integrity: sha512-FJBsNMbBZpqNwT6rp5leVYMevWUjnyD1QS8erNMAMWoBifvaVUklXIjE+bllLDSowjM3abXuRvljliSXUU+d1A==}
+
+ '@types/cytoscape@3.21.8':
+ resolution: {integrity: sha512-6Bo9ZDrv0vfwe8Sg/ERc5VL0yU0gYvP4dgZi0fAXYkKHfyHaNqWRMcwYm3mu4sLsXbB8ZuXE75sR7qnaOL5JgQ==}
+
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
@@ -2139,6 +2166,9 @@ packages:
'@types/d3@7.4.3':
resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
+ '@types/dagre@0.7.52':
+ resolution: {integrity: sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==}
+
'@types/doctrine@0.0.9':
resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==}
@@ -2900,14 +2930,34 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+ cytoscape-cola@2.5.1:
+ resolution: {integrity: sha512-4/2S9bW1LvdsEPmxXN1OEAPFPbk7DvCx2c9d+TblkQAAvptGaSgtPWCByTEGgT8UxCxcVqes2aFPO5pzwo7R2w==}
+ peerDependencies:
+ cytoscape: ^3.2.0
+
+ cytoscape-dagre@2.5.0:
+ resolution: {integrity: sha512-VG2Knemmshop4kh5fpLO27rYcyUaaDkRw+6PiX4bstpB+QFt0p2oauMrsjVbUamGWQ6YNavh7x2em2uZlzV44g==}
+ peerDependencies:
+ cytoscape: ^3.2.22
+
+ cytoscape@3.30.3:
+ resolution: {integrity: sha512-HncJ9gGJbVtw7YXtIs3+6YAFSSiKsom0amWc33Z7QbylbY2JGMrA0yz4EwrdTScZxnwclXeEZHzO5pxoy0ZE4g==}
+ engines: {node: '>=0.10'}
+
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
+ d3-dispatch@1.0.6:
+ resolution: {integrity: sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==}
+
d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
+ d3-drag@1.2.5:
+ resolution: {integrity: sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==}
+
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
@@ -2920,10 +2970,22 @@ packages:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
+ d3-path@1.0.9:
+ resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
+
+ d3-selection@1.4.2:
+ resolution: {integrity: sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==}
+
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
+ d3-shape@1.3.7:
+ resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==}
+
+ d3-timer@1.0.10:
+ resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==}
+
d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
@@ -2938,6 +3000,9 @@ packages:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
+ dagre@0.8.5:
+ resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==}
+
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
@@ -3438,6 +3503,9 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ graphlib@2.1.8:
+ resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==}
+
handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'}
@@ -5333,6 +5401,9 @@ packages:
resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==}
engines: {node: '>=10.13.0'}
+ webcola@3.4.0:
+ resolution: {integrity: sha512-4BiLXjXw3SJHo3Xd+rF+7fyClT6n7I+AR6TkBqyQ4kTsePSAMDLRCXY1f3B/kXJeP9tYn4G1TblxTO+jAt0gaw==}
+
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -7418,6 +7489,12 @@ snapshots:
dependencies:
'@types/node': 22.9.0
+ '@types/cytoscape-dagre@2.3.3':
+ dependencies:
+ '@types/cytoscape': 3.21.8
+
+ '@types/cytoscape@3.21.8': {}
+
'@types/d3-array@3.2.1': {}
'@types/d3-axis@3.0.6':
@@ -7535,6 +7612,8 @@ snapshots:
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
+ '@types/dagre@0.7.52': {}
+
'@types/doctrine@0.0.9': {}
'@types/eslint-scope@3.7.7':
@@ -8428,10 +8507,29 @@ snapshots:
csstype@3.1.3: {}
+ cytoscape-cola@2.5.1(cytoscape@3.30.3):
+ dependencies:
+ cytoscape: 3.30.3
+ webcola: 3.4.0
+
+ cytoscape-dagre@2.5.0(cytoscape@3.30.3):
+ dependencies:
+ cytoscape: 3.30.3
+ dagre: 0.8.5
+
+ cytoscape@3.30.3: {}
+
d3-color@3.1.0: {}
+ d3-dispatch@1.0.6: {}
+
d3-dispatch@3.0.1: {}
+ d3-drag@1.2.5:
+ dependencies:
+ d3-dispatch: 1.0.6
+ d3-selection: 1.4.2
+
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
@@ -8443,8 +8541,18 @@ snapshots:
dependencies:
d3-color: 3.1.0
+ d3-path@1.0.9: {}
+
+ d3-selection@1.4.2: {}
+
d3-selection@3.0.0: {}
+ d3-shape@1.3.7:
+ dependencies:
+ d3-path: 1.0.9
+
+ d3-timer@1.0.10: {}
+
d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
@@ -8464,6 +8572,11 @@ snapshots:
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
+ dagre@0.8.5:
+ dependencies:
+ graphlib: 2.1.8
+ lodash: 4.17.21
+
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
@@ -9018,6 +9131,10 @@ snapshots:
graceful-fs@4.2.11: {}
+ graphlib@2.1.8:
+ dependencies:
+ lodash: 4.17.21
+
handlebars@4.7.8:
dependencies:
minimist: 1.2.8
@@ -10957,6 +11074,13 @@ snapshots:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
+ webcola@3.4.0:
+ dependencies:
+ d3-dispatch: 1.0.6
+ d3-drag: 1.2.5
+ d3-shape: 1.3.7
+ d3-timer: 1.0.10
+
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}