Files
cameleer-server/ui/src/pages/executions/ExecutionExplorer.tsx
hsiegeln 393d19e3f4
Some checks failed
CI / build (push) Successful in 1m11s
CI / deploy (push) Has been cancelled
CI / docker (push) Has been cancelled
Move failed count and avg duration from page-derived to backend stats
All stat card values now come from the /search/stats endpoint which
queries the full time window, not just the current page of results.
Consolidated into a single ClickHouse query for efficiency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 22:51:43 +01:00

72 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
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 ?? [];
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={`from current search`} sparkData={sparkTotal} />
<StatCard label="Avg Duration" value={stats ? `${stats.avgDurationMs.toLocaleString()}ms` : '--'} accent="cyan" sparkData={sparkAvgDuration} />
<StatCard label="Failed" value={stats ? stats.failedCount.toLocaleString() : '--'} accent="rose" sparkData={sparkFailed} />
<StatCard label="P99 Latency" value={stats ? `${stats.p99LatencyMs.toLocaleString()}ms` : '--'} accent="green" 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} />
</>
);
}