fix: Dashboard DetailPanel uses flat scrollable layout matching mock
Changed from tabs-based to children-based DetailPanel layout: - Flat scrollable sections: Open Details → Overview → Errors → Route Flow → Processor Timeline - Title shows "route — exchangeId" matching mock pattern - Removed unused state (detailTab, processorIdx) - Added panelSectionMeta CSS for duration display in timeline header Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,18 @@
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelSectionMeta {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.overviewGrid {
|
.overviewGrid {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
Alert, Collapsible, CodeBlock, ShortcutsBar,
|
Alert, Collapsible, CodeBlock, ShortcutsBar,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
import type { Column } from '@cameleer/design-system';
|
import type { Column } from '@cameleer/design-system';
|
||||||
import { useSearchExecutions, useExecutionStats, useStatsTimeseries, useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
|
import { useSearchExecutions, useExecutionStats, useStatsTimeseries, useExecutionDetail } from '../../api/queries/executions';
|
||||||
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
||||||
import { useGlobalFilters } from '@cameleer/design-system';
|
import { useGlobalFilters } from '@cameleer/design-system';
|
||||||
import type { ExecutionSummary } from '../../api/types';
|
import type { ExecutionSummary } from '../../api/types';
|
||||||
@@ -28,8 +28,6 @@ export default function Dashboard() {
|
|||||||
const timeTo = timeRange.end.toISOString();
|
const timeTo = timeRange.end.toISOString();
|
||||||
|
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const [detailTab, setDetailTab] = useState('overview');
|
|
||||||
const [processorIdx, setProcessorIdx] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const timeWindowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000;
|
const timeWindowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000;
|
||||||
|
|
||||||
@@ -42,7 +40,6 @@ export default function Dashboard() {
|
|||||||
offset: 0, limit: 50,
|
offset: 0, limit: 50,
|
||||||
}, true);
|
}, true);
|
||||||
const { data: detail } = useExecutionDetail(selectedId);
|
const { data: detail } = useExecutionDetail(selectedId);
|
||||||
const { data: snapshot } = useProcessorSnapshot(selectedId, processorIdx);
|
|
||||||
const { data: diagram } = useDiagramByRoute(detail?.groupName, detail?.routeId);
|
const { data: diagram } = useDiagramByRoute(detail?.groupName, detail?.routeId);
|
||||||
|
|
||||||
const rows: Row[] = useMemo(() =>
|
const rows: Row[] = useMemo(() =>
|
||||||
@@ -109,95 +106,7 @@ export default function Dashboard() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const detailTabs = detail ? [
|
const procList = detail ? (detail.processors?.length ? detail.processors : (detail.children ?? [])) : [];
|
||||||
{
|
|
||||||
label: 'Overview', value: 'overview',
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<div className={styles.panelSection}>
|
|
||||||
<button
|
|
||||||
className={styles.openDetailLink}
|
|
||||||
onClick={() => navigate(`/exchanges/${detail.executionId}`)}
|
|
||||||
>
|
|
||||||
Open full details →
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.panelSection}>
|
|
||||||
<div className={styles.panelSectionTitle}>Overview</div>
|
|
||||||
<div className={styles.overviewGrid}>
|
|
||||||
<div className={styles.overviewRow}>
|
|
||||||
<span className={styles.overviewLabel}>Status</span>
|
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
||||||
<StatusDot variant={detail.status === 'COMPLETED' ? 'success' : 'error'} />
|
|
||||||
<span>{detail.status}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.overviewRow}>
|
|
||||||
<span className={styles.overviewLabel}>Duration</span>
|
|
||||||
<MonoText size="sm">{formatDuration(detail.durationMs)}</MonoText>
|
|
||||||
</div>
|
|
||||||
<div className={styles.overviewRow}>
|
|
||||||
<span className={styles.overviewLabel}>Route</span>
|
|
||||||
<span>{detail.routeId}</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.overviewRow}>
|
|
||||||
<span className={styles.overviewLabel}>Agent</span>
|
|
||||||
<MonoText size="sm">{detail.agentId ?? '—'}</MonoText>
|
|
||||||
</div>
|
|
||||||
<div className={styles.overviewRow}>
|
|
||||||
<span className={styles.overviewLabel}>Correlation</span>
|
|
||||||
<MonoText size="xs">{detail.correlationId ?? '—'}</MonoText>
|
|
||||||
</div>
|
|
||||||
<div className={styles.overviewRow}>
|
|
||||||
<span className={styles.overviewLabel}>Timestamp</span>
|
|
||||||
<MonoText size="xs">{detail.startTime ? new Date(detail.startTime).toISOString().replace('T', ' ').slice(0, 19) : '—'}</MonoText>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{detail.errorMessage && (
|
|
||||||
<div className={styles.panelSection}>
|
|
||||||
<div className={styles.panelSectionTitle}>Errors</div>
|
|
||||||
<Alert variant="error">
|
|
||||||
<strong>{detail.errorMessage.split(':')[0]}</strong>
|
|
||||||
<div>{detail.errorMessage.includes(':') ? detail.errorMessage.substring(detail.errorMessage.indexOf(':') + 1).trim() : ''}</div>
|
|
||||||
</Alert>
|
|
||||||
{detail.errorStackTrace && (
|
|
||||||
<Collapsible title="Stack Trace">
|
|
||||||
<CodeBlock content={detail.errorStackTrace} />
|
|
||||||
</Collapsible>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Processors', value: 'processors',
|
|
||||||
content: (() => {
|
|
||||||
const procList = detail.processors?.length ? detail.processors : (detail.children ?? []);
|
|
||||||
return procList.length ? (
|
|
||||||
<ProcessorTimeline
|
|
||||||
processors={flattenProcessors(procList)}
|
|
||||||
totalMs={detail.durationMs}
|
|
||||||
onProcessorClick={(_p, i) => setProcessorIdx(i)}
|
|
||||||
selectedIndex={processorIdx ?? undefined}
|
|
||||||
/>
|
|
||||||
) : <div style={{ padding: '1rem' }}>No processor data</div>;
|
|
||||||
})(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Route Flow', value: 'flow',
|
|
||||||
content: diagram ? (
|
|
||||||
<RouteFlow
|
|
||||||
nodes={mapDiagramToRouteNodes(
|
|
||||||
diagram.nodes || [],
|
|
||||||
detail.processors?.length ? detail.processors : (detail.children ?? [])
|
|
||||||
)}
|
|
||||||
onNodeClick={(_node, _i) => { /* optionally select processor */ }}
|
|
||||||
/>
|
|
||||||
) : <div style={{ padding: '1rem' }}>No diagram available</div>,
|
|
||||||
},
|
|
||||||
] : [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -255,7 +164,7 @@ export default function Dashboard() {
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={rows}
|
||||||
onRowClick={(row) => { setSelectedId(row.id); setProcessorIdx(null); }}
|
onRowClick={(row) => { setSelectedId(row.id); }}
|
||||||
selectedId={selectedId ?? undefined}
|
selectedId={selectedId ?? undefined}
|
||||||
sortable
|
sortable
|
||||||
pageSize={25}
|
pageSize={25}
|
||||||
@@ -267,10 +176,94 @@ export default function Dashboard() {
|
|||||||
key={selectedId}
|
key={selectedId}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={() => setSelectedId(null)}
|
onClose={() => setSelectedId(null)}
|
||||||
title={`Exchange ${selectedId.slice(0, 12)}...`}
|
title={`${detail.routeId} — ${selectedId.slice(0, 12)}`}
|
||||||
tabs={detailTabs}
|
|
||||||
className={styles.detailPanelOverride}
|
className={styles.detailPanelOverride}
|
||||||
|
>
|
||||||
|
{/* Open full details link */}
|
||||||
|
<div className={styles.panelSection}>
|
||||||
|
<button
|
||||||
|
className={styles.openDetailLink}
|
||||||
|
onClick={() => navigate(`/exchanges/${detail.executionId}`)}
|
||||||
|
>
|
||||||
|
Open full details →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
|
<div className={styles.panelSection}>
|
||||||
|
<div className={styles.panelSectionTitle}>Overview</div>
|
||||||
|
<div className={styles.overviewGrid}>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Status</span>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<StatusDot variant={detail.status === 'COMPLETED' ? 'success' : 'error'} />
|
||||||
|
<span>{detail.status}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Duration</span>
|
||||||
|
<MonoText size="sm">{formatDuration(detail.durationMs)}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Route</span>
|
||||||
|
<span>{detail.routeId}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Agent</span>
|
||||||
|
<MonoText size="sm">{detail.agentId ?? '—'}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Correlation</span>
|
||||||
|
<MonoText size="xs">{detail.correlationId ?? '—'}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Timestamp</span>
|
||||||
|
<MonoText size="xs">{detail.startTime ? new Date(detail.startTime).toISOString().replace('T', ' ').slice(0, 19) : '—'}</MonoText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Errors */}
|
||||||
|
{detail.errorMessage && (
|
||||||
|
<div className={styles.panelSection}>
|
||||||
|
<div className={styles.panelSectionTitle}>Errors</div>
|
||||||
|
<Alert variant="error">
|
||||||
|
<strong>{detail.errorMessage.split(':')[0]}</strong>
|
||||||
|
<div>{detail.errorMessage.includes(':') ? detail.errorMessage.substring(detail.errorMessage.indexOf(':') + 1).trim() : ''}</div>
|
||||||
|
</Alert>
|
||||||
|
{detail.errorStackTrace && (
|
||||||
|
<Collapsible title="Stack Trace">
|
||||||
|
<CodeBlock content={detail.errorStackTrace} />
|
||||||
|
</Collapsible>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Route Flow */}
|
||||||
|
<div className={styles.panelSection}>
|
||||||
|
<div className={styles.panelSectionTitle}>Route Flow</div>
|
||||||
|
{diagram ? (
|
||||||
|
<RouteFlow
|
||||||
|
nodes={mapDiagramToRouteNodes(diagram.nodes || [], procList)}
|
||||||
|
onNodeClick={(_node, _i) => {}}
|
||||||
/>
|
/>
|
||||||
|
) : <div style={{ color: 'var(--text-muted)', fontSize: 12 }}>No diagram available</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Processor Timeline */}
|
||||||
|
<div className={styles.panelSection}>
|
||||||
|
<div className={styles.panelSectionTitle}>
|
||||||
|
Processor Timeline
|
||||||
|
<span className={styles.panelSectionMeta}>{formatDuration(detail.durationMs)}</span>
|
||||||
|
</div>
|
||||||
|
{procList.length ? (
|
||||||
|
<ProcessorTimeline
|
||||||
|
processors={flattenProcessors(procList)}
|
||||||
|
totalMs={detail.durationMs}
|
||||||
|
/>
|
||||||
|
) : <div style={{ color: 'var(--text-muted)', fontSize: 12 }}>No processor data</div>}
|
||||||
|
</div>
|
||||||
|
</DetailPanel>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user