diff --git a/ui/package-lock.json b/ui/package-lock.json
index 366ccd10..a126f5aa 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -8,7 +8,7 @@
"name": "ui",
"version": "0.0.0",
"dependencies": {
- "@cameleer/design-system": "^0.0.0-snapshot.20260325.499c86b",
+ "@cameleer/design-system": "^0.1.10",
"@tanstack/react-query": "^5.90.21",
"openapi-fetch": "^0.17.0",
"react": "^19.2.4",
@@ -276,9 +276,9 @@
}
},
"node_modules/@cameleer/design-system": {
- "version": "0.0.0-snapshot.20260325.499c86b",
- "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.0.0-snapshot.20260325.499c86b/design-system-0.0.0-snapshot.20260325.499c86b.tgz",
- "integrity": "sha512-uiBdWYTT0wzIgL8QX21oHyb7xjeepnXvGAl/YHapd1o4u+GuXuB23kcyECurM/OTUN4dM7RGjGBp46Mbe8xcIQ==",
+ "version": "0.1.10",
+ "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.10/design-system-0.1.10.tgz",
+ "integrity": "sha512-1gQ8SM7AEgYv3UQNlC7qhcQ0HSIEFkYA34AV/hjDuFUQV3xrPI0g/p/YrJ5W4KIRb2YvnNqDPNgb6kLANufqRQ==",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/ui/package.json b/ui/package.json
index 9c7150a1..76a16096 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -14,7 +14,7 @@
"generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts"
},
"dependencies": {
- "@cameleer/design-system": "^0.0.0-snapshot.20260325.499c86b",
+ "@cameleer/design-system": "^0.1.10",
"@tanstack/react-query": "^5.90.21",
"openapi-fetch": "^0.17.0",
"react": "^19.2.4",
diff --git a/ui/src/api/queries/admin/opensearch.ts b/ui/src/api/queries/admin/opensearch.ts
index 1482b9cf..38859c2b 100644
--- a/ui/src/api/queries/admin/opensearch.ts
+++ b/ui/src/api/queries/admin/opensearch.ts
@@ -5,11 +5,11 @@ import { useRefreshInterval } from '../use-refresh-interval';
// ── Types ──────────────────────────────────────────────────────────────
export interface OpenSearchStatus {
- connected: boolean;
+ reachable: boolean;
clusterHealth: string;
version: string | null;
- numberOfNodes: number;
- url: string;
+ nodeCount: number;
+ host: string;
}
export interface PipelineStats {
diff --git a/ui/src/pages/Admin/OpenSearchAdminPage.tsx b/ui/src/pages/Admin/OpenSearchAdminPage.tsx
index 9f57c378..664e10cd 100644
--- a/ui/src/pages/Admin/OpenSearchAdminPage.tsx
+++ b/ui/src/pages/Admin/OpenSearchAdminPage.tsx
@@ -23,10 +23,10 @@ export default function OpenSearchAdminPage() {
OpenSearch Administration
-
+
-
+
{pipeline && (
diff --git a/ui/src/pages/Dashboard/Dashboard.tsx b/ui/src/pages/Dashboard/Dashboard.tsx
index fde1c879..30fcc1d1 100644
--- a/ui/src/pages/Dashboard/Dashboard.tsx
+++ b/ui/src/pages/Dashboard/Dashboard.tsx
@@ -12,7 +12,7 @@ import {
Badge,
useGlobalFilters,
} from '@cameleer/design-system'
-import type { Column, KpiItem, RouteNode } from '@cameleer/design-system'
+import type { Column, KpiItem } from '@cameleer/design-system'
import {
useSearchExecutions,
useExecutionStats,
@@ -21,7 +21,7 @@ import {
} from '../../api/queries/executions'
import { useDiagramLayout } from '../../api/queries/diagrams'
import type { ExecutionSummary } from '../../api/types'
-import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping'
+import { mapDiagramToRouteNodes, toFlowSegments } from '../../utils/diagram-mapping'
import styles from './Dashboard.module.css'
// Row type extends ExecutionSummary with an `id` field for DataTable
@@ -355,9 +355,9 @@ export default function Dashboard() {
: (detail.children ?? [])
: []
- const routeNodes: RouteNode[] = useMemo(() => {
+ const routeFlows = useMemo(() => {
if (diagram?.nodes) {
- return mapDiagramToRouteNodes(diagram.nodes || [], procList)
+ return toFlowSegments(mapDiagramToRouteNodes(diagram.nodes || [], procList)).flows
}
return []
}, [diagram, procList])
@@ -475,8 +475,8 @@ export default function Dashboard() {
Route Flow
- {routeNodes.length > 0 ? (
-
+ {routeFlows.length > 0 ? (
+
) : (
No diagram available
)}
diff --git a/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx b/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx
index 059d9ba6..bf5cfe58 100644
--- a/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx
+++ b/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx
@@ -8,7 +8,7 @@ import type { ProcessorStep, RouteNode, NodeBadge } from '@cameleer/design-syste
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'
import { useCorrelationChain } from '../../api/queries/correlation'
import { useDiagramLayout } from '../../api/queries/diagrams'
-import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping'
+import { mapDiagramToRouteNodes, toFlowSegments } from '../../utils/diagram-mapping'
import { useTracingStore } from '../../stores/tracing-store'
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'
import styles from './ExchangeDetail.module.css'
@@ -128,32 +128,36 @@ export default function ExchangeDetail() {
const inputBody = snapshot?.inputBody ?? null
const outputBody = snapshot?.outputBody ?? null
- // Build RouteFlow nodes from diagram + execution data
- const routeNodes: RouteNode[] = useMemo(() => {
+ // Build RouteFlow nodes from diagram + execution data, split into flow segments
+ const { routeFlows, flowIndexMap } = useMemo(() => {
+ let nodes: RouteNode[]
if (diagram?.nodes) {
// Flatten processors to build diagramNodeId → processorId lookup
const flatProcs: Array<{ diagramNodeId?: string; processorId?: string }> = []
- function flattenProcs(nodes: any[]) {
- for (const n of nodes) { flatProcs.push(n); if (n.children) flattenProcs(n.children) }
+ function flattenProcs(list: any[]) {
+ for (const n of list) { flatProcs.push(n); if (n.children) flattenProcs(n.children) }
}
flattenProcs(procList)
const pidLookup = new Map(flatProcs
.filter(p => p.diagramNodeId && p.processorId)
.map(p => [p.diagramNodeId!, p.processorId!]))
- return mapDiagramToRouteNodes(diagram.nodes, procList).map((node, i) => ({
+ nodes = mapDiagramToRouteNodes(diagram.nodes, procList).map((node, i) => ({
...node,
badges: badgesFor(pidLookup.get(diagram.nodes[i]?.id ?? '') ?? diagram.nodes[i]?.id ?? ''),
}))
+ } else {
+ // Fallback: build from processor list
+ nodes = processors.map((p) => ({
+ name: p.name,
+ type: 'process' as RouteNode['type'],
+ durationMs: p.durationMs,
+ status: p.status,
+ badges: badgesFor(p.name),
+ }))
}
- // Fallback: build from processor list
- return processors.map((p) => ({
- name: p.name,
- type: 'process' as RouteNode['type'],
- durationMs: p.durationMs,
- status: p.status,
- badges: badgesFor(p.name),
- }))
+ const { flows, indexMap } = toFlowSegments(nodes)
+ return { routeFlows: flows, flowIndexMap: indexMap }
}, [diagram, processors, procList, tracedMap])
// ProcessorId lookup: timeline index → processorId
@@ -388,13 +392,14 @@ export default function ExchangeDetail() {
No processor data available
)
) : (
- routeNodes.length > 0 ? (
+ routeFlows.length > 0 ? (
setSelectedProcessorIndex(index)}
- selectedIndex={activeIndex}
+ flows={routeFlows}
+ onNodeClick={(_node, index) => setSelectedProcessorIndex(flowIndexMap[index] ?? index)}
+ selectedIndex={flowIndexMap.indexOf(activeIndex)}
getActions={(_node, index) => {
- const pid = flowProcessorIds[index]
+ const origIdx = flowIndexMap[index] ?? index
+ const pid = flowProcessorIds[origIdx]
if (!pid || !detail?.applicationName) return []
return [{
label: tracingStore.isTraced(app, pid) ? 'Disable Tracing' : 'Enable Tracing',
diff --git a/ui/src/pages/Routes/RouteDetail.tsx b/ui/src/pages/Routes/RouteDetail.tsx
index d1205d3a..5dcb116c 100644
--- a/ui/src/pages/Routes/RouteDetail.tsx
+++ b/ui/src/pages/Routes/RouteDetail.tsx
@@ -21,7 +21,7 @@ import { useDiagramByRoute } from '../../api/queries/diagrams';
import { useProcessorMetrics } from '../../api/queries/processor-metrics';
import { useStatsTimeseries, useSearchExecutions, useExecutionStats } from '../../api/queries/executions';
import type { ExecutionSummary, AppCatalogEntry, RouteSummary } from '../../api/types';
-import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping';
+import { mapDiagramToRouteNodes, toFlowSegments } from '../../utils/diagram-mapping';
import styles from './RouteDetail.module.css';
// ── Row types ────────────────────────────────────────────────────────────────
@@ -321,9 +321,9 @@ export default function RouteDetail() {
}, [health]);
// Route flow from diagram
- const diagramNodes = useMemo(() => {
+ const diagramFlows = useMemo(() => {
if (!diagram?.nodes) return [];
- return mapDiagramToRouteNodes(diagram.nodes, []);
+ return toFlowSegments(mapDiagramToRouteNodes(diagram.nodes, [])).flows;
}, [diagram]);
// Processor table rows
@@ -458,8 +458,8 @@ export default function RouteDetail() {
Route Diagram
- {diagramNodes.length > 0 ? (
-
+ {diagramFlows.length > 0 ? (
+
) : (
No diagram available for this route.
@@ -497,12 +497,12 @@ export default function RouteDetail() {
{/* Route Flow section */}
- {diagramNodes.length > 0 && (
+ {diagramFlows.length > 0 && (
)}
diff --git a/ui/src/utils/diagram-mapping.ts b/ui/src/utils/diagram-mapping.ts
index 8f6146cb..b47b7292 100644
--- a/ui/src/utils/diagram-mapping.ts
+++ b/ui/src/utils/diagram-mapping.ts
@@ -1,4 +1,4 @@
-import type { RouteNode } from '@cameleer/design-system';
+import type { RouteNode, FlowSegment } from '@cameleer/design-system';
// Map NodeType strings to RouteNode types
function mapNodeType(type: string): RouteNode['type'] {
@@ -53,3 +53,29 @@ export function mapDiagramToRouteNodes(
};
});
}
+
+/**
+ * Splits a flat RouteNode[] into FlowSegment[] (main route + error handlers).
+ * Returns the segments and an index map: indexMap[newFlatIndex] = originalIndex.
+ */
+export function toFlowSegments(nodes: RouteNode[]): { flows: FlowSegment[]; indexMap: number[] } {
+ const mainIndices: number[] = [];
+ const errorIndices: number[] = [];
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].type === 'error-handler') {
+ errorIndices.push(i);
+ } else {
+ mainIndices.push(i);
+ }
+ }
+
+ const flows: FlowSegment[] = [
+ { label: 'Main Route', nodes: mainIndices.map(i => nodes[i]) },
+ ];
+ if (errorIndices.length > 0) {
+ flows.push({ label: 'Error Handler', nodes: errorIndices.map(i => nodes[i]), variant: 'error' });
+ }
+
+ const indexMap = [...mainIndices, ...errorIndices];
+ return { flows, indexMap };
+}