diff --git a/client/src/api/client.ts b/client/src/api/client.ts index c9f5697..2188fdd 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -162,8 +162,8 @@ export const environments = { // ── Sessions ────────────────────────────────────────────────────────────────── export const sessions = { - list(page = 1, limit = 50): Promise> { - return request(`/sessions?page=${page}&limit=${limit}`); + list(page = 1, limit = 50, orderBy = 'id', orderDir: 'ASC' | 'DESC' = 'DESC'): Promise> { + return request(`/sessions?page=${page}&limit=${limit}&orderBy=${orderBy}&orderDir=${orderDir}`); }, get(id: string): Promise { return request(`/sessions/${id}`); @@ -176,8 +176,13 @@ export const sessions = { // ── Scenarios ───────────────────────────────────────────────────────────────── export const scenarios = { - list(page = 1, limit = 50): Promise> { - return request(`/scenarios?page=${page}&limit=${limit}&orderBy=updatedAt&orderDir=DESC`); + list( + page = 1, + limit = 50, + orderBy = 'updatedAt', + orderDir: 'ASC' | 'DESC' = 'DESC', + ): Promise> { + return request(`/scenarios?page=${page}&limit=${limit}&orderBy=${orderBy}&orderDir=${orderDir}`); }, get(id: string): Promise { return request(`/scenarios/${id}`); @@ -245,12 +250,14 @@ export const runs = { page = 1, limit = 20, status?: string, + orderBy = 'createdAt', + orderDir: 'ASC' | 'DESC' = 'DESC', ): Promise< PaginatedResponse< ScenarioRun & { stepRuns: ScenarioRunStep[]; scenario: { id: string; name: string } } > > { - const q = new URLSearchParams({ page: String(page), limit: String(limit) }); + const q = new URLSearchParams({ page: String(page), limit: String(limit), orderBy, orderDir }); if (status) q.set('status', status); return request(`/scenarios/runs?${q}`); }, @@ -258,8 +265,10 @@ export const runs = { scenarioId: string, page = 1, limit = 20, + orderBy = 'createdAt', + orderDir: 'ASC' | 'DESC' = 'DESC', ): Promise> { - return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}`); + return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}&orderBy=${orderBy}&orderDir=${orderDir}`); }, get(scenarioId: string, runId: string, q?: string): Promise { const qs = q?.trim() ? `?q=${encodeURIComponent(q.trim())}` : ''; diff --git a/client/src/pages/run/AllRunsPage.tsx b/client/src/pages/run/AllRunsPage.tsx index f9732e7..f153cd5 100644 --- a/client/src/pages/run/AllRunsPage.tsx +++ b/client/src/pages/run/AllRunsPage.tsx @@ -1,9 +1,10 @@ +import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Activity } from 'lucide-react'; import { runs } from '../../api'; -import type { ScenarioRun, ScenarioRunStep, ScenarioRunStatus } from '../../api'; +import type { ScenarioRun, ScenarioRunStatus } from '../../api'; import { AutoRefreshIndicator, Badge, @@ -12,6 +13,7 @@ import { Timestamp, UuidBadge, type TableColumn, + type SortDirection, } from '../../ui'; import type { BadgeVariant } from '../../ui'; import styles from '../Page.module.css'; @@ -31,7 +33,6 @@ const STATUS_LABEL: Record = { }; type AllRunRow = ScenarioRun & { - stepRuns: ScenarioRunStep[]; scenario: { id: number; name: string }; }; @@ -39,26 +40,46 @@ export function AllRunsPage() { const { t } = useTranslation(); const navigate = useNavigate(); + const [sortBy, setSortBy] = useState('createdAt'); + const [sortDir, setSortDir] = useState('desc'); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(20); + const { - data: items = [], + data, isLoading, error, refetch, - } = useQuery({ - queryKey: ['runs'], - queryFn: async () => { - const res = await runs.listAll(); - return res.data as AllRunRow[]; - }, + } = useQuery({ + queryKey: ['runs', sortBy, sortDir, page, pageSize], + queryFn: () => + runs.listAll(page, pageSize, undefined, sortBy, sortDir === 'asc' ? 'ASC' : 'DESC'), refetchInterval: 10_000, staleTime: 0, }); + const items = (data?.data ?? []) as AllRunRow[]; + const total = data?.total ?? 0; + + const handleSort = (key: string, dir: SortDirection) => { + setSortBy(key); + setSortDir(dir); + setPage(1); + }; + + const handlePageChange = (p: number) => setPage(p); + + const handlePageSizeChange = (size: number) => { + setPageSize(size); + setPage(1); + }; + const columns: TableColumn[] = [ { key: 'id', header: t('runs.col_id'), render: (r) => , width: 60 }, { - key: 'scenario', + key: 'scenario.name', header: t('runs.col_scenario'), + sortable: true, render: (r) => ( {STATUS_LABEL[r.status]}, }, { - key: 'steps', - header: t('runs.col_steps'), - width: 80, - render: (r) => r.stepRuns.length, - }, - { - key: 'created', + key: 'createdAt', header: t('runs.col_created'), width: 160, + sortable: true, render: (r) => , }, ]; @@ -109,9 +126,16 @@ export function AllRunsPage() { rowKey={(r) => r.id} loading={isLoading} emptyMessage={t('runs.empty')} - pageSize={20} + pageSize={pageSize} pageSizeOptions={[10, 20, 50]} onRowClick={(r) => navigate(`/scenarios/${r.scenarioId}/runs/${r.id}`)} + sortBy={sortBy} + sortDir={sortDir} + onSort={handleSort} + total={total} + page={page} + onPageChange={handlePageChange} + onPageSizeChange={handlePageSizeChange} /> ); diff --git a/client/src/pages/run/RunsPage.tsx b/client/src/pages/run/RunsPage.tsx index 6f7759b..f765838 100644 --- a/client/src/pages/run/RunsPage.tsx +++ b/client/src/pages/run/RunsPage.tsx @@ -7,7 +7,6 @@ import type { Environment, Scenario, ScenarioRun, - ScenarioRunStep, ScenarioRunStatus, } from '../../api'; import { @@ -22,6 +21,7 @@ import { UuidBadge, useToast, type TableColumn, + type SortDirection, } from '../../ui'; import type { BadgeVariant } from '../../ui'; import styles from '../Page.module.css'; @@ -40,7 +40,7 @@ const STATUS_LABEL: Record = { fail: 'Fail', }; -type RunRow = ScenarioRun & { stepRuns: ScenarioRunStep[] }; +type RunRow = ScenarioRun; export function RunsPage() { const { t } = useTranslation(); @@ -60,29 +60,74 @@ export function RunsPage() { const pollRef = useRef | null>(null); const hasDataRef = useRef(false); - const load = useCallback(() => { - if (!id) return; - if (!hasDataRef.current) setLoading(true); - Promise.all([scenarios.get(id!), runs.list(id!)]) + const [sortBy, setSortBy] = useState('createdAt'); + const [sortDir, setSortDir] = useState('desc'); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(20); + const [total, setTotal] = useState(0); + + // Keep refs so the poll interval always reads current values + const sortByRef = useRef(sortBy); + const sortDirRef = useRef(sortDir); + const pageRef = useRef(page); + const pageSizeRef = useRef(pageSize); + sortByRef.current = sortBy; + sortDirRef.current = sortDir; + pageRef.current = page; + pageSizeRef.current = pageSize; + + const load = useCallback( + (orderBy: string, orderDir: SortDirection, p: number, limit: number) => { + if (!id) return; + if (!hasDataRef.current) setLoading(true); + Promise.all([ + scenarios.get(id!), + runs.list(id!, p, limit, orderBy, orderDir === 'asc' ? 'ASC' : 'DESC'), + ]) .then(([sc, res]) => { setScenario(sc); setItems(res.data); + setTotal(res.total); setError(null); setPulseKey((k) => k + 1); hasDataRef.current = true; }) .catch((err: Error) => setError(err.message)) .finally(() => setLoading(false)); - }, [id]); + }, + [id], + ); useEffect(() => { - load(); - pollRef.current = setInterval(load, 10_000); + load('createdAt', 'desc', 1, 20); + pollRef.current = setInterval( + () => load(sortByRef.current, sortDirRef.current, pageRef.current, pageSizeRef.current), + 10_000, + ); return () => { if (pollRef.current) clearInterval(pollRef.current); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [load]); + const handleSort = (key: string, dir: SortDirection) => { + setSortBy(key); + setSortDir(dir); + setPage(1); + load(key, dir, 1, pageSize); + }; + + const handlePageChange = (p: number) => { + setPage(p); + load(sortBy, sortDir, p, pageSize); + }; + + const handlePageSizeChange = (size: number) => { + setPageSize(size); + setPage(1); + load(sortBy, sortDir, 1, size); + }; + useEffect(() => { environments .list(1, 200) @@ -117,18 +162,14 @@ export function RunsPage() { key: 'status', header: t('runs.col_status'), width: 100, + sortable: true, render: (r) => {STATUS_LABEL[r.status]}, }, { - key: 'steps', - header: t('runs.col_steps'), - width: 80, - render: (r) => r.stepRuns.length, - }, - { - key: 'created', + key: 'createdAt', header: t('runs.col_created'), width: 160, + sortable: true, render: (r) => , }, ]; @@ -151,7 +192,7 @@ export function RunsPage() { ]} />
- + load(sortBy, sortDir, page, pageSize)} />