From 20ee448f4e42af56d5523458ac95bc1f19595a19 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:34:58 +0100 Subject: [PATCH] fix: OpenSearch status field mismatch, adopt RouteFlow flows prop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix admin OpenSearch page always showing "Disconnected" by aligning frontend field names (reachable/nodeCount/host) with backend DTO. Update design system to v0.1.10 and adopt the new multi-flow RouteFlow API — error-handler nodes now render as labeled segments with error variant instead of relying on legacy auto-separation. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/package-lock.json | 8 ++-- ui/package.json | 2 +- ui/src/api/queries/admin/opensearch.ts | 6 +-- ui/src/pages/Admin/OpenSearchAdminPage.tsx | 4 +- ui/src/pages/Dashboard/Dashboard.tsx | 12 +++--- .../pages/ExchangeDetail/ExchangeDetail.tsx | 43 +++++++++++-------- ui/src/pages/Routes/RouteDetail.tsx | 14 +++--- ui/src/utils/diagram-mapping.ts | 28 +++++++++++- 8 files changed, 74 insertions(+), 43 deletions(-) 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 && (
Route Flow
- +
)} 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 }; +}