diff --git a/app/api/flowchart/route.ts b/app/api/flowchart/route.ts index 737dbfb..1c08a15 100644 --- a/app/api/flowchart/route.ts +++ b/app/api/flowchart/route.ts @@ -30,6 +30,8 @@ interface Server { id: number; name: string; ip: string; + host: boolean; + hostServer: number | null; } interface Application { @@ -43,11 +45,13 @@ const NODE_WIDTH = 220; const NODE_HEIGHT = 60; const APP_NODE_WIDTH = 160; const APP_NODE_HEIGHT = 40; -const HORIZONTAL_SPACING = 280; -const VERTICAL_SPACING = 60; +const HORIZONTAL_SPACING = 400; +const VERTICAL_SPACING = 100; const START_Y = 120; const ROOT_NODE_WIDTH = 300; const CONTAINER_PADDING = 40; +const COLUMN_SPACING = 200; +const VM_APP_SPACING = 200; export async function GET() { try { @@ -60,74 +64,131 @@ export async function GET() { }) as Promise, ]); - // Root Node - const rootNode: Node = { - id: "root", - type: "infrastructure", - data: { label: "My Infrastructure" }, - position: { x: 0, y: 0 }, - style: { - background: "#ffffff", - color: "#0f0f0f", - border: "2px solid #e6e4e1", - borderRadius: "8px", - padding: "16px", - width: ROOT_NODE_WIDTH, - height: NODE_HEIGHT, - fontSize: "1.2rem", - fontWeight: "bold", - }, - }; + // Level 2: Physical Servers + const serverNodes: Node[] = servers + .filter(server => !server.hostServer) + .map((server, index, filteredServers) => { + const xPos = + index * HORIZONTAL_SPACING - + ((filteredServers.length - 1) * HORIZONTAL_SPACING) / 2; - // Server Nodes - const serverNodes: Node[] = servers.map((server, index) => { - const xPos = - index * HORIZONTAL_SPACING - - ((servers.length - 1) * HORIZONTAL_SPACING) / 2; + return { + id: `server-${server.id}`, + type: "server", + data: { + label: `${server.name}\n${server.ip}`, + ...server, + }, + position: { x: xPos, y: START_Y }, + style: { + background: "#ffffff", + color: "#0f0f0f", + border: "2px solid #e6e4e1", + borderRadius: "4px", + padding: "8px", + width: NODE_WIDTH, + height: NODE_HEIGHT, + fontSize: "0.9rem", + lineHeight: "1.2", + whiteSpace: "pre-wrap", + }, + }; + }); - return { - id: `server-${server.id}`, - type: "server", - data: { - label: `${server.name}\n${server.ip}`, - ...server, - }, - position: { x: xPos, y: START_Y }, - style: { - background: "#ffffff", - color: "#0f0f0f", - border: "2px solid #e6e4e1", - borderRadius: "4px", - padding: "8px", - width: NODE_WIDTH, - height: NODE_HEIGHT, - fontSize: "0.9rem", - lineHeight: "1.2", - whiteSpace: "pre-wrap", - }, - }; - }); - - // Application Nodes - const appNodes: Node[] = []; + // Level 3: Services and VMs + const serviceNodes: Node[] = []; + const vmNodes: Node[] = []; + servers.forEach((server) => { const serverNode = serverNodes.find((n) => n.id === `server-${server.id}`); - const serverX = serverNode?.position.x || 0; - const xOffset = (NODE_WIDTH - APP_NODE_WIDTH) / 2; + if (serverNode) { + const serverX = serverNode.position.x; + + // Services (left column) + applications + .filter(app => app.serverId === server.id) + .forEach((app, appIndex) => { + serviceNodes.push({ + id: `service-${app.id}`, + type: "service", + data: { + label: `${app.name}\n${app.localURL}`, + ...app, + }, + position: { + x: serverX - COLUMN_SPACING, + y: START_Y + NODE_HEIGHT + VERTICAL_SPACING + appIndex * (APP_NODE_HEIGHT + 20), + }, + style: { + background: "#f0f9ff", + color: "#0f0f0f", + border: "2px solid #60a5fa", + borderRadius: "4px", + padding: "6px", + width: APP_NODE_WIDTH, + height: APP_NODE_HEIGHT, + fontSize: "0.8rem", + lineHeight: "1.1", + whiteSpace: "pre-wrap", + }, + }); + }); + // VMs (middle column) mit dynamischem Abstand + const hostVMs = servers.filter(vm => vm.hostServer === server.id); + let currentY = START_Y + NODE_HEIGHT + VERTICAL_SPACING; + + hostVMs.forEach(vm => { + const appCount = applications.filter(app => app.serverId === vm.id).length; + + vmNodes.push({ + id: `vm-${vm.id}`, + type: "vm", + data: { + label: `${vm.name}\n${vm.ip}`, + ...vm, + }, + position: { + x: serverX, + y: currentY, + }, + style: { + background: "#fef2f2", + color: "#0f0f0f", + border: "2px solid #fecaca", + borderRadius: "4px", + padding: "6px", + width: APP_NODE_WIDTH, + height: APP_NODE_HEIGHT, + fontSize: "0.8rem", + lineHeight: "1.1", + whiteSpace: "pre-wrap", + }, + }); + + // Dynamischer Abstand basierend auf Anzahl Apps + currentY += appCount * (APP_NODE_HEIGHT + 20) + 40; // 40px Basisabstand + }); + } + }); + + // Level 4: VM Applications (right column) + const vmAppNodes: Node[] = []; + vmNodes.forEach((vm) => { + const vmX = vm.position.x; applications - .filter((app) => app.serverId === server.id) + .filter(app => app.serverId === vm.data.id) .forEach((app, appIndex) => { - appNodes.push({ - id: `app-${app.id}`, + vmAppNodes.push({ + id: `vm-app-${app.id}`, type: "application", data: { label: `${app.name}\n${app.localURL}`, ...app, }, position: { - x: serverX + xOffset, - y: START_Y + NODE_HEIGHT + 30 + appIndex * VERTICAL_SPACING, + x: vmX + VM_APP_SPACING, + y: vm.position.y + appIndex * (APP_NODE_HEIGHT + 20), }, style: { background: "#f5f5f5", @@ -145,38 +206,14 @@ export async function GET() { }); }); - // Connections - const connections: Edge[] = [ - ...servers.map((server) => ({ - id: `conn-root-${server.id}`, - source: "root", - target: `server-${server.id}`, - type: "straight", - style: { - stroke: "#94a3b8", - strokeWidth: 2, - }, - })), - ...applications.map((app) => ({ - id: `conn-${app.serverId}-${app.id}`, - source: `server-${app.serverId}`, - target: `app-${app.id}`, - type: "straight", - style: { - stroke: "#60a5fa", - strokeWidth: 2, - }, - })), - ]; - - // Container Box - const allNodes = [rootNode, ...serverNodes, ...appNodes]; + // Calculate dimensions for root node positioning + const tempNodes = [...serverNodes, ...serviceNodes, ...vmNodes, ...vmAppNodes]; let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity; - allNodes.forEach((node) => { + tempNodes.forEach((node) => { const width = parseInt(node.style.width?.toString() || "0", 10); const height = parseInt(node.style.height?.toString() || "0", 10); @@ -186,17 +223,47 @@ export async function GET() { maxY = Math.max(maxY, node.position.y + height); }); + const centerX = (minX + maxX) / 2; + const rootX = centerX - ROOT_NODE_WIDTH / 2; + + // Level 1: Root Node (centered at top) + const rootNode: Node = { + id: "root", + type: "infrastructure", + data: { label: "My Infrastructure" }, + position: { x: rootX, y: 0 }, + style: { + background: "#ffffff", + color: "#0f0f0f", + border: "2px solid #e6e4e1", + borderRadius: "8px", + padding: "16px", + width: ROOT_NODE_WIDTH, + height: NODE_HEIGHT, + fontSize: "1.2rem", + fontWeight: "bold", + }, + }; + + // Update dimensions with root node + const allNodes = [rootNode, ...tempNodes]; + let newMinX = Math.min(minX, rootNode.position.x); + let newMaxX = Math.max(maxX, rootNode.position.x + ROOT_NODE_WIDTH); + let newMinY = Math.min(minY, rootNode.position.y); + let newMaxY = Math.max(maxY, rootNode.position.y + NODE_HEIGHT); + + // Container Node const containerNode: Node = { id: 'container', type: 'container', data: { label: '' }, position: { - x: minX - CONTAINER_PADDING, - y: minY - CONTAINER_PADDING + x: newMinX - CONTAINER_PADDING, + y: newMinY - CONTAINER_PADDING }, style: { - width: maxX - minX + 2 * CONTAINER_PADDING, - height: maxY - minY + 2 * CONTAINER_PADDING, + width: newMaxX - newMinX + 2 * CONTAINER_PADDING, + height: newMaxY - newMinY + 2 * CONTAINER_PADDING, background: 'transparent', border: '2px dashed #e2e8f0', borderRadius: '8px', @@ -207,6 +274,116 @@ export async function GET() { zIndex: -1, }; + // Connections with hierarchical chaining + const connections: Edge[] = []; + + // Root to Servers + serverNodes.forEach((server) => { + connections.push({ + id: `conn-root-${server.id}`, + source: "root", + target: server.id, + type: "straight", + style: { + stroke: "#94a3b8", + strokeWidth: 2, + }, + }); + }); + + // Services chaining + const servicesByServer = new Map(); + serviceNodes.forEach(service => { + const serverId = service.data.serverId; + if (!servicesByServer.has(serverId)) servicesByServer.set(serverId, []); + servicesByServer.get(serverId)!.push(service); + }); + servicesByServer.forEach((services, serverId) => { + services.sort((a, b) => a.position.y - b.position.y); + services.forEach((service, index) => { + if (index === 0) { + connections.push({ + id: `conn-service-${service.id}`, + source: `server-${serverId}`, + target: service.id, + type: "straight", + style: { stroke: "#60a5fa", strokeWidth: 2 }, + }); + } else { + const prevService = services[index - 1]; + connections.push({ + id: `conn-service-${service.id}-${prevService.id}`, + source: prevService.id, + target: service.id, + type: "straight", + style: { stroke: "#60a5fa", strokeWidth: 2 }, + }); + } + }); + }); + + // VMs chaining + const vmsByHost = new Map(); + vmNodes.forEach(vm => { + const hostId = vm.data.hostServer; + if (!vmsByHost.has(hostId)) vmsByHost.set(hostId, []); + vmsByHost.get(hostId)!.push(vm); + }); + vmsByHost.forEach((vms, hostId) => { + vms.sort((a, b) => a.position.y - b.position.y); + vms.forEach((vm, index) => { + if (index === 0) { + connections.push({ + id: `conn-vm-${vm.id}`, + source: `server-${hostId}`, + target: vm.id, + type: "straight", + style: { stroke: "#f87171", strokeWidth: 2 }, + }); + } else { + const prevVm = vms[index - 1]; + connections.push({ + id: `conn-vm-${vm.id}-${prevVm.id}`, + source: prevVm.id, + target: vm.id, + type: "straight", + style: { stroke: "#f87171", strokeWidth: 2 }, + }); + } + }); + }); + + // VM Applications chaining + const appsByVM = new Map(); + vmAppNodes.forEach(app => { + const vmId = app.data.serverId; + if (!appsByVM.has(vmId)) appsByVM.set(vmId, []); + appsByVM.get(vmId)!.push(app); + }); + appsByVM.forEach((apps, vmId) => { + apps.sort((a, b) => a.position.y - b.position.y); + apps.forEach((app, index) => { + if (index === 0) { + connections.push({ + id: `conn-vm-app-${app.id}`, + source: `vm-${vmId}`, + target: app.id, + type: "straight", + style: { stroke: "#f87171", strokeWidth: 2 }, + }); + } else { + const prevApp = apps[index - 1]; + connections.push({ + id: `conn-vm-app-${app.id}-${prevApp.id}`, + source: prevApp.id, + target: app.id, + type: "straight", + style: { stroke: "#f87171", strokeWidth: 2 }, + }); + } + }); + }); + return NextResponse.json({ nodes: [containerNode, ...allNodes], edges: connections,