feat(runs): add runs pages, polling, autorefresh indicator, and scheduler fix

- add global runs page and per-scenario runs/run-detail pages
- run detail page polls every 1s until pass/fail, cleans up on unmount
- runs list pages poll every 10s with AutoRefreshIndicator (pulse on data load)
- fix scheduler: set run to pass after empty step loop to prevent stuck in_progress
- fix table header colors and link cell color for readability
- fix play button to navigate to the new run after creation
- fix package.json import paths in server for Docker build context
This commit is contained in:
2026-04-09 21:58:27 +03:00
parent ebcb8b8ff4
commit 1f3a604940
20 changed files with 745 additions and 22 deletions
+21 -3
View File
@@ -5,6 +5,8 @@ import type {
Session,
Scenario,
ScenarioRun,
ScenarioRunDetail,
ScenarioRunStep,
ScenarioStep,
} from './types';
@@ -103,17 +105,33 @@ export const scenarios = {
run(id: number): Promise<ScenarioRun> {
return request(`/scenarios/${id}/run`, { method: 'POST' });
},
getRun(scenarioId: number, runId: number): Promise<ScenarioRun> {
getRun(scenarioId: number, runId: number): Promise<ScenarioRunDetail> {
return request(`/scenarios/${scenarioId}/run/${runId}`);
},
waitForRun(scenarioId: number, runId: number): Promise<ScenarioRun> {
waitForRun(scenarioId: number, runId: number): Promise<ScenarioRunDetail> {
return request(`/scenarios/${scenarioId}/run/${runId}/wait`, { method: 'POST' });
},
listRuns(scenarioId: number, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun>> {
listRuns(scenarioId: number, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}`);
},
};
// ── Runs ─────────────────────────────────────────────────────────────────────
export const runs = {
listAll(page = 1, limit = 20, status?: string): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[]; scenario: { id: number; name: string } }>> {
const q = new URLSearchParams({ page: String(page), limit: String(limit) });
if (status) q.set('status', status);
return request(`/scenarios/runs?${q}`);
},
list(scenarioId: number, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}`);
},
get(scenarioId: number, runId: number): Promise<ScenarioRunDetail> {
return request(`/scenarios/${scenarioId}/run/${runId}`);
},
};
// ── Scenario Steps ────────────────────────────────────────────────────────────
export interface CreateStepPayload {
+34 -1
View File
@@ -68,12 +68,45 @@ export interface Scenario {
updatedAt: string;
}
export type ScenarioRunStatus = 'pending' | 'running' | 'pass' | 'fail';
export type ScenarioRunStatus = 'pending' | 'in_progress' | 'pass' | 'fail';
export type RunStepStatus = 'waiting' | 'pending' | 'in_progress' | 'pass' | 'fail' | 'cancelled';
export type LogLevel = 'log' | 'warn' | 'error';
export interface ScenarioRun {
id: number;
scenarioId: number;
scenario?: Pick<Scenario, 'id' | 'name'>;
status: ScenarioRunStatus;
stepRuns?: ScenarioRunStep[];
createdAt: string;
updatedAt: string;
}
export interface ScenarioRunStep {
id: number;
runId: number;
scenarioStepId: number;
scenarioStep: ScenarioStep;
status: RunStepStatus;
order: number;
description: string | null;
output: string | null;
createdAt: string;
updatedAt: string;
}
export interface ScenarioRunLog {
id: number;
runId: number;
stepRunId: number | null;
level: LogLevel;
message: string;
createdAt: string;
}
export interface ScenarioRunDetail extends ScenarioRun {
stepRuns: ScenarioRunStep[];
logs: ScenarioRunLog[];
}