Compare commits
5 Commits
77e87504d6
...
26de222884
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26de222884 | ||
|
|
2f2f93f37e | ||
|
|
1b9a3b84a0 | ||
|
|
c77de4a232 | ||
|
|
15b8c09e17 |
@@ -28,19 +28,6 @@ const TABS: { key: DetailTab; label: string }[] = [
|
|||||||
{ key: 'log', label: 'Log' },
|
{ key: 'log', label: 'Log' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function formatDuration(ms: number | undefined): string {
|
|
||||||
if (ms === undefined || ms === null) return '-';
|
|
||||||
if (ms < 1000) return `${ms}ms`;
|
|
||||||
return `${(ms / 1000).toFixed(1)}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusClass(status: string): string {
|
|
||||||
const s = status?.toUpperCase();
|
|
||||||
if (s === 'COMPLETED') return styles.statusCompleted;
|
|
||||||
if (s === 'FAILED') return styles.statusFailed;
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DetailPanel({
|
export function DetailPanel({
|
||||||
selectedProcessor,
|
selectedProcessor,
|
||||||
executionDetail,
|
executionDetail,
|
||||||
@@ -99,22 +86,11 @@ export function DetailPanel({
|
|||||||
if (activeTab === 'output' && !hasOutput) setActiveTab('info');
|
if (activeTab === 'output' && !hasOutput) setActiveTab('info');
|
||||||
}, [hasHeaders, hasInput, hasOutput, activeTab]);
|
}, [hasHeaders, hasInput, hasOutput, activeTab]);
|
||||||
|
|
||||||
// Header display
|
|
||||||
const headerName = selectedProcessor ? selectedProcessor.processorType : 'Exchange';
|
|
||||||
const headerStatus = selectedProcessor ? selectedProcessor.status : executionDetail.status;
|
|
||||||
const headerId = selectedProcessor ? selectedProcessor.processorId : executionDetail.executionId;
|
|
||||||
const headerDuration = selectedProcessor ? selectedProcessor.durationMs : executionDetail.durationMs;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.detailPanel}>
|
<div className={styles.detailPanel}>
|
||||||
{/* Processor / Exchange header bar */}
|
{/* Header bar */}
|
||||||
<div className={styles.processorHeader}>
|
<div className={styles.processorHeader}>
|
||||||
<span className={styles.processorName}>{headerName}</span>
|
<span className={styles.processorName}>Details</span>
|
||||||
<span className={`${styles.statusBadge} ${statusClass(headerStatus)}`}>
|
|
||||||
{headerStatus}
|
|
||||||
</span>
|
|
||||||
<span className={styles.processorId}>{headerId}</span>
|
|
||||||
<span className={styles.processorDuration}>{formatDuration(headerDuration)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab bar */}
|
{/* Tab bar */}
|
||||||
|
|||||||
@@ -61,6 +61,28 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.downloadBtn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 10;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: 1px solid var(--border, #E4DFD8);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-surface, #FFFFFF);
|
||||||
|
color: var(--text-secondary, #5C5347);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadBtn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: var(--bg-hover, #F5F0EA);
|
||||||
|
}
|
||||||
|
|
||||||
.splitter {
|
.splitter {
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: var(--border, #E4DFD8);
|
background: var(--border, #E4DFD8);
|
||||||
|
|||||||
@@ -120,6 +120,18 @@ export function ExecutionDiagram({
|
|||||||
}
|
}
|
||||||
}, [detail?.processors]);
|
}, [detail?.processors]);
|
||||||
|
|
||||||
|
const handleDownloadJson = useCallback(() => {
|
||||||
|
if (!detail) return;
|
||||||
|
const json = JSON.stringify(detail, null, 2);
|
||||||
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `execution-${executionId}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, [detail, executionId]);
|
||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
if (detailLoading || (detail && diagramLoading)) {
|
if (detailLoading || (detail && diagramLoading)) {
|
||||||
return (
|
return (
|
||||||
@@ -158,6 +170,13 @@ export function ExecutionDiagram({
|
|||||||
<div ref={containerRef} className={`${styles.executionDiagram} ${className ?? ''}`}>
|
<div ref={containerRef} className={`${styles.executionDiagram} ${className ?? ''}`}>
|
||||||
{/* Diagram area */}
|
{/* Diagram area */}
|
||||||
<div className={styles.diagramArea} style={{ height: `${splitPercent}%` }}>
|
<div className={styles.diagramArea} style={{ height: `${splitPercent}%` }}>
|
||||||
|
<button
|
||||||
|
className={styles.downloadBtn}
|
||||||
|
onClick={handleDownloadJson}
|
||||||
|
title="Download execution JSON"
|
||||||
|
>
|
||||||
|
↓ JSON
|
||||||
|
</button>
|
||||||
<ProcessDiagram
|
<ProcessDiagram
|
||||||
application={detail.applicationName}
|
application={detail.applicationName}
|
||||||
routeId={detail.routeId}
|
routeId={detail.routeId}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
||||||
import type { NodeConfig, LatencyHeatmapEntry } from './types';
|
import type { NodeConfig, LatencyHeatmapEntry } from './types';
|
||||||
import type { NodeExecutionState } from '../ExecutionDiagram/types';
|
import type { NodeExecutionState } from '../ExecutionDiagram/types';
|
||||||
import { colorForType, iconForType, type IconElement } from './node-colors';
|
import { colorForType, iconForType, type IconElement } from './node-colors';
|
||||||
import { ConfigBadge } from './ConfigBadge';
|
|
||||||
|
|
||||||
const TOP_BAR_HEIGHT = 6;
|
const TOP_BAR_HEIGHT = 6;
|
||||||
const TEXT_LEFT = 32;
|
const TEXT_LEFT = 32;
|
||||||
@@ -158,7 +158,7 @@ export function DiagramNode({
|
|||||||
{detail}
|
{detail}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
<text x={TEXT_LEFT} y={h - 5} fill="#1A7F8E" fontSize={9} fontStyle="italic">
|
<text x={TEXT_LEFT} y={TOP_BAR_HEIGHT + (detail && detail !== typeName ? 35 : 24)} fill="#1A7F8E" fontSize={9} fontStyle="italic">
|
||||||
→ {resolvedUri.split('?')[0]}
|
→ {resolvedUri.split('?')[0]}
|
||||||
</text>
|
</text>
|
||||||
</>
|
</>
|
||||||
@@ -176,38 +176,92 @@ export function DiagramNode({
|
|||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Config badges */}
|
{/* Inline badges row: hasTrace, hasTap, status — inside card, top-right */}
|
||||||
{(config || executionState?.hasTraceData) && (
|
{(() => {
|
||||||
<ConfigBadge nodeWidth={w} config={config ?? {}} hasTraceData={executionState?.hasTraceData} />
|
const BADGE_R = 6;
|
||||||
)}
|
const BADGE_D = BADGE_R * 2;
|
||||||
|
const BADGE_GAP = 3;
|
||||||
|
const cy = TOP_BAR_HEIGHT + BADGE_R + 2;
|
||||||
|
const showTrace = config?.traceEnabled || executionState?.hasTraceData;
|
||||||
|
const showTap = !!config?.tapExpression;
|
||||||
|
if (!showTrace && !showTap && !isCompleted && !isFailed) return null;
|
||||||
|
const badges: React.ReactNode[] = [];
|
||||||
|
let slot = 0;
|
||||||
|
|
||||||
{/* Execution overlay: status badge inside card, top-right corner */}
|
// Status badge (rightmost, only during overlay)
|
||||||
{isCompleted && (
|
const statusCx = w - BADGE_R - 4;
|
||||||
<>
|
if (isCompleted) {
|
||||||
<circle cx={w - 10} cy={TOP_BAR_HEIGHT + 8} r={6} fill="#3D7C47" />
|
badges.push(
|
||||||
<path
|
<g key="status">
|
||||||
d={`M${w - 13} ${TOP_BAR_HEIGHT + 8} l2 2 4-4`}
|
<circle cx={statusCx} cy={cy} r={BADGE_R} fill="#3D7C47" />
|
||||||
fill="none" stroke="white" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round"
|
<path d={`M${statusCx - 3} ${cy} l2 2 4-4`} fill="none" stroke="white" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||||
/>
|
</g>
|
||||||
</>
|
);
|
||||||
)}
|
slot++;
|
||||||
{isFailed && (
|
} else if (isFailed) {
|
||||||
<>
|
badges.push(
|
||||||
<circle cx={w - 10} cy={TOP_BAR_HEIGHT + 8} r={6} fill="none" stroke="#C0392B" strokeWidth={2} opacity={0.5}>
|
<g key="status">
|
||||||
<animate attributeName="r" values="6;14" dur="1.5s" repeatCount="indefinite" />
|
<circle cx={statusCx} cy={cy} r={BADGE_R} fill="none" stroke="#C0392B" strokeWidth={2} opacity={0.5}>
|
||||||
<animate attributeName="opacity" values="0.5;0" dur="1.5s" repeatCount="indefinite" />
|
<animate attributeName="r" values="6;14" dur="1.5s" repeatCount="indefinite" />
|
||||||
</circle>
|
<animate attributeName="opacity" values="0.5;0" dur="1.5s" repeatCount="indefinite" />
|
||||||
<circle cx={w - 10} cy={TOP_BAR_HEIGHT + 8} r={6} fill="none" stroke="#C0392B" strokeWidth={2} opacity={0.5}>
|
</circle>
|
||||||
<animate attributeName="r" values="6;14" dur="1.5s" begin="0.75s" repeatCount="indefinite" />
|
<circle cx={statusCx} cy={cy} r={BADGE_R} fill="none" stroke="#C0392B" strokeWidth={2} opacity={0.5}>
|
||||||
<animate attributeName="opacity" values="0.5;0" dur="1.5s" begin="0.75s" repeatCount="indefinite" />
|
<animate attributeName="r" values="6;14" dur="1.5s" begin="0.75s" repeatCount="indefinite" />
|
||||||
</circle>
|
<animate attributeName="opacity" values="0.5;0" dur="1.5s" begin="0.75s" repeatCount="indefinite" />
|
||||||
<circle cx={w - 10} cy={TOP_BAR_HEIGHT + 8} r={6} fill="#C0392B" />
|
</circle>
|
||||||
<path
|
<circle cx={statusCx} cy={cy} r={BADGE_R} fill="#C0392B" />
|
||||||
d={`M${w - 10} ${TOP_BAR_HEIGHT + 5} v4 M${w - 10} ${TOP_BAR_HEIGHT + 10.5} v0.5`}
|
<path d={`M${statusCx} ${cy - 3} v4 M${statusCx} ${cy + 2.5} v0.5`} fill="none" stroke="white" strokeWidth={1.5} strokeLinecap="round" />
|
||||||
fill="none" stroke="white" strokeWidth={1.5} strokeLinecap="round"
|
</g>
|
||||||
/>
|
);
|
||||||
</>
|
slot++;
|
||||||
)}
|
}
|
||||||
|
|
||||||
|
// Tap badge (before status)
|
||||||
|
if (showTap) {
|
||||||
|
const tapCx = statusCx - slot * (BADGE_D + BADGE_GAP);
|
||||||
|
badges.push(
|
||||||
|
<g key="tap">
|
||||||
|
<circle cx={tapCx} cy={cy} r={BADGE_R} fill="#7C3AED" />
|
||||||
|
<g transform={`translate(${tapCx - 5}, ${cy - 5})`} stroke="white" strokeWidth={1.4} fill="none" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M5 1 C5 1 2 4.5 2 6.5a3 3 0 006 0C8 4.5 5 1 5 1z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
slot++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace badge (leftmost)
|
||||||
|
if (showTrace) {
|
||||||
|
const traceCx = statusCx - slot * (BADGE_D + BADGE_GAP);
|
||||||
|
const tracePulse = overlayActive && executionState?.hasTraceData;
|
||||||
|
const traceHasData = executionState?.hasTraceData;
|
||||||
|
badges.push(
|
||||||
|
<g key="trace">
|
||||||
|
{tracePulse && (
|
||||||
|
<>
|
||||||
|
<circle cx={traceCx} cy={cy} r={BADGE_R} fill="none" stroke="#1A7F8E" strokeWidth={2} opacity={0.5}>
|
||||||
|
<animate attributeName="r" values={`${BADGE_R};${BADGE_R + 8}`} dur="1.5s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="opacity" values="0.5;0" dur="1.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx={traceCx} cy={cy} r={BADGE_R} fill="none" stroke="#1A7F8E" strokeWidth={2} opacity={0.5}>
|
||||||
|
<animate attributeName="r" values={`${BADGE_R};${BADGE_R + 8}`} dur="1.5s" begin="0.75s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="opacity" values="0.5;0" dur="1.5s" begin="0.75s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<circle cx={traceCx} cy={cy} r={BADGE_R} fill={traceHasData ? '#1A7F8E' : '#1A7F8E'} opacity={traceHasData ? 1 : 0.2} />
|
||||||
|
<g transform={`translate(${traceCx - 5}, ${cy - 5}) scale(${10/24})`} stroke={traceHasData ? 'white' : '#1A7F8E'} strokeWidth={2.4} fill="none" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z" />
|
||||||
|
<path d="M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z" />
|
||||||
|
<path d="M16 17h4" />
|
||||||
|
<path d="M4 13h4" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{badges}</>;
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Execution overlay: duration text at bottom-right */}
|
{/* Execution overlay: duration text at bottom-right */}
|
||||||
{executionState && statusColor && (
|
{executionState && statusColor && (
|
||||||
|
|||||||
@@ -152,13 +152,12 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
|||||||
return map;
|
return map;
|
||||||
}, [catalog]);
|
}, [catalog]);
|
||||||
|
|
||||||
// Build nodeConfigs from tracing store + app config (for TRACE/TAP badges)
|
// Build nodeConfigs from app config (for TRACE/TAP badges)
|
||||||
const { data: appConfig } = useApplicationConfig(appId);
|
const { data: appConfig } = useApplicationConfig(appId);
|
||||||
const tracedMap = useTracingStore((s) => s.tracedProcessors[appId]);
|
|
||||||
const nodeConfigs = useMemo(() => {
|
const nodeConfigs = useMemo(() => {
|
||||||
const map = new Map<string, NodeConfig>();
|
const map = new Map<string, NodeConfig>();
|
||||||
if (tracedMap) {
|
if (appConfig?.tracedProcessors) {
|
||||||
for (const pid of Object.keys(tracedMap)) {
|
for (const pid of Object.keys(appConfig.tracedProcessors)) {
|
||||||
map.set(pid, { traceEnabled: true });
|
map.set(pid, { traceEnabled: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +170,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [tracedMap, appConfig]);
|
}, [appConfig]);
|
||||||
|
|
||||||
// Processor options for tap modal dropdown
|
// Processor options for tap modal dropdown
|
||||||
const processorOptions = useMemo(() => {
|
const processorOptions = useMemo(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user