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;
|
||||
color: var(--text-muted);
|
||||
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 {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Alert, Collapsible, CodeBlock, ShortcutsBar,
|
||||
} 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 { useGlobalFilters } from '@cameleer/design-system';
|
||||
import type { ExecutionSummary } from '../../api/types';
|
||||
@@ -28,8 +28,6 @@ export default function Dashboard() {
|
||||
const timeTo = timeRange.end.toISOString();
|
||||
|
||||
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;
|
||||
|
||||
@@ -42,7 +40,6 @@ export default function Dashboard() {
|
||||
offset: 0, limit: 50,
|
||||
}, true);
|
||||
const { data: detail } = useExecutionDetail(selectedId);
|
||||
const { data: snapshot } = useProcessorSnapshot(selectedId, processorIdx);
|
||||
const { data: diagram } = useDiagramByRoute(detail?.groupName, detail?.routeId);
|
||||
|
||||
const rows: Row[] = useMemo(() =>
|
||||
@@ -109,95 +106,7 @@ export default function Dashboard() {
|
||||
},
|
||||
];
|
||||
|
||||
const detailTabs = detail ? [
|
||||
{
|
||||
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>,
|
||||
},
|
||||
] : [];
|
||||
const procList = detail ? (detail.processors?.length ? detail.processors : (detail.children ?? [])) : [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -255,7 +164,7 @@ export default function Dashboard() {
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
onRowClick={(row) => { setSelectedId(row.id); setProcessorIdx(null); }}
|
||||
onRowClick={(row) => { setSelectedId(row.id); }}
|
||||
selectedId={selectedId ?? undefined}
|
||||
sortable
|
||||
pageSize={25}
|
||||
@@ -267,10 +176,94 @@ export default function Dashboard() {
|
||||
key={selectedId}
|
||||
open={true}
|
||||
onClose={() => setSelectedId(null)}
|
||||
title={`Exchange ${selectedId.slice(0, 12)}...`}
|
||||
tabs={detailTabs}
|
||||
title={`${detail.routeId} — ${selectedId.slice(0, 12)}`}
|
||||
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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user