feat: align Dashboard stat cards with mock, add errors section to DetailPanel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { useParams } from 'react-router';
|
||||
import {
|
||||
StatCard, StatusDot, Badge, MonoText, Sparkline,
|
||||
DataTable, DetailPanel, ProcessorTimeline, RouteFlow,
|
||||
Alert, Collapsible, CodeBlock,
|
||||
} from '@cameleer/design-system';
|
||||
import type { Column } from '@cameleer/design-system';
|
||||
import { useSearchExecutions, useExecutionStats, useStatsTimeseries, useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
|
||||
@@ -22,6 +23,8 @@ export default function Dashboard() {
|
||||
const [detailTab, setDetailTab] = useState('overview');
|
||||
const [processorIdx, setProcessorIdx] = useState<number | null>(null);
|
||||
|
||||
const timeWindowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000;
|
||||
|
||||
const { data: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId);
|
||||
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId);
|
||||
const { data: searchResult } = useSearchExecutions({
|
||||
@@ -62,56 +65,88 @@ export default function Dashboard() {
|
||||
{
|
||||
label: 'Overview', value: 'overview',
|
||||
content: (
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>Details</div>
|
||||
<div className={styles.overviewGrid}>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Exchange ID</span>
|
||||
<MonoText size="sm">{detail.executionId}</MonoText>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Status</span>
|
||||
<Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} />
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Route</span>
|
||||
<span>{detail.routeId}</span>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Duration</span>
|
||||
<span>{detail.durationMs}ms</span>
|
||||
</div>
|
||||
{detail.errorMessage && (
|
||||
<>
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>Details</div>
|
||||
<div className={styles.overviewGrid}>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Error</span>
|
||||
<span>{detail.errorMessage}</span>
|
||||
<span className={styles.overviewLabel}>Exchange ID</span>
|
||||
<MonoText size="sm">{detail.executionId}</MonoText>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Status</span>
|
||||
<Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} />
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Route</span>
|
||||
<span>{detail.routeId}</span>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Duration</span>
|
||||
<span>{detail.durationMs}ms</span>
|
||||
</div>
|
||||
</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: detail.children ? (
|
||||
<ProcessorTimeline
|
||||
processors={flattenProcessors(detail.children)}
|
||||
totalMs={detail.durationMs}
|
||||
onProcessorClick={(_p, i) => setProcessorIdx(i)}
|
||||
selectedIndex={processorIdx ?? undefined}
|
||||
/>
|
||||
) : <div style={{ padding: '1rem' }}>No processor data</div>,
|
||||
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>;
|
||||
})(),
|
||||
},
|
||||
] : [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.healthStrip}>
|
||||
<StatCard label="Total Exchanges" value={stats?.totalCount ?? 0} sparkline={sparklineData} />
|
||||
<StatCard label="Failed" value={stats?.failedCount ?? 0} accent="error" />
|
||||
<StatCard label="Avg Duration" value={`${stats?.avgDurationMs ?? 0}ms`} />
|
||||
<StatCard label="P99 Duration" value={`${stats?.p99LatencyMs ?? 0}ms`} accent="warning" />
|
||||
<StatCard label="Active" value={stats?.activeCount ?? 0} accent="running" />
|
||||
<StatCard
|
||||
label="Throughput"
|
||||
value={timeWindowSeconds > 0 ? `${((stats?.totalCount ?? 0) / timeWindowSeconds).toFixed(2)} ex/s` : '0.00 ex/s'}
|
||||
sparkline={sparklineData}
|
||||
/>
|
||||
<StatCard
|
||||
label="Error Rate"
|
||||
value={(stats?.totalCount ?? 0) > 0 ? `${((stats?.failedCount ?? 0) / stats!.totalCount * 100).toFixed(1)}%` : '0.0%'}
|
||||
accent="error"
|
||||
/>
|
||||
<StatCard
|
||||
label="Avg Latency"
|
||||
value={`${stats?.avgDurationMs ?? 0}ms`}
|
||||
/>
|
||||
<StatCard
|
||||
label="P99 Latency"
|
||||
value={`${stats?.p99LatencyMs ?? 0}ms`}
|
||||
accent="warning"
|
||||
/>
|
||||
<StatCard
|
||||
label="In-Flight"
|
||||
value={stats?.activeCount ?? 0}
|
||||
accent="running"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.tableSection}>
|
||||
|
||||
Reference in New Issue
Block a user