From 38f49edb875ef3277b7741be37d476c733b4b0be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 01:33:42 +0000 Subject: [PATCH 01/15] Initial plan From f4eb29e186785da09369bbc28b8e4c115b9c9f4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 01:45:25 +0000 Subject: [PATCH 02/15] Implement React Flow diagram for services with network connections Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- .../components/env-services-page.tsx | 40 +--- .../components/service-flow-diagram.tsx | 213 ++++++++++++++++++ apps/web/package.json | 1 + .../api/src/routers/services/listServices.ts | 1 + pnpm-lock.yaml | 175 ++++++++++++++ 5 files changed, 392 insertions(+), 38 deletions(-) create mode 100644 apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx 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..0e44fcb 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, @@ -60,32 +49,7 @@ export default function EnvServicesPage({ - - {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/service-flow-diagram.tsx b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx new file mode 100644 index 0000000..468ad09 --- /dev/null +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -0,0 +1,213 @@ +'use client'; +import { useCallback, useMemo } from 'react'; +import { + ReactFlow, + Background, + Controls, + MiniMap, + Node, + Edge, + useNodesState, + useEdgesState, + addEdge, + Connection, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import { useRouter } from 'next/navigation'; +import { Badge } from '@/components/ui/badge'; +import { useTheme } from 'next-themes'; +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 ServiceFlowDiagramProps = { + readonly services: Service[]; +}; + +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(() => { + // Group services by networks + const networkToServices = new Map(); + + services.forEach((service) => { + service.networks.forEach((network) => { + if (!networkToServices.has(network.id)) { + networkToServices.set(network.id, []); + } + networkToServices.get(network.id)!.push(service); + }); + }); + + // Create nodes for services + const nodes: Node[] = services.map((service, index) => { + const row = Math.floor(index / 3); + const col = index % 3; + + return { + id: service.id, + type: 'default', + position: { x: col * 350, y: row * 200 }, + data: { + label: ( +
+
+
+ {service.name} +
+
e.stopPropagation()}> + +
+
+ {service.description && ( +
+ {service.description} +
+ )} +
+ + {service.server.name} + +
+
+ ), + }, + style: { + background: isDark ? 'oklch(0.205 0 0)' : 'oklch(1 0 0)', + border: `1px solid ${isDark ? 'oklch(1 0 0 / 10%)' : 'oklch(0.922 0 0)'}`, + borderRadius: '0.625rem', + padding: 0, + width: 300, + cursor: 'pointer', + }, + }; + }); + + // Create edges based on shared networks + const edges: Edge[] = []; + const edgeMap = new Map(); + + networkToServices.forEach((servicesInNetwork, networkId) => { + const network = services + .flatMap((s) => s.networks) + .find((n) => n.id === networkId); + + if (!network || servicesInNetwork.length < 2) return; + + // Connect all services in the same network + for (let i = 0; i < servicesInNetwork.length; i++) { + for (let j = i + 1; j < servicesInNetwork.length; j++) { + const sourceService = servicesInNetwork[i]; + const targetService = servicesInNetwork[j]; + if (!sourceService || !targetService) continue; + + const sourceId = sourceService.id; + const targetId = targetService.id; + const edgeId = [sourceId, targetId].sort().join('-'); + + if (!edgeMap.has(edgeId)) { + edgeMap.set(edgeId, { networks: [] }); + } + edgeMap.get(edgeId)!.networks.push(network.name); + } + } + }); + + edgeMap.forEach(({ networks }, edgeId) => { + const [source, target] = edgeId.split('-'); + if (!source || !target) return; + + edges.push({ + id: edgeId, + source, + target, + label: networks.join(', '), + type: 'smoothstep', + animated: false, + style: { + stroke: isDark ? 'oklch(0.488 0.243 264.376)' : 'oklch(0.205 0 0)', + strokeWidth: 2, + }, + labelStyle: { + fill: isDark ? 'oklch(0.985 0 0)' : 'oklch(0.145 0 0)', + fontSize: 12, + fontWeight: 500, + }, + labelBgStyle: { + fill: isDark ? 'oklch(0.205 0 0)' : 'oklch(1 0 0)', + fillOpacity: 0.9, + }, + }); + }); + + return { nodes, edges }; + }, [services, isDark]); + + const [nodes, , onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + const onConnect = useCallback( + (params: Connection) => setEdges((eds) => addEdge(params, eds)), + [setEdges] + ); + + const onNodeClick = useCallback( + (_event: React.MouseEvent, node: Node) => { + router.push(`/dashboard/services/${node.id}`); + }, + [router] + ); + + if (services.length === 0) { + return ( +
+ No services found in this environment +
+ ); + } + + return ( +
+ + + + + +
+ ); +} diff --git a/apps/web/package.json b/apps/web/package.json index 44406d5..1cbc30e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -41,6 +41,7 @@ "@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", 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..afe3a52 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) @@ -2676,6 +2679,24 @@ 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/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2910,6 +2931,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 +3264,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 +3360,44 @@ 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'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -6123,6 +6194,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 +8171,27 @@ 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/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -8319,6 +8426,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 +8821,8 @@ snapshots: dependencies: clsx: 2.1.1 + classcat@5.0.5: {} + clean-stack@2.2.0: {} client-only@0.0.1: {} @@ -8768,6 +8900,42 @@ 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) + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -12400,4 +12568,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: {} From 68be1a714d8e6909b75ac49762d4396c999aac5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 01:49:05 +0000 Subject: [PATCH 03/15] Address code review feedback: remove unnecessary null checks, disable edge creation, remove hideAttribution Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- .../components/service-flow-diagram.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) 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 index 468ad09..6e6a5e3 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -9,8 +9,6 @@ import { Edge, useNodesState, useEdgesState, - addEdge, - Connection, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { useRouter } from 'next/navigation'; @@ -109,7 +107,6 @@ export default function ServiceFlowDiagram({ for (let j = i + 1; j < servicesInNetwork.length; j++) { const sourceService = servicesInNetwork[i]; const targetService = servicesInNetwork[j]; - if (!sourceService || !targetService) continue; const sourceId = sourceService.id; const targetId = targetService.id; @@ -125,7 +122,6 @@ export default function ServiceFlowDiagram({ edgeMap.forEach(({ networks }, edgeId) => { const [source, target] = edgeId.split('-'); - if (!source || !target) return; edges.push({ id: edgeId, @@ -154,12 +150,7 @@ export default function ServiceFlowDiagram({ }, [services, isDark]); const [nodes, , onNodesChange] = useNodesState(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - - const onConnect = useCallback( - (params: Connection) => setEdges((eds) => addEdge(params, eds)), - [setEdges] - ); + const [edges, , onEdgesChange] = useEdgesState(initialEdges); const onNodeClick = useCallback( (_event: React.MouseEvent, node: Node) => { @@ -183,11 +174,9 @@ export default function ServiceFlowDiagram({ edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} - onConnect={onConnect} onNodeClick={onNodeClick} fitView attributionPosition="bottom-left" - proOptions={{ hideAttribution: true }} > Date: Fri, 5 Dec 2025 02:16:43 +0000 Subject: [PATCH 04/15] Fix React Flow: remove minimap, fix controls styling, enable theme updates, prevent edge editing, add logging Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- .../components/service-flow-diagram.tsx | 60 ++++++++++++------- apps/web/app/globals.css | 15 +++++ 2 files changed, 55 insertions(+), 20 deletions(-) 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 index 6e6a5e3..bff6a8b 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -1,10 +1,9 @@ 'use client'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useEffect } from 'react'; import { ReactFlow, Background, Controls, - MiniMap, Node, Edge, useNodesState, @@ -40,10 +39,14 @@ export default function ServiceFlowDiagram({ services.forEach((service) => { service.networks.forEach((network) => { - if (!networkToServices.has(network.id)) { - networkToServices.set(network.id, []); + const networkId = String(network.id); + if (!networkToServices.has(networkId)) { + networkToServices.set(networkId, []); + } + const servicesArray = networkToServices.get(networkId); + if (servicesArray) { + servicesArray.push(service); } - networkToServices.get(network.id)!.push(service); }); }); @@ -98,7 +101,7 @@ export default function ServiceFlowDiagram({ networkToServices.forEach((servicesInNetwork, networkId) => { const network = services .flatMap((s) => s.networks) - .find((n) => n.id === networkId); + .find((n) => String(n.id) === networkId); if (!network || servicesInNetwork.length < 2) return; @@ -115,21 +118,31 @@ export default function ServiceFlowDiagram({ if (!edgeMap.has(edgeId)) { edgeMap.set(edgeId, { networks: [] }); } - edgeMap.get(edgeId)!.networks.push(network.name); + const edgeData = edgeMap.get(edgeId); + if (edgeData) { + edgeData.networks.push(String(network.name)); + } } } }); edgeMap.forEach(({ networks }, edgeId) => { - const [source, target] = edgeId.split('-'); + const parts = edgeId.split('-'); + const source = parts[0]; + const target = parts[1]; + + if (!source || !target) return; edges.push({ id: edgeId, - source, - target, + source: source, + target: target, label: networks.join(', '), type: 'smoothstep', animated: false, + deletable: false, + selectable: false, + focusable: false, style: { stroke: isDark ? 'oklch(0.488 0.243 264.376)' : 'oklch(0.205 0 0)', strokeWidth: 2, @@ -146,11 +159,20 @@ export default function ServiceFlowDiagram({ }); }); + console.log('ServiceFlowDiagram - Created edges:', edges.length, edges); + console.log('ServiceFlowDiagram - Created nodes:', nodes.length, nodes.map(n => n.id)); + return { nodes, edges }; }, [services, isDark]); - const [nodes, , onNodesChange] = useNodesState(initialNodes); - const [edges, , onEdgesChange] = useEdgesState(initialEdges); + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + // Update nodes and edges when theme changes + useEffect(() => { + setNodes(initialNodes); + setEdges(initialEdges); + }, [initialNodes, initialEdges, setNodes, setEdges]); const onNodeClick = useCallback( (_event: React.MouseEvent, node: Node) => { @@ -177,20 +199,18 @@ export default function ServiceFlowDiagram({ onNodeClick={onNodeClick} fitView attributionPosition="bottom-left" + edgesUpdatable={false} + edgesFocusable={false} + nodesDraggable={true} + nodesConnectable={false} + elementsSelectable={true} > - Date: Fri, 5 Dec 2025 02:25:43 +0000 Subject: [PATCH 05/15] Fix dark mode controls with proper CSS selectors and enhance edge debugging Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- .../components/service-flow-diagram.tsx | 12 ++++++++--- apps/web/app/globals.css | 20 +++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) 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 index bff6a8b..d537daa 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -131,7 +131,12 @@ export default function ServiceFlowDiagram({ const source = parts[0]; const target = parts[1]; - if (!source || !target) return; + if (!source || !target) { + console.warn('Invalid edge ID:', edgeId); + return; + } + + console.log(`Creating edge from ${source} to ${target} with networks:`, networks); edges.push({ id: edgeId, @@ -159,8 +164,10 @@ export default function ServiceFlowDiagram({ }); }); + console.log('ServiceFlowDiagram - networkToServices:', Array.from(networkToServices.entries())); + console.log('ServiceFlowDiagram - edgeMap:', Array.from(edgeMap.entries())); console.log('ServiceFlowDiagram - Created edges:', edges.length, edges); - console.log('ServiceFlowDiagram - Created nodes:', nodes.length, nodes.map(n => n.id)); + console.log('ServiceFlowDiagram - Created nodes:', nodes.length, nodes.map(n => ({ id: n.id, name: nodes.find(node => node.id === n.id)?.data.label }))); return { nodes, edges }; }, [services, isDark]); @@ -210,7 +217,6 @@ export default function ServiceFlowDiagram({ gap={16} /> Date: Fri, 5 Dec 2025 02:30:36 +0000 Subject: [PATCH 06/15] Fix edge creation by using proper delimiter for UUID-based edge IDs Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- .../[deploymentEnvId]/components/service-flow-diagram.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index d537daa..a09dd93 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -113,7 +113,7 @@ export default function ServiceFlowDiagram({ const sourceId = sourceService.id; const targetId = targetService.id; - const edgeId = [sourceId, targetId].sort().join('-'); + const edgeId = [sourceId, targetId].sort().join('|||'); if (!edgeMap.has(edgeId)) { edgeMap.set(edgeId, { networks: [] }); @@ -127,7 +127,7 @@ export default function ServiceFlowDiagram({ }); edgeMap.forEach(({ networks }, edgeId) => { - const parts = edgeId.split('-'); + const parts = edgeId.split('|||'); const source = parts[0]; const target = parts[1]; From 268fc74af4daf5b2ea1bfc75c6d5f54bbed489b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 02:41:35 +0000 Subject: [PATCH 07/15] Use gray dotted edges, remove debug logging, and add automatic graph layout with Dagre Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- .../components/service-flow-diagram.tsx | 55 +++++++++++++------ apps/web/package.json | 2 + pnpm-lock.yaml | 26 +++++++++ 3 files changed, 67 insertions(+), 16 deletions(-) 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 index a09dd93..c8a66b6 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -16,6 +16,7 @@ import { useTheme } from 'next-themes'; import ServiceSettingsDropdown from './service-settings-dropdown'; import { inferProcedureOutput } from '@trpc/server'; import { appRouter } from '@repo/api'; +import dagre from 'dagre'; type Service = inferProcedureOutput< typeof appRouter.services.listServices @@ -25,6 +26,36 @@ type ServiceFlowDiagramProps = { readonly services: Service[]; }; +// Function to calculate automatic layout using Dagre +const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + dagreGraph.setGraph({ rankdir: 'LR', ranksep: 150, nodesep: 100 }); + + 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, edges }; +}; + export default function ServiceFlowDiagram({ services, }: ServiceFlowDiagramProps) { @@ -50,15 +81,12 @@ export default function ServiceFlowDiagram({ }); }); - // Create nodes for services - const nodes: Node[] = services.map((service, index) => { - const row = Math.floor(index / 3); - const col = index % 3; - + // Create nodes for services (initial positioning will be updated by layout) + const nodes: Node[] = services.map((service) => { return { id: service.id, type: 'default', - position: { x: col * 350, y: row * 200 }, + position: { x: 0, y: 0 }, // Will be set by dagre layout data: { label: (
@@ -132,12 +160,9 @@ export default function ServiceFlowDiagram({ const target = parts[1]; if (!source || !target) { - console.warn('Invalid edge ID:', edgeId); return; } - console.log(`Creating edge from ${source} to ${target} with networks:`, networks); - edges.push({ id: edgeId, source: source, @@ -149,8 +174,9 @@ export default function ServiceFlowDiagram({ selectable: false, focusable: false, style: { - stroke: isDark ? 'oklch(0.488 0.243 264.376)' : 'oklch(0.205 0 0)', + stroke: isDark ? 'oklch(0.556 0 0)' : 'oklch(0.556 0 0)', strokeWidth: 2, + strokeDasharray: '5,5', }, labelStyle: { fill: isDark ? 'oklch(0.985 0 0)' : 'oklch(0.145 0 0)', @@ -164,12 +190,9 @@ export default function ServiceFlowDiagram({ }); }); - console.log('ServiceFlowDiagram - networkToServices:', Array.from(networkToServices.entries())); - console.log('ServiceFlowDiagram - edgeMap:', Array.from(edgeMap.entries())); - console.log('ServiceFlowDiagram - Created edges:', edges.length, edges); - console.log('ServiceFlowDiagram - Created nodes:', nodes.length, nodes.map(n => ({ id: n.id, name: nodes.find(node => node.id === n.id)?.data.label }))); - - return { nodes, edges }; + // Apply automatic layout based on edges + const layouted = getLayoutedElements(nodes, edges); + return layouted; }, [services, isDark]); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); diff --git a/apps/web/package.json b/apps/web/package.json index 1cbc30e..4881568 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -46,6 +46,7 @@ "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", @@ -68,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/pnpm-lock.yaml b/pnpm-lock.yaml index afe3a52..0737272 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,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 @@ -268,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 @@ -2697,6 +2703,9 @@ packages: '@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==} @@ -3398,6 +3407,9 @@ packages: 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'} @@ -3938,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'} @@ -8192,6 +8207,8 @@ snapshots: '@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 @@ -8936,6 +8953,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-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -9655,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 From e7e13e68e12dfa3bb6787be0825ab7cc805f8c7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 02:52:48 +0000 Subject: [PATCH 08/15] Fix build error with undefined check and improve layout to prevent edge overlaps Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- .../components/service-flow-diagram.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 index c8a66b6..5a46a09 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -30,7 +30,16 @@ type ServiceFlowDiagramProps = { const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); - dagreGraph.setGraph({ rankdir: 'LR', ranksep: 150, nodesep: 100 }); + + // Configure graph with increased spacing to prevent overlaps + dagreGraph.setGraph({ + rankdir: 'TB', // Top to bottom for better vertical spacing + ranksep: 200, // Increased spacing between ranks + nodesep: 150, // Increased spacing between nodes + edgesep: 50, // 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 }); @@ -139,6 +148,8 @@ export default function ServiceFlowDiagram({ const sourceService = servicesInNetwork[i]; const targetService = servicesInNetwork[j]; + if (!sourceService || !targetService) continue; + const sourceId = sourceService.id; const targetId = targetService.id; const edgeId = [sourceId, targetId].sort().join('|||'); From 5de70196554e7e18f88c90516678837490a6b84e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:23:01 +0000 Subject: [PATCH 09/15] Create network nodes and connect services to networks instead of service-to-service connections Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- .../components/service-flow-diagram.tsx | 156 +++++++++--------- 1 file changed, 74 insertions(+), 82 deletions(-) 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 index 5a46a09..a2d676b 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -33,16 +33,21 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { // Configure graph with increased spacing to prevent overlaps dagreGraph.setGraph({ - rankdir: 'TB', // Top to bottom for better vertical spacing + rankdir: 'LR', // Left to right for bipartite graph (services -> networks) ranksep: 200, // Increased spacing between ranks - nodesep: 150, // Increased spacing between nodes - edgesep: 50, // Spacing between edges + nodesep: 100, // 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 }); + // Different sizes for service and network nodes + const isNetworkNode = node.id.startsWith('network-'); + dagreGraph.setNode(node.id, { + width: isNetworkNode ? 200 : 300, + height: isNetworkNode ? 80 : 150 + }); }); edges.forEach((edge) => { @@ -53,11 +58,12 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { const layoutedNodes = nodes.map((node) => { const nodeWithPosition = dagreGraph.node(node.id); + const isNetworkNode = node.id.startsWith('network-'); return { ...node, position: { - x: nodeWithPosition.x - 150, - y: nodeWithPosition.y - 75, + x: nodeWithPosition.x - (isNetworkNode ? 100 : 150), + y: nodeWithPosition.y - (isNetworkNode ? 40 : 75), }, }; }); @@ -74,24 +80,23 @@ export default function ServiceFlowDiagram({ // Create nodes and edges from services const { nodes: initialNodes, edges: initialEdges } = useMemo(() => { - // Group services by networks - const networkToServices = new Map(); + // Collect all unique networks + const networksMap = new Map(); services.forEach((service) => { service.networks.forEach((network) => { const networkId = String(network.id); - if (!networkToServices.has(networkId)) { - networkToServices.set(networkId, []); - } - const servicesArray = networkToServices.get(networkId); - if (servicesArray) { - servicesArray.push(service); + if (!networksMap.has(networkId)) { + networksMap.set(networkId, { + id: networkId, + name: String(network.name), + }); } }); }); - // Create nodes for services (initial positioning will be updated by layout) - const nodes: Node[] = services.map((service) => { + // Create service nodes + const serviceNodes: Node[] = services.map((service) => { return { id: service.id, type: 'default', @@ -131,73 +136,57 @@ export default function ServiceFlowDiagram({ }; }); - // Create edges based on shared networks - const edges: Edge[] = []; - const edgeMap = new Map(); - - networkToServices.forEach((servicesInNetwork, networkId) => { - const network = services - .flatMap((s) => s.networks) - .find((n) => String(n.id) === networkId); - - if (!network || servicesInNetwork.length < 2) return; - - // Connect all services in the same network - for (let i = 0; i < servicesInNetwork.length; i++) { - for (let j = i + 1; j < servicesInNetwork.length; j++) { - const sourceService = servicesInNetwork[i]; - const targetService = servicesInNetwork[j]; - - if (!sourceService || !targetService) continue; - - const sourceId = sourceService.id; - const targetId = targetService.id; - const edgeId = [sourceId, targetId].sort().join('|||'); - - if (!edgeMap.has(edgeId)) { - edgeMap.set(edgeId, { networks: [] }); - } - const edgeData = edgeMap.get(edgeId); - if (edgeData) { - edgeData.networks.push(String(network.name)); - } - } - } - }); - - edgeMap.forEach(({ networks }, edgeId) => { - const parts = edgeId.split('|||'); - const source = parts[0]; - const target = parts[1]; - - if (!source || !target) { - return; - } - - edges.push({ - id: edgeId, - source: source, - target: target, - label: networks.join(', '), - type: 'smoothstep', - animated: false, - deletable: false, - selectable: false, - focusable: false, - style: { - stroke: isDark ? 'oklch(0.556 0 0)' : 'oklch(0.556 0 0)', - strokeWidth: 2, - strokeDasharray: '5,5', - }, - labelStyle: { - fill: isDark ? 'oklch(0.985 0 0)' : 'oklch(0.145 0 0)', - fontSize: 12, - fontWeight: 500, + // Create network nodes + const networkNodes: Node[] = Array.from(networksMap.values()).map((network) => { + return { + id: `network-${network.id}`, + type: 'default', + position: { x: 0, y: 0 }, // Will be set by dagre layout + data: { + label: ( +
+
+ {network.name} +
+
+ ), }, - labelBgStyle: { - fill: isDark ? 'oklch(0.205 0 0)' : 'oklch(1 0 0)', - fillOpacity: 0.9, + style: { + background: isDark ? 'oklch(0.269 0 0)' : 'oklch(0.97 0 0)', + border: `2px solid ${isDark ? 'oklch(0.488 0.243 264.376)' : 'oklch(0.646 0.222 41.116)'}`, + borderRadius: '0.625rem', + padding: 0, + width: 200, + cursor: 'default', }, + }; + }); + + 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: 'smoothstep', + animated: false, + deletable: false, + selectable: false, + focusable: false, + style: { + stroke: isDark ? 'oklch(0.556 0 0)' : 'oklch(0.556 0 0)', + strokeWidth: 2, + strokeDasharray: '5,5', + }, + }); }); }); @@ -217,7 +206,10 @@ export default function ServiceFlowDiagram({ const onNodeClick = useCallback( (_event: React.MouseEvent, node: Node) => { - router.push(`/dashboard/services/${node.id}`); + // Only navigate for service nodes, not network nodes + if (!node.id.startsWith('network-')) { + router.push(`/dashboard/services/${node.id}`); + } }, [router] ); From e216e8f99981e161ac179e63eb04ff0e89da83c7 Mon Sep 17 00:00:00 2001 From: SimonLoir Date: Fri, 5 Dec 2025 12:46:34 +0100 Subject: [PATCH 10/15] chore: enhance service flow diagram and improve code consistency - Added `ConnectionMode.Loose` to ReactFlow configuration for flexible edge connections. - Enabled edge animations and improved styling for better visualization. - Standardized CSS classes to single quotes for consistency. - Refactored layout generation to enhance readability and maintainability. - Improved graph spacing and node dimensions for optimized presentation. --- .../components/service-flow-diagram.tsx | 111 ++++++++++-------- 1 file changed, 61 insertions(+), 50 deletions(-) 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 index a2d676b..275cf9b 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -8,6 +8,7 @@ import { Edge, useNodesState, useEdgesState, + ConnectionMode, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { useRouter } from 'next/navigation'; @@ -30,23 +31,23 @@ type ServiceFlowDiagramProps = { const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); - + // Configure graph with increased spacing to prevent overlaps - dagreGraph.setGraph({ + dagreGraph.setGraph({ rankdir: 'LR', // Left to right for bipartite graph (services -> networks) - ranksep: 200, // Increased spacing between ranks - nodesep: 100, // Spacing between nodes - edgesep: 30, // Spacing between edges - marginx: 50, // Margin on x-axis - marginy: 50, // Margin on y-axis + ranksep: 200, // Increased spacing between ranks + nodesep: 100, // Spacing between nodes + edgesep: 30, // Spacing between edges + marginx: 50, // Margin on x-axis + marginy: 50, // Margin on y-axis }); nodes.forEach((node) => { // Different sizes for service and network nodes const isNetworkNode = node.id.startsWith('network-'); - dagreGraph.setNode(node.id, { - width: isNetworkNode ? 200 : 300, - height: isNetworkNode ? 80 : 150 + dagreGraph.setNode(node.id, { + width: isNetworkNode ? 200 : 300, + height: isNetworkNode ? 80 : 150, }); }); @@ -82,7 +83,7 @@ export default function ServiceFlowDiagram({ 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); @@ -103,22 +104,24 @@ export default function ServiceFlowDiagram({ position: { x: 0, y: 0 }, // Will be set by dagre layout data: { label: ( -
-
-
+
+
+
{service.name}
e.stopPropagation()}> - +
{service.description && ( -
+
{service.description}
)} -
- +
+ {service.server.name}
@@ -137,52 +140,58 @@ export default function ServiceFlowDiagram({ }); // Create network nodes - const networkNodes: Node[] = Array.from(networksMap.values()).map((network) => { - return { - id: `network-${network.id}`, - type: 'default', - position: { x: 0, y: 0 }, // Will be set by dagre layout - data: { - label: ( -
-
- {network.name} + const networkNodes: Node[] = Array.from(networksMap.values()).map( + (network) => { + return { + id: `network-${network.id}`, + type: 'default', + position: { x: 0, y: 0 }, // Will be set by dagre layout + data: { + label: ( +
+
+ {network.name} +
-
- ), - }, - style: { - background: isDark ? 'oklch(0.269 0 0)' : 'oklch(0.97 0 0)', - border: `2px solid ${isDark ? 'oklch(0.488 0.243 264.376)' : 'oklch(0.646 0.222 41.116)'}`, - borderRadius: '0.625rem', - padding: 0, - width: 200, - cursor: 'default', - }, - }; - }); + ), + }, + style: { + background: isDark + ? 'oklch(0.269 0 0)' + : 'oklch(0.97 0 0)', + border: `2px solid ${isDark ? 'oklch(0.488 0.243 264.376)' : 'oklch(0.646 0.222 41.116)'}`, + borderRadius: '0.625rem', + padding: 0, + width: 200, + cursor: 'default', + }, + }; + } + ); 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: 'smoothstep', - animated: false, + animated: true, deletable: false, selectable: false, focusable: false, style: { - stroke: isDark ? 'oklch(0.556 0 0)' : 'oklch(0.556 0 0)', + stroke: isDark + ? 'oklch(0.556 0 0)' + : 'oklch(0.556 0 0)', strokeWidth: 2, strokeDasharray: '5,5', }, @@ -216,14 +225,14 @@ export default function ServiceFlowDiagram({ if (services.length === 0) { return ( -
+
No services found in this environment
); } return ( -
+
From 72f4f858e5e9491169c1b919043d1e1ef0b4a3be Mon Sep 17 00:00:00 2001 From: SimonLoir Date: Fri, 5 Dec 2025 13:26:39 +0100 Subject: [PATCH 11/15] chore: enhance service flow diagram and refactor nodes rendering - Updated node and network rendering to use `Card` components for improved UI consistency. - Introduced `service` and `network` node types with enhanced styling and interaction capabilities. - Adjusted graph layout parameters for better spacing and alignment. - Refactored edge rendering to use `bezier` for smoother connections. - Improved `ServiceFlowDiagram` component structure, simplifying layout logic. - Updated `env-services-page` to wrap content in a structured container (`div` with `space-y-4`). --- .../components/env-services-page.tsx | 4 +- .../components/service-flow-diagram.tsx | 147 +++++++++++------- 2 files changed, 89 insertions(+), 62 deletions(-) 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 0e44fcb..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 @@ -26,7 +26,7 @@ export default function EnvServicesPage({ ); if (!services || !env) return ; return ( - <> +
- +
); } 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 index 275cf9b..0855244 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -9,6 +9,8 @@ import { useNodesState, useEdgesState, ConnectionMode, + Position, + Handle, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { useRouter } from 'next/navigation'; @@ -18,6 +20,14 @@ import ServiceSettingsDropdown from './service-settings-dropdown'; import { inferProcedureOutput } from '@trpc/server'; import { appRouter } from '@repo/api'; import dagre from 'dagre'; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; type Service = inferProcedureOutput< typeof appRouter.services.listServices @@ -36,10 +46,10 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { dagreGraph.setGraph({ rankdir: 'LR', // Left to right for bipartite graph (services -> networks) ranksep: 200, // Increased spacing between ranks - nodesep: 100, // Spacing between nodes - edgesep: 30, // Spacing between edges - marginx: 50, // Margin on x-axis - marginy: 50, // Margin on y-axis + nodesep: 50, // Spacing between nodes + edgesep: 100, // Spacing between edges + marginx: 0, // Margin on x-axis + marginy: 0, // Margin on y-axis }); nodes.forEach((node) => { @@ -100,42 +110,11 @@ export default function ServiceFlowDiagram({ const serviceNodes: Node[] = services.map((service) => { return { id: service.id, - type: 'default', - position: { x: 0, y: 0 }, // Will be set by dagre layout + type: 'service', data: { - label: ( -
-
-
- {service.name} -
-
e.stopPropagation()}> - -
-
- {service.description && ( -
- {service.description} -
- )} -
- - {service.server.name} - -
-
- ), - }, - style: { - background: isDark ? 'oklch(0.205 0 0)' : 'oklch(1 0 0)', - border: `1px solid ${isDark ? 'oklch(1 0 0 / 10%)' : 'oklch(0.922 0 0)'}`, - borderRadius: '0.625rem', - padding: 0, - width: 300, - cursor: 'pointer', + service, }, + position: { x: 0, y: 0 }, // Will be set by dagre layout }; }); @@ -144,26 +123,10 @@ export default function ServiceFlowDiagram({ (network) => { return { id: `network-${network.id}`, - type: 'default', + type: 'network', position: { x: 0, y: 0 }, // Will be set by dagre layout data: { - label: ( -
-
- {network.name} -
-
- ), - }, - style: { - background: isDark - ? 'oklch(0.269 0 0)' - : 'oklch(0.97 0 0)', - border: `2px solid ${isDark ? 'oklch(0.488 0.243 264.376)' : 'oklch(0.646 0.222 41.116)'}`, - borderRadius: '0.625rem', - padding: 0, - width: 200, - cursor: 'default', + network, }, }; } @@ -183,7 +146,7 @@ export default function ServiceFlowDiagram({ id: edgeId, source: service.id, target: `network-${networkId}`, - type: 'smoothstep', + type: 'bezier', animated: true, deletable: false, selectable: false, @@ -200,8 +163,7 @@ export default function ServiceFlowDiagram({ }); // Apply automatic layout based on edges - const layouted = getLayoutedElements(nodes, edges); - return layouted; + return getLayoutedElements(nodes, edges); }, [services, isDark]); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); @@ -240,12 +202,76 @@ export default function ServiceFlowDiagram({ onEdgesChange={onEdgesChange} onNodeClick={onNodeClick} fitView - attributionPosition='bottom-left' + attributionPosition='bottom-right' edgesFocusable={false} nodesDraggable={true} nodesConnectable={false} elementsSelectable={true} connectionMode={ConnectionMode.Loose} + nodeTypes={{ + service: ({ + data: { service }, + isConnectable, + }: Readonly<{ + data: { service: Service }; + isConnectable: boolean; + }>) => ( + <> + + + + + {service.name} + {service.description && ( + + {service.description} + + )} + e.stopPropagation()} + > + + + + + + {service.server.name} + + + + + ), + network: ({ + data: { network }, + isConnectable, + }: Readonly<{ + data: { network: Service['networks'][number] }; + isConnectable: boolean; + }>) => ( + <> + + + + + {network.name} + + + + ), + }} >
From 4dc7f68f4f3c4f9afd04fc0dd08ee47ee2678ff3 Mon Sep 17 00:00:00 2001 From: SimonLoir Date: Fri, 5 Dec 2025 13:33:36 +0100 Subject: [PATCH 12/15] chore: remove unused margin properties from ServiceFlowDiagram configuration --- .../[deploymentEnvId]/components/service-flow-diagram.tsx | 2 -- 1 file changed, 2 deletions(-) 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 index 0855244..745e80f 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -48,8 +48,6 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { ranksep: 200, // Increased spacing between ranks nodesep: 50, // Spacing between nodes edgesep: 100, // Spacing between edges - marginx: 0, // Margin on x-axis - marginy: 0, // Margin on y-axis }); nodes.forEach((node) => { From a7e8d8f0e343068dee00bdd0909decc11ca689a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:27:52 +0000 Subject: [PATCH 13/15] Refactor: Extract components and utils into separate files, fix network node handle type Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- .../components/flow-utils.ts | 54 +++++++ .../components/network-node.tsx | 34 +++++ .../components/service-flow-diagram.tsx | 140 ++---------------- .../components/service-node.tsx | 65 ++++++++ 4 files changed, 168 insertions(+), 125 deletions(-) create mode 100644 apps/web/app/dashboard/environments/[deploymentEnvId]/components/flow-utils.ts create mode 100644 apps/web/app/dashboard/environments/[deploymentEnvId]/components/network-node.tsx create mode 100644 apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-node.tsx 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..0c876b1 --- /dev/null +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/flow-utils.ts @@ -0,0 +1,54 @@ +import dagre from 'dagre'; +import { 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: 100, // Spacing between nodes + edgesep: 30, // Spacing between edges + marginx: 50, // Margin on x-axis + marginy: 50, // Margin on y-axis + }); + + nodes.forEach((node) => { + // Different sizes for service and network nodes + const isNetworkNode = node.id.startsWith('network-'); + dagreGraph.setNode(node.id, { + width: isNetworkNode ? 200 : 300, + height: isNetworkNode ? 80 : 150, + }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + const layoutedNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + const isNetworkNode = node.id.startsWith('network-'); + return { + ...node, + position: { + x: nodeWithPosition.x - (isNetworkNode ? 100 : 150), + y: nodeWithPosition.y - (isNetworkNode ? 40 : 75), + }, + }; + }); + + return { nodes: layoutedNodes, 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..9e0558f --- /dev/null +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/network-node.tsx @@ -0,0 +1,34 @@ +import { Handle, Position } from '@xyflow/react'; +import { + Card, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +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 index 745e80f..aa6184b 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -9,25 +9,15 @@ import { useNodesState, useEdgesState, ConnectionMode, - Position, - Handle, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { useRouter } from 'next/navigation'; -import { Badge } from '@/components/ui/badge'; import { useTheme } from 'next-themes'; -import ServiceSettingsDropdown from './service-settings-dropdown'; import { inferProcedureOutput } from '@trpc/server'; import { appRouter } from '@repo/api'; -import dagre from 'dagre'; -import { - Card, - CardAction, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import { getLayoutedElements } from './flow-utils'; +import { ServiceNode } from './service-node'; +import { NetworkNode } from './network-node'; type Service = inferProcedureOutput< typeof appRouter.services.listServices @@ -37,49 +27,11 @@ type ServiceFlowDiagramProps = { readonly services: Service[]; }; -// Function to calculate automatic layout using Dagre -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: 50, // Spacing between nodes - edgesep: 100, // Spacing between edges - }); - - nodes.forEach((node) => { - // Different sizes for service and network nodes - const isNetworkNode = node.id.startsWith('network-'); - dagreGraph.setNode(node.id, { - width: isNetworkNode ? 200 : 300, - height: isNetworkNode ? 80 : 150, - }); - }); - - edges.forEach((edge) => { - dagreGraph.setEdge(edge.source, edge.target); - }); - - dagre.layout(dagreGraph); - - const layoutedNodes = nodes.map((node) => { - const nodeWithPosition = dagreGraph.node(node.id); - const isNetworkNode = node.id.startsWith('network-'); - return { - ...node, - position: { - x: nodeWithPosition.x - (isNetworkNode ? 100 : 150), - y: nodeWithPosition.y - (isNetworkNode ? 40 : 75), - }, - }; - }); - - return { nodes: layoutedNodes, edges }; -}; - +/** + * 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) { @@ -144,15 +96,13 @@ export default function ServiceFlowDiagram({ id: edgeId, source: service.id, target: `network-${networkId}`, - type: 'bezier', - animated: true, + type: 'smoothstep', + animated: false, deletable: false, selectable: false, focusable: false, style: { - stroke: isDark - ? 'oklch(0.556 0 0)' - : 'oklch(0.556 0 0)', + stroke: 'oklch(0.556 0 0)', strokeWidth: 2, strokeDasharray: '5,5', }, @@ -162,12 +112,12 @@ export default function ServiceFlowDiagram({ // Apply automatic layout based on edges return getLayoutedElements(nodes, edges); - }, [services, isDark]); + }, [services]); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - // Update nodes and edges when theme changes + // Update nodes and edges when they change useEffect(() => { setNodes(initialNodes); setEdges(initialEdges); @@ -207,68 +157,8 @@ export default function ServiceFlowDiagram({ elementsSelectable={true} connectionMode={ConnectionMode.Loose} nodeTypes={{ - service: ({ - data: { service }, - isConnectable, - }: Readonly<{ - data: { service: Service }; - isConnectable: boolean; - }>) => ( - <> - - - - - {service.name} - {service.description && ( - - {service.description} - - )} - e.stopPropagation()} - > - - - - - - {service.server.name} - - - - - ), - network: ({ - data: { network }, - isConnectable, - }: Readonly<{ - data: { network: Service['networks'][number] }; - isConnectable: boolean; - }>) => ( - <> - - - - - {network.name} - - - - ), + service: ServiceNode, + network: NetworkNode, }} > [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} + + + + + ); +} From 951872e224ce03ef28b77fbaca3048013d2d129a Mon Sep 17 00:00:00 2001 From: Simon Loir Date: Fri, 5 Dec 2025 19:00:50 +0100 Subject: [PATCH 14/15] fix: use "as" instead of type inference --- .../components/flow-utils.ts | 19 ++++++++----------- .../components/network-node.tsx | 18 ++++++++++-------- .../components/service-flow-diagram.tsx | 4 ++-- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/flow-utils.ts b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/flow-utils.ts index 0c876b1..bbe291e 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/flow-utils.ts +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/flow-utils.ts @@ -1,10 +1,10 @@ import dagre from 'dagre'; -import { Node, Edge } from '@xyflow/react'; +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 @@ -17,18 +17,16 @@ export const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { dagreGraph.setGraph({ rankdir: 'LR', // Left to right for bipartite graph (services -> networks) ranksep: 200, // Increased spacing between ranks - nodesep: 100, // Spacing between nodes + 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) => { - // Different sizes for service and network nodes - const isNetworkNode = node.id.startsWith('network-'); dagreGraph.setNode(node.id, { - width: isNetworkNode ? 200 : 300, - height: isNetworkNode ? 80 : 150, + width: 300, + height: 150, }); }); @@ -40,15 +38,14 @@ export const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { const layoutedNodes = nodes.map((node) => { const nodeWithPosition = dagreGraph.node(node.id); - const isNetworkNode = node.id.startsWith('network-'); return { ...node, position: { - x: nodeWithPosition.x - (isNetworkNode ? 100 : 150), - y: nodeWithPosition.y - (isNetworkNode ? 40 : 75), + x: nodeWithPosition.x - 150, + y: nodeWithPosition.y - 75, }, }; }); - return { nodes: layoutedNodes, edges }; + 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 index 9e0558f..6761842 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/network-node.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/network-node.tsx @@ -1,9 +1,6 @@ import { Handle, Position } from '@xyflow/react'; -import { - Card, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import { Card, CardHeader, CardTitle } from '@/components/ui/card'; +import { NetworkIcon } from 'lucide-react'; type NetworkNodeProps = Readonly<{ data: { network: { id: string; name: string } }; @@ -15,7 +12,10 @@ type NetworkNodeProps = Readonly<{ * 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) { +export function NetworkNode({ + data: { network }, + isConnectable, +}: NetworkNodeProps) { return ( <> - + - {network.name} + + {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 index aa6184b..1831b9d 100644 --- a/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx +++ b/apps/web/app/dashboard/environments/[deploymentEnvId]/components/service-flow-diagram.tsx @@ -96,8 +96,8 @@ export default function ServiceFlowDiagram({ id: edgeId, source: service.id, target: `network-${networkId}`, - type: 'smoothstep', - animated: false, + type: 'bezier', + animated: true, deletable: false, selectable: false, focusable: false, From 67ee612c49bdecf864264855dda4951f72410647 Mon Sep 17 00:00:00 2001 From: Simon Loir Date: Fri, 5 Dec 2025 19:04:38 +0100 Subject: [PATCH 15/15] chore: bump package version --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 4881568..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": {