fix: OpenSearch status field mismatch, adopt RouteFlow flows prop
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) <noreply@anthropic.com>
This commit is contained in:
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -23,10 +23,10 @@ export default function OpenSearchAdminPage() {
|
||||
<h2 style={{ marginBottom: '1rem' }}>OpenSearch Administration</h2>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
||||
<StatCard label="Status" value={status?.connected ? 'Connected' : 'Disconnected'} accent={status?.connected ? 'success' : 'error'} />
|
||||
<StatCard label="Status" value={status?.reachable ? 'Connected' : 'Disconnected'} accent={status?.reachable ? 'success' : 'error'} />
|
||||
<StatCard label="Health" value={status?.clusterHealth ?? '—'} accent={status?.clusterHealth === 'green' ? 'success' : 'warning'} />
|
||||
<StatCard label="Version" value={status?.version ?? '—'} />
|
||||
<StatCard label="Nodes" value={status?.numberOfNodes ?? 0} />
|
||||
<StatCard label="Nodes" value={status?.nodeCount ?? 0} />
|
||||
</div>
|
||||
|
||||
{pipeline && (
|
||||
|
||||
@@ -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() {
|
||||
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>Route Flow</div>
|
||||
{routeNodes.length > 0 ? (
|
||||
<RouteFlow nodes={routeNodes} />
|
||||
{routeFlows.length > 0 ? (
|
||||
<RouteFlow flows={routeFlows} />
|
||||
) : (
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>No diagram available</div>
|
||||
)}
|
||||
|
||||
@@ -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() {
|
||||
<InfoCallout>No processor data available</InfoCallout>
|
||||
)
|
||||
) : (
|
||||
routeNodes.length > 0 ? (
|
||||
routeFlows.length > 0 ? (
|
||||
<RouteFlow
|
||||
nodes={routeNodes}
|
||||
onNodeClick={(_node, index) => 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',
|
||||
|
||||
@@ -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() {
|
||||
<div className={styles.diagramStatsGrid}>
|
||||
<div className={styles.diagramPane}>
|
||||
<div className={styles.paneTitle}>Route Diagram</div>
|
||||
{diagramNodes.length > 0 ? (
|
||||
<RouteFlow nodes={diagramNodes} />
|
||||
{diagramFlows.length > 0 ? (
|
||||
<RouteFlow flows={diagramFlows} />
|
||||
) : (
|
||||
<div className={styles.emptyText}>
|
||||
No diagram available for this route.
|
||||
@@ -497,12 +497,12 @@ export default function RouteDetail() {
|
||||
</div>
|
||||
|
||||
{/* Route Flow section */}
|
||||
{diagramNodes.length > 0 && (
|
||||
{diagramFlows.length > 0 && (
|
||||
<div className={styles.routeFlowSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
<span className={styles.tableTitle}>Route Flow</span>
|
||||
</div>
|
||||
<RouteFlow nodes={diagramNodes} />
|
||||
<RouteFlow flows={diagramFlows} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user