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: {}