From 3027e9b24f8b7d4e4e9c48f1aa12b8776d19a217 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:34:25 +0100 Subject: [PATCH] fix: scrollable headers/timeline, CodeBlock for body, ELK node alignment - Make headers tab and timeline tab scrollable when content overflows - Replace custom
 code block with design system CodeBlock component
  for body tabs (Input/Output) to match existing styleguide
- Add LINEAR_SEGMENTS node placement strategy to ELK layout to fix
  Y-offset misalignment between nodes in left-to-right diagrams
  (e.g., ENDPOINT at different Y level than subsequent processors)

Co-Authored-By: Claude Opus 4.6 (1M context) 
---
 .../app/diagram/ElkDiagramRenderer.java       |  3 ++
 .../ExecutionDiagram.module.css               |  5 ++-
 .../ExecutionDiagram/tabs/BodyTab.tsx         | 43 +++++--------------
 3 files changed, 17 insertions(+), 34 deletions(-)

diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java
index 25234df2..ae357cb3 100644
--- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java
@@ -13,6 +13,7 @@ import org.eclipse.elk.core.RecursiveGraphLayoutEngine;
 import org.eclipse.elk.core.options.CoreOptions;
 import org.eclipse.elk.core.options.Direction;
 import org.eclipse.elk.core.options.HierarchyHandling;
+import org.eclipse.elk.alg.layered.options.NodePlacementStrategy;
 import org.eclipse.elk.core.util.BasicProgressMonitor;
 import org.eclipse.elk.graph.ElkBendPoint;
 import org.eclipse.elk.graph.ElkEdge;
@@ -181,6 +182,8 @@ public class ElkDiagramRenderer implements DiagramRenderer {
         rootNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING);
         rootNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING);
         rootNode.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
+        rootNode.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
+                NodePlacementStrategy.LINEAR_SEGMENTS);
 
         // Build index of all RouteNodes (flat list from graph + recursive children)
         Map routeNodeMap = new HashMap<>();
diff --git a/ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css b/ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css
index 4c50b751..dfd17468 100644
--- a/ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css
+++ b/ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css
@@ -293,12 +293,13 @@
   display: flex;
   gap: 0;
   min-height: 0;
+  overflow-y: auto;
+  max-height: 100%;
 }
 
 .headersColumn {
   flex: 1;
   min-width: 0;
-  overflow: hidden;
 }
 
 .headersColumn + .headersColumn {
@@ -457,6 +458,8 @@
   display: flex;
   flex-direction: column;
   gap: 2px;
+  overflow-y: auto;
+  max-height: 100%;
 }
 
 .ganttRow {
diff --git a/ui/src/components/ExecutionDiagram/tabs/BodyTab.tsx b/ui/src/components/ExecutionDiagram/tabs/BodyTab.tsx
index 1ef04535..20eae893 100644
--- a/ui/src/components/ExecutionDiagram/tabs/BodyTab.tsx
+++ b/ui/src/components/ExecutionDiagram/tabs/BodyTab.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { CodeBlock } from '@cameleer/design-system';
 import styles from '../ExecutionDiagram.module.css';
 
 interface BodyTabProps {
@@ -6,22 +6,22 @@ interface BodyTabProps {
   label: string;
 }
 
-function detectFormat(text: string): 'JSON' | 'XML' | 'Text' {
+function detectLanguage(text: string): string {
   const trimmed = text.trimStart();
   if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
     try {
       JSON.parse(text);
-      return 'JSON';
+      return 'json';
     } catch {
       // not valid JSON
     }
   }
-  if (trimmed.startsWith('<')) return 'XML';
-  return 'Text';
+  if (trimmed.startsWith('<')) return 'xml';
+  return 'text';
 }
 
-function formatBody(text: string, format: string): string {
-  if (format === 'JSON') {
+function formatBody(text: string, language: string): string {
+  if (language === 'json') {
     try {
       return JSON.stringify(JSON.parse(text), null, 2);
     } catch {
@@ -31,40 +31,17 @@ function formatBody(text: string, format: string): string {
   return text;
 }
 
-function byteSize(text: string): string {
-  const bytes = new TextEncoder().encode(text).length;
-  if (bytes < 1024) return `${bytes} B`;
-  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
-  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
-}
-
 export function BodyTab({ body, label }: BodyTabProps) {
-  const [copied, setCopied] = useState(false);
-
   if (!body) {
     return 
No {label.toLowerCase()} body available
; } - const format = detectFormat(body); - const formatted = formatBody(body, format); - - function handleCopy() { - navigator.clipboard.writeText(body!).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 1500); - }); - } + const language = detectLanguage(body); + const formatted = formatBody(body, language); return (
-
- {format} - {byteSize(body)} - -
-
{formatted}
+
); }