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 {
|
import {
|
||||||
StatCard, StatusDot, Badge, MonoText, Sparkline,
|
StatCard, StatusDot, Badge, MonoText, Sparkline,
|
||||||
DataTable, DetailPanel, ProcessorTimeline, RouteFlow,
|
DataTable, DetailPanel, ProcessorTimeline, RouteFlow,
|
||||||
|
Alert, Collapsible, CodeBlock,
|
||||||
} 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, useProcessorSnapshot } from '../../api/queries/executions';
|
||||||
@@ -22,6 +23,8 @@ export default function Dashboard() {
|
|||||||
const [detailTab, setDetailTab] = useState('overview');
|
const [detailTab, setDetailTab] = useState('overview');
|
||||||
const [processorIdx, setProcessorIdx] = useState<number | null>(null);
|
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: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId);
|
||||||
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId);
|
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId);
|
||||||
const { data: searchResult } = useSearchExecutions({
|
const { data: searchResult } = useSearchExecutions({
|
||||||
@@ -62,56 +65,88 @@ export default function Dashboard() {
|
|||||||
{
|
{
|
||||||
label: 'Overview', value: 'overview',
|
label: 'Overview', value: 'overview',
|
||||||
content: (
|
content: (
|
||||||
<div className={styles.panelSection}>
|
<>
|
||||||
<div className={styles.panelSectionTitle}>Details</div>
|
<div className={styles.panelSection}>
|
||||||
<div className={styles.overviewGrid}>
|
<div className={styles.panelSectionTitle}>Details</div>
|
||||||
<div className={styles.overviewRow}>
|
<div className={styles.overviewGrid}>
|
||||||
<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.overviewRow}>
|
<div className={styles.overviewRow}>
|
||||||
<span className={styles.overviewLabel}>Error</span>
|
<span className={styles.overviewLabel}>Exchange ID</span>
|
||||||
<span>{detail.errorMessage}</span>
|
<MonoText size="sm">{detail.executionId}</MonoText>
|
||||||
</div>
|
</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>
|
||||||
</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',
|
label: 'Processors', value: 'processors',
|
||||||
content: detail.children ? (
|
content: (() => {
|
||||||
<ProcessorTimeline
|
const procList = detail.processors?.length ? detail.processors : (detail.children ?? []);
|
||||||
processors={flattenProcessors(detail.children)}
|
return procList.length ? (
|
||||||
totalMs={detail.durationMs}
|
<ProcessorTimeline
|
||||||
onProcessorClick={(_p, i) => setProcessorIdx(i)}
|
processors={flattenProcessors(procList)}
|
||||||
selectedIndex={processorIdx ?? undefined}
|
totalMs={detail.durationMs}
|
||||||
/>
|
onProcessorClick={(_p, i) => setProcessorIdx(i)}
|
||||||
) : <div style={{ padding: '1rem' }}>No processor data</div>,
|
selectedIndex={processorIdx ?? undefined}
|
||||||
|
/>
|
||||||
|
) : <div style={{ padding: '1rem' }}>No processor data</div>;
|
||||||
|
})(),
|
||||||
},
|
},
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.healthStrip}>
|
<div className={styles.healthStrip}>
|
||||||
<StatCard label="Total Exchanges" value={stats?.totalCount ?? 0} sparkline={sparklineData} />
|
<StatCard
|
||||||
<StatCard label="Failed" value={stats?.failedCount ?? 0} accent="error" />
|
label="Throughput"
|
||||||
<StatCard label="Avg Duration" value={`${stats?.avgDurationMs ?? 0}ms`} />
|
value={timeWindowSeconds > 0 ? `${((stats?.totalCount ?? 0) / timeWindowSeconds).toFixed(2)} ex/s` : '0.00 ex/s'}
|
||||||
<StatCard label="P99 Duration" value={`${stats?.p99LatencyMs ?? 0}ms`} accent="warning" />
|
sparkline={sparklineData}
|
||||||
<StatCard label="Active" value={stats?.activeCount ?? 0} accent="running" />
|
/>
|
||||||
|
<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>
|
||||||
|
|
||||||
<div className={styles.tableSection}>
|
<div className={styles.tableSection}>
|
||||||
|
|||||||
Reference in New Issue
Block a user