fix: Dashboard DetailPanel uses flat scrollable layout matching mock
Some checks failed
CI / build (push) Failing after 41s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

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:
hsiegeln
2026-03-23 20:51:23 +01:00
parent 695969d759
commit a950feaef1
2 changed files with 102 additions and 97 deletions

View File

@@ -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 {

View File

@@ -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 &#x2192;
</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 &#x2192;
</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>
);