diff --git a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/env-services-page.tsx b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/env-services-page.tsx index 9ac4517..95ee3ca 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/env-services-page.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/env-services-page.tsx @@ -2,21 +2,11 @@ import { useQuery } from '@tanstack/react-query'; import { useTRPC } from '@/lib/trpc'; import PaddedSpinner from '@/components/padded-spinner'; -import { - Card, - CardAction, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; import { BreadCrumbs } from '@/components/app-page-context'; import PageTitle from '@/components/page-title'; import { Badge } from '@/components/ui/badge'; import { CreateService } from '@/app/dashboard/environments/[deploymentEnvId]/components/create-service'; -import { useRouter } from 'next/navigation'; -import ServiceSettingsDropdown from '@/app/dashboard/environments/[deploymentEnvId]/components/service-settings-dropdown'; -import CardsGrid from '@/components/cards-grid'; +import ServiceFlowDiagram from '@/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram'; export default function EnvServicesPage({ deploymentEnvId, @@ -24,7 +14,6 @@ export default function EnvServicesPage({ readonly deploymentEnvId: string; }) { const trpc = useTRPC(); - const router = useRouter(); const { data: services } = useQuery( trpc.services.listServices.queryOptions({ environmentId: deploymentEnvId, @@ -37,7 +26,7 @@ export default function EnvServicesPage({ ); if (!services || !env) return ; return ( - <> +
- - {services.map((service) => ( - - router.push(`/dashboard/services/${service.id}`) - } - className='cursor-pointer hover:bg-muted' - > - - {service.name} - {service.description && ( - - {service.description} - - )} - e.stopPropagation()}> - - - - - {service.server.name} - - - ))} - - + + ); } diff --git a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/flow-utils.ts b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/flow-utils.ts new file mode 100644 index 0000000..bbe291e --- /dev/null +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/flow-utils.ts @@ -0,0 +1,51 @@ +import dagre from 'dagre'; +import type { Node, Edge } from '@xyflow/react'; + +/** + * Calculate automatic layout using Dagre for a bipartite graph. + * Services are positioned on the left, networks on the right. + * + * @param nodes - Array of React Flow nodes (services and networks) + * @param edges - Array of React Flow edges connecting services to networks + * @returns Object containing layouted nodes and original edges + */ +export const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + + // Configure graph with increased spacing to prevent overlaps + dagreGraph.setGraph({ + rankdir: 'LR', // Left to right for bipartite graph (services -> networks) + ranksep: 200, // Increased spacing between ranks + nodesep: 55, // Spacing between nodes + edgesep: 30, // Spacing between edges + marginx: 50, // Margin on x-axis + marginy: 50, // Margin on y-axis + }); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { + width: 300, + height: 150, + }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + const layoutedNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + return { + ...node, + position: { + x: nodeWithPosition.x - 150, + y: nodeWithPosition.y - 75, + }, + }; + }); + + return { nodes: layoutedNodes as Node[], edges }; +}; diff --git a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/network-node.tsx b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/network-node.tsx new file mode 100644 index 0000000..6761842 --- /dev/null +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/network-node.tsx @@ -0,0 +1,36 @@ +import { Handle, Position } from '@xyflow/react'; +import { Card, CardHeader, CardTitle } from '@/components/ui/card'; +import { NetworkIcon } from 'lucide-react'; + +type NetworkNodeProps = Readonly<{ + data: { network: { id: string; name: string } }; + isConnectable: boolean; +}>; + +/** + * Network node component for the React Flow diagram. + * Displays network name as a smaller card on the right side of the graph. + * Has a target handle on the left for receiving connections from service nodes. + */ +export function NetworkNode({ + data: { network }, + isConnectable, +}: NetworkNodeProps) { + return ( + <> + + + + + + {network.name} + + + + + ); +} diff --git a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx new file mode 100644 index 0000000..1831b9d --- /dev/null +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -0,0 +1,180 @@ +'use client'; +import { useCallback, useMemo, useEffect } from 'react'; +import { + ReactFlow, + Background, + Controls, + Node, + Edge, + useNodesState, + useEdgesState, + ConnectionMode, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import { useRouter } from 'next/navigation'; +import { useTheme } from 'next-themes'; +import { inferProcedureOutput } from '@trpc/server'; +import { appRouter } from '@repo/api'; +import { getLayoutedElements } from './flow-utils'; +import { ServiceNode } from './service-node'; +import { NetworkNode } from './network-node'; + +type Service = inferProcedureOutput< + typeof appRouter.services.listServices +>[number]; + +type ServiceFlowDiagramProps = { + readonly services: Service[]; +}; + +/** + * ServiceFlowDiagram component displays services and networks as a bipartite graph. + * Services are shown on the left, networks on the right, with edges connecting + * services to their associated networks. Uses Dagre for automatic layout. + */ +export default function ServiceFlowDiagram({ + services, +}: ServiceFlowDiagramProps) { + const router = useRouter(); + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === 'dark'; + + // Create nodes and edges from services + const { nodes: initialNodes, edges: initialEdges } = useMemo(() => { + // Collect all unique networks + const networksMap = new Map(); + + services.forEach((service) => { + service.networks.forEach((network) => { + const networkId = String(network.id); + if (!networksMap.has(networkId)) { + networksMap.set(networkId, { + id: networkId, + name: String(network.name), + }); + } + }); + }); + + // Create service nodes + const serviceNodes: Node[] = services.map((service) => { + return { + id: service.id, + type: 'service', + data: { + service, + }, + position: { x: 0, y: 0 }, // Will be set by dagre layout + }; + }); + + // Create network nodes + const networkNodes: Node[] = Array.from(networksMap.values()).map( + (network) => { + return { + id: `network-${network.id}`, + type: 'network', + position: { x: 0, y: 0 }, // Will be set by dagre layout + data: { + network, + }, + }; + } + ); + + const nodes = [...serviceNodes, ...networkNodes]; + + // Create edges connecting services to networks + const edges: Edge[] = []; + + services.forEach((service) => { + service.networks.forEach((network) => { + const networkId = String(network.id); + const edgeId = `${service.id}|||network-${networkId}`; + + edges.push({ + id: edgeId, + source: service.id, + target: `network-${networkId}`, + type: 'bezier', + animated: true, + deletable: false, + selectable: false, + focusable: false, + style: { + stroke: 'oklch(0.556 0 0)', + strokeWidth: 2, + strokeDasharray: '5,5', + }, + }); + }); + }); + + // Apply automatic layout based on edges + return getLayoutedElements(nodes, edges); + }, [services]); + + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + // Update nodes and edges when they change + useEffect(() => { + setNodes(initialNodes); + setEdges(initialEdges); + }, [initialNodes, initialEdges, setNodes, setEdges]); + + const onNodeClick = useCallback( + (_event: React.MouseEvent, node: Node) => { + // Only navigate for service nodes, not network nodes + if (!node.id.startsWith('network-')) { + router.push(`/dashboard/services/${node.id}`); + } + }, + [router] + ); + + if (services.length === 0) { + return ( +
+ No services found in this environment +
+ ); + } + + return ( +
+ + + + +
+ ); +} diff --git a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-node.tsx b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-node.tsx new file mode 100644 index 0000000..8671c60 --- /dev/null +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-node.tsx @@ -0,0 +1,65 @@ +import { Handle, Position } from '@xyflow/react'; +import { Badge } from '@/components/ui/badge'; +import { + Card, + CardAction, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from '@/components/ui/card'; +import ServiceSettingsDropdown from './service-settings-dropdown'; +import { inferProcedureOutput } from '@trpc/server'; +import { appRouter } from '@repo/api'; + +type Service = inferProcedureOutput< + typeof appRouter.services.listServices +>[number]; + +type ServiceNodeProps = Readonly<{ + data: { service: Service }; + isConnectable: boolean; +}>; + +/** + * Service node component for the React Flow diagram. + * Displays service information including name, description, and server badge. + * Has a source handle on the right for connecting to network nodes. + */ +export function ServiceNode({ data: { service }, isConnectable }: ServiceNodeProps) { + return ( + <> + + + + + {service.name} + {service.description && ( + + {service.description} + + )} + e.stopPropagation()} + > + + + + + + {service.server.name} + + + + + ); +} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 422a091..0868352 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -119,3 +119,22 @@ @apply bg-background text-foreground; } } + +/* React Flow dark mode control buttons fix */ +.dark .react-flow__controls button { + background: oklch(0.269 0 0) !important; + border-color: oklch(1 0 0 / 10%) !important; + color: oklch(0.985 0 0) !important; +} + +.dark .react-flow__controls button:hover { + background: oklch(0.35 0 0) !important; +} + +.dark .react-flow__controls button svg { + fill: oklch(0.985 0 0) !important; +} + +.dark .react-flow__controls button path { + fill: oklch(0.985 0 0) !important; +} diff --git a/apps/web/package.json b/apps/web/package.json index 44406d5..dedb826 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "0.11.1", + "version": "0.11.2", "type": "module", "private": true, "scripts": { @@ -41,10 +41,12 @@ "@trpc/tanstack-react-query": "^11.7.2", "@types/pg": "^8.15.6", "@xterm/xterm": "^5.5.0", + "@xyflow/react": "^12.10.0", "better-auth": "^1.4.5", "bullmq": "^5.65.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dagre": "^0.8.5", "ioredis": "^5.8.2", "lucide-react": "^0.548.0", "next": "^16.0.6", @@ -67,6 +69,7 @@ "devDependencies": { "@repo/eslint-config": "workspace:*", "@repo/typescript-config": "workspace:*", + "@types/dagre": "^0.7.53", "@types/node": "^22.19.1", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", diff --git a/packages/api/src/routers/services/listServices.ts b/packages/api/src/routers/services/listServices.ts index a741281..5f0b4b7 100644 --- a/packages/api/src/routers/services/listServices.ts +++ b/packages/api/src/routers/services/listServices.ts @@ -25,6 +25,7 @@ export const listServices = protectedProcedure include: { swarmService: true, server: true, + networks: true, }, }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b659c4b..0737272 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,6 +192,9 @@ importers: '@xterm/xterm': specifier: ^5.5.0 version: 5.5.0 + '@xyflow/react': + specifier: ^12.10.0 + version: 12.10.0(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) better-auth: specifier: ^1.4.5 version: 1.4.5(next@16.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -204,6 +207,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + dagre: + specifier: ^0.8.5 + version: 0.8.5 ioredis: specifier: ^5.8.2 version: 5.8.2 @@ -265,6 +271,9 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config + '@types/dagre': + specifier: ^0.7.53 + version: 0.7.53 '@types/node': specifier: ^22.19.1 version: 22.19.1 @@ -2676,6 +2685,27 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/dagre@0.7.53': + resolution: {integrity: sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2910,6 +2940,15 @@ packages: '@xterm/xterm@5.5.0': resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} + '@xyflow/react@12.10.0': + resolution: {integrity: sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.74': + resolution: {integrity: sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3234,6 +3273,9 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -3327,6 +3369,47 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + dagre@0.8.5: + resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -3867,6 +3950,9 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + 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'} @@ -6123,6 +6209,21 @@ packages: zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -8085,6 +8186,29 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/d3-color@3.1.3': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/dagre@0.7.53': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -8319,6 +8443,29 @@ snapshots: '@xterm/xterm@5.5.0': {} + '@xyflow/react@12.10.0(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@xyflow/system': 0.0.74 + classcat: 5.0.5 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + zustand: 4.5.7(@types/react@19.2.0)(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.74': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -8691,6 +8838,8 @@ snapshots: dependencies: clsx: 2.1.1 + classcat@5.0.5: {} + clean-stack@2.2.0: {} client-only@0.0.1: {} @@ -8768,6 +8917,47 @@ snapshots: csstype@3.2.3: {} + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-selection@3.0.0: {} + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + 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-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -9487,6 +9677,10 @@ snapshots: graphemer@1.4.0: {} + graphlib@2.1.8: + dependencies: + lodash: 4.17.21 + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -12400,4 +12594,11 @@ snapshots: zod@4.1.13: {} + zustand@4.5.7(@types/react@19.2.0)(react@19.2.0): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.0 + react: 19.2.0 + zwitch@2.0.4: {}