Files
cameleer-server/ui/src/components/ExecutionDiagram/ExecutionDiagram.tsx
hsiegeln e703a9d39d
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
fix(ui): remove exchange summary bar from ExecutionDiagram
2026-03-28 14:47:03 +01:00

194 lines
6.5 KiB
TypeScript

import { useCallback, useRef, useState } from 'react';
import type { NodeAction, NodeConfig } from '../ProcessDiagram/types';
import type { ExecutionDetail, ProcessorNode } from './types';
import { useExecutionDetail } from '../../api/queries/executions';
import { useDiagramLayout } from '../../api/queries/diagrams';
import { ProcessDiagram } from '../ProcessDiagram';
import { DetailPanel } from './DetailPanel';
import { useExecutionOverlay } from './useExecutionOverlay';
import { useIterationState } from './useIterationState';
import styles from './ExecutionDiagram.module.css';
interface ExecutionDiagramProps {
executionId: string;
executionDetail?: ExecutionDetail;
direction?: 'LR' | 'TB';
knownRouteIds?: Set<string>;
onNodeAction?: (nodeId: string, action: NodeAction) => void;
nodeConfigs?: Map<string, NodeConfig>;
className?: string;
}
function findProcessorInTree(
nodes: ProcessorNode[] | undefined,
processorId: string | null,
): ProcessorNode | null {
if (!nodes || !processorId) return null;
for (const n of nodes) {
if (n.processorId === processorId) return n;
if (n.children) {
const found = findProcessorInTree(n.children, processorId);
if (found) return found;
}
}
return null;
}
function findFailedProcessor(nodes: ProcessorNode[]): ProcessorNode | null {
for (const n of nodes) {
if (n.status === 'FAILED') return n;
if (n.children) {
const found = findFailedProcessor(n.children);
if (found) return found;
}
}
return null;
}
function statusBadgeClass(status: string): string {
const s = status?.toUpperCase();
if (s === 'COMPLETED') return `${styles.statusBadge} ${styles.statusCompleted}`;
if (s === 'FAILED') return `${styles.statusBadge} ${styles.statusFailed}`;
if (s === 'RUNNING') return `${styles.statusBadge} ${styles.statusRunning}`;
return styles.statusBadge;
}
export function ExecutionDiagram({
executionId,
executionDetail: externalDetail,
direction = 'LR',
knownRouteIds,
onNodeAction,
nodeConfigs,
className,
}: ExecutionDiagramProps) {
// 1. Fetch execution data (skip if pre-fetched prop provided)
const detailQuery = useExecutionDetail(externalDetail ? null : executionId);
const detail = externalDetail ?? detailQuery.data;
const detailLoading = !externalDetail && detailQuery.isLoading;
const detailError = !externalDetail && detailQuery.error;
// 2. Load diagram by content hash
const diagramQuery = useDiagramLayout(detail?.diagramContentHash ?? null, direction);
const diagramLayout = diagramQuery.data;
const diagramLoading = diagramQuery.isLoading;
const diagramError = diagramQuery.error;
// 3. Initialize iteration state
const { iterationState, setIteration } = useIterationState(detail?.processors);
// 4. Compute overlay
const overlay = useExecutionOverlay(detail?.processors, iterationState);
// 5. Manage selection + center-on-node
const [selectedProcessorId, setSelectedProcessorId] = useState<string>('');
const [centerOnNodeId, setCenterOnNodeId] = useState<string>('');
// 6. Resizable splitter state
const [splitPercent, setSplitPercent] = useState(60);
const containerRef = useRef<HTMLDivElement>(null);
const handleSplitterDown = useCallback((e: React.PointerEvent) => {
e.currentTarget.setPointerCapture(e.pointerId);
const container = containerRef.current;
if (!container) return;
const onMove = (me: PointerEvent) => {
const rect = container.getBoundingClientRect();
const y = me.clientY - rect.top;
const pct = Math.min(85, Math.max(30, (y / rect.height) * 100));
setSplitPercent(pct);
};
const onUp = () => {
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onUp);
};
document.addEventListener('pointermove', onMove);
document.addEventListener('pointerup', onUp);
}, []);
// Jump to error: find first FAILED processor, select it, and center the viewport
const handleJumpToError = useCallback(() => {
if (!detail?.processors) return;
const failed = findFailedProcessor(detail.processors);
if (failed?.processorId) {
setSelectedProcessorId(failed.processorId);
// Use a unique value to re-trigger centering even if the same node
setCenterOnNodeId('');
requestAnimationFrame(() => setCenterOnNodeId(failed.processorId));
}
}, [detail?.processors]);
// Loading state
if (detailLoading || (detail && diagramLoading)) {
return (
<div className={`${styles.executionDiagram} ${className ?? ''}`}>
<div className={styles.loadingState}>Loading execution data...</div>
</div>
);
}
// Error state
if (detailError) {
return (
<div className={`${styles.executionDiagram} ${className ?? ''}`}>
<div className={styles.errorState}>Failed to load execution detail</div>
</div>
);
}
if (diagramError) {
return (
<div className={`${styles.executionDiagram} ${className ?? ''}`}>
<div className={styles.errorState}>Failed to load diagram</div>
</div>
);
}
if (!detail) {
return (
<div className={`${styles.executionDiagram} ${className ?? ''}`}>
<div className={styles.loadingState}>No execution data</div>
</div>
);
}
return (
<div ref={containerRef} className={`${styles.executionDiagram} ${className ?? ''}`}>
{/* Diagram area */}
<div className={styles.diagramArea} style={{ height: `${splitPercent}%` }}>
<ProcessDiagram
application={detail.applicationName}
routeId={detail.routeId}
direction={direction}
diagramLayout={diagramLayout}
selectedNodeId={selectedProcessorId}
onNodeSelect={setSelectedProcessorId}
onNodeAction={onNodeAction}
nodeConfigs={nodeConfigs}
knownRouteIds={knownRouteIds}
executionOverlay={overlay}
iterationState={iterationState}
onIterationChange={setIteration}
centerOnNodeId={centerOnNodeId}
/>
</div>
{/* Resizable splitter */}
<div
className={styles.splitter}
onPointerDown={handleSplitterDown}
/>
{/* Detail panel */}
<div className={styles.detailArea} style={{ height: `${100 - splitPercent}%` }}>
<DetailPanel
selectedProcessor={findProcessorInTree(detail.processors, selectedProcessorId || null)}
executionDetail={detail}
executionId={executionId}
onSelectProcessor={setSelectedProcessorId}
/>
</div>
</div>
);
}