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}+