feat(table,runs,sessions): add server-side sorting and pagination

- add sortable column support to Table with asc/desc/unsorted icons
- active sort icon uses --color-primary; no header background change
- Table supports server-side mode via total/page/onPageChange props
- wire server-side sort+pagination in ScenariosPage, AllRunsPage, RunsPage, SessionsPage
- remove steps count column from run tables
- fix server: RunsQueryDto now extends PaginationQueryDto with orderBy/orderDir
- fix findAllRuns to use QueryBuilder; sort by scenario.name via JOIN
- add per-resource typed query DTOs with @IsIn allowlist on orderBy
- prevents SQL injection and returns 400 for unknown orderBy values
This commit is contained in:
2026-04-15 12:18:47 +03:00
parent f268d0e3eb
commit ad155e269c
20 changed files with 456 additions and 125 deletions
+15 -6
View File
@@ -162,8 +162,8 @@ export const environments = {
// ── Sessions ──────────────────────────────────────────────────────────────────
export const sessions = {
list(page = 1, limit = 50): Promise<PaginatedResponse<Session>> {
return request(`/sessions?page=${page}&limit=${limit}`);
list(page = 1, limit = 50, orderBy = 'id', orderDir: 'ASC' | 'DESC' = 'DESC'): Promise<PaginatedResponse<Session>> {
return request(`/sessions?page=${page}&limit=${limit}&orderBy=${orderBy}&orderDir=${orderDir}`);
},
get(id: string): Promise<Session> {
return request(`/sessions/${id}`);
@@ -176,8 +176,13 @@ export const sessions = {
// ── Scenarios ─────────────────────────────────────────────────────────────────
export const scenarios = {
list(page = 1, limit = 50): Promise<PaginatedResponse<Scenario>> {
return request(`/scenarios?page=${page}&limit=${limit}&orderBy=updatedAt&orderDir=DESC`);
list(
page = 1,
limit = 50,
orderBy = 'updatedAt',
orderDir: 'ASC' | 'DESC' = 'DESC',
): Promise<PaginatedResponse<Scenario>> {
return request(`/scenarios?page=${page}&limit=${limit}&orderBy=${orderBy}&orderDir=${orderDir}`);
},
get(id: string): Promise<Scenario & { steps: ScenarioStep[] }> {
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<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
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<ScenarioRunDetail> {
const qs = q?.trim() ? `?q=${encodeURIComponent(q.trim())}` : '';