Stats endpoint now returns current + previous period (24h shift) values plus today's total count. UI shows: - Total Matches: "of 12.3K today" - Avg Duration: arrow + % vs yesterday - Failure Rate: percentage of errors vs total, arrow + % vs yesterday - P99 Latency: arrow + % vs yesterday - In-Flight: unchanged (running executions) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
97 lines
4.8 KiB
TypeScript
97 lines
4.8 KiB
TypeScript
import { useSearchExecutions, useExecutionStats, useStatsTimeseries } from '../../api/queries/executions';
|
||
import { useExecutionSearch } from './use-execution-search';
|
||
import { StatCard } from '../../components/shared/StatCard';
|
||
import { Pagination } from '../../components/shared/Pagination';
|
||
import { SearchFilters } from './SearchFilters';
|
||
import { ResultsTable } from './ResultsTable';
|
||
import styles from './ExecutionExplorer.module.css';
|
||
|
||
function formatCompact(n: number): string {
|
||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
||
return n.toLocaleString();
|
||
}
|
||
|
||
function pctChange(current: number, previous: number): { text: string; direction: 'up' | 'down' | 'neutral' } {
|
||
if (previous === 0) return { text: 'no prior data', direction: 'neutral' };
|
||
const pct = ((current - previous) / previous) * 100;
|
||
if (Math.abs(pct) < 0.5) return { text: '~0% vs yesterday', direction: 'neutral' };
|
||
const arrow = pct > 0 ? '\u2191' : '\u2193';
|
||
return { text: `${arrow} ${Math.abs(pct).toFixed(1)}% vs yesterday`, direction: pct > 0 ? 'up' : 'down' };
|
||
}
|
||
|
||
export function ExecutionExplorer() {
|
||
const { toSearchRequest, offset, limit, setOffset, live, toggleLive } = useExecutionSearch();
|
||
const searchRequest = toSearchRequest();
|
||
const { data, isLoading, isFetching } = useSearchExecutions(searchRequest, live);
|
||
const timeFrom = searchRequest.timeFrom ?? undefined;
|
||
const timeTo = searchRequest.timeTo ?? undefined;
|
||
const { data: stats } = useExecutionStats(timeFrom, timeTo);
|
||
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo);
|
||
|
||
const sparkTotal = timeseries?.buckets.map((b) => b.totalCount) ?? [];
|
||
const sparkFailed = timeseries?.buckets.map((b) => b.failedCount) ?? [];
|
||
const sparkAvgDuration = timeseries?.buckets.map((b) => b.avgDurationMs) ?? [];
|
||
const sparkP99 = timeseries?.buckets.map((b) => b.p99DurationMs) ?? [];
|
||
const sparkActive = timeseries?.buckets.map((b) => b.activeCount) ?? [];
|
||
|
||
const total = data?.total ?? 0;
|
||
const results = data?.data ?? [];
|
||
|
||
// Failure rate as percentage
|
||
const failureRate = stats && stats.totalCount > 0
|
||
? (stats.failedCount / stats.totalCount) * 100 : 0;
|
||
const prevFailureRate = stats && stats.prevTotalCount > 0
|
||
? (stats.prevFailedCount / stats.prevTotalCount) * 100 : 0;
|
||
|
||
// Comparison vs yesterday
|
||
const avgChange = stats ? pctChange(stats.avgDurationMs, stats.prevAvgDurationMs) : null;
|
||
const failRateChange = stats ? pctChange(failureRate, prevFailureRate) : null;
|
||
const p99Change = stats ? pctChange(stats.p99LatencyMs, stats.prevP99LatencyMs) : null;
|
||
|
||
const showFrom = total > 0 ? offset + 1 : 0;
|
||
const showTo = Math.min(offset + limit, total);
|
||
|
||
return (
|
||
<>
|
||
{/* Page Header */}
|
||
<div className={`${styles.pageHeader} animate-in`}>
|
||
<div>
|
||
<h1>Route Explorer</h1>
|
||
<div className={styles.subtitle}>Search and analyze route executions</div>
|
||
</div>
|
||
<button className={`${styles.liveToggle} ${live ? styles.liveOn : styles.liveOff}`} onClick={toggleLive}>
|
||
<span className={styles.liveDot} />
|
||
{live ? 'LIVE' : 'PAUSED'}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Stats Bar */}
|
||
<div className={styles.statsBar}>
|
||
<StatCard label="Total Matches" value={total.toLocaleString()} accent="amber" change={stats ? `of ${formatCompact(stats.totalToday)} today` : 'from current search'} sparkData={sparkTotal} />
|
||
<StatCard label="Avg Duration" value={stats ? `${stats.avgDurationMs.toLocaleString()}ms` : '--'} accent="cyan" change={avgChange?.text} changeDirection={avgChange?.direction} sparkData={sparkAvgDuration} />
|
||
<StatCard label="Failure Rate" value={stats ? `${failureRate.toFixed(1)}%` : '--'} accent="rose" change={failRateChange?.text} changeDirection={failRateChange?.direction} sparkData={sparkFailed} />
|
||
<StatCard label="P99 Latency" value={stats ? `${stats.p99LatencyMs.toLocaleString()}ms` : '--'} accent="green" change={p99Change?.text} changeDirection={p99Change?.direction} sparkData={sparkP99} />
|
||
<StatCard label="In-Flight" value={stats ? stats.activeCount.toLocaleString() : '--'} accent="blue" change="running executions" sparkData={sparkActive} />
|
||
</div>
|
||
|
||
{/* Filters */}
|
||
<SearchFilters />
|
||
|
||
{/* Results Header */}
|
||
<div className={`${styles.resultsHeader} animate-in delay-4`}>
|
||
<span className={styles.resultsCount}>
|
||
Showing <strong>{showFrom}–{showTo}</strong> of <strong>{total.toLocaleString()}</strong> results
|
||
{isFetching && !isLoading && ' · updating...'}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Results Table */}
|
||
<ResultsTable results={results} loading={isLoading} />
|
||
|
||
{/* Pagination */}
|
||
<Pagination total={total} offset={offset} limit={limit} onChange={setOffset} />
|
||
</>
|
||
);
|
||
}
|