- add Search pill component with lucide icon and focus highlight - wire debounced search input to GET /run/:id?q= backend filter - backend filters run logs with LIKE %q% via TypeORM - add sectionHeadingRow layout for heading + search alignment - add i18n keys: runs.step_title, logs_search_placeholder
274 lines
10 KiB
TypeScript
274 lines
10 KiB
TypeScript
import type {
|
|
PaginatedResponse,
|
|
Credential,
|
|
Environment,
|
|
KeysResponse,
|
|
ScenarioCredential,
|
|
Session,
|
|
Scenario,
|
|
ScenarioRun,
|
|
ScenarioRunDetail,
|
|
ScenarioRunStep,
|
|
ScenarioStep,
|
|
Snippet,
|
|
} from './types';
|
|
|
|
// In dev, Vite proxies /environments /sessions /scenarios /keys to localhost:3000.
|
|
// In production (or when VITE_API_URL is set) we hit the configured origin directly.
|
|
const BASE_URL: string = (import.meta.env.VITE_API_URL as string | undefined) ?? '';
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(`${BASE_URL}${path}`, {
|
|
headers: { 'Content-Type': 'application/json', ...init?.headers },
|
|
...init,
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => res.statusText);
|
|
throw new Error(`${res.status} ${text}`);
|
|
}
|
|
const contentLength = res.headers.get('content-length');
|
|
if (res.status === 204 || contentLength === '0') return undefined as T;
|
|
const text = await res.text();
|
|
if (!text) return undefined as T;
|
|
return JSON.parse(text) as T;
|
|
}
|
|
|
|
// ── Credentials ───────────────────────────────────────────────────────────────
|
|
|
|
export const credentials = {
|
|
list(page = 1, limit = 50): Promise<PaginatedResponse<Credential>> {
|
|
return request(`/credentials?page=${page}&limit=${limit}`);
|
|
},
|
|
get(id: string): Promise<Credential> {
|
|
return request(`/credentials/${id}`);
|
|
},
|
|
create(name: string, data?: string): Promise<Credential> {
|
|
return request('/credentials', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name, data }),
|
|
});
|
|
},
|
|
update(id: string, patch: Partial<Pick<Credential, 'name' | 'data'>>): Promise<Credential> {
|
|
return request(`/credentials/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(patch),
|
|
});
|
|
},
|
|
remove(id: string): Promise<void> {
|
|
return request(`/credentials/${id}`, { method: 'DELETE' });
|
|
},
|
|
exportCredential(id: string): Promise<unknown> {
|
|
return request(`/credentials/${id}/export`);
|
|
},
|
|
importCredential(payload: unknown): Promise<Credential> {
|
|
return request('/credentials/import', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
},
|
|
};
|
|
|
|
// ── Snippets ──────────────────────────────────────────────────────────────────
|
|
|
|
export const snippets = {
|
|
list(page = 1, limit = 50): Promise<PaginatedResponse<Snippet>> {
|
|
return request(`/snippets?page=${page}&limit=${limit}`);
|
|
},
|
|
get(id: string): Promise<Snippet> {
|
|
return request(`/snippets/${id}`);
|
|
},
|
|
create(payload: Pick<Snippet, 'name' | 'code'> & { description?: string }): Promise<Snippet> {
|
|
return request('/snippets', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
},
|
|
update(id: string, patch: Partial<Pick<Snippet, 'name' | 'description' | 'code'>>): Promise<Snippet> {
|
|
return request(`/snippets/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(patch),
|
|
});
|
|
},
|
|
remove(id: string): Promise<void> {
|
|
return request(`/snippets/${id}`, { method: 'DELETE' });
|
|
},
|
|
exportSnippet(id: string): Promise<unknown> {
|
|
return request(`/snippets/${id}/export`);
|
|
},
|
|
importSnippet(payload: unknown): Promise<Snippet> {
|
|
return request('/snippets/import', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
},
|
|
};
|
|
|
|
// ── Environments ──────────────────────────────────────────────────────────────
|
|
|
|
export const environments = {
|
|
list(page = 1, limit = 50): Promise<PaginatedResponse<Environment>> {
|
|
return request(`/environments?page=${page}&limit=${limit}`);
|
|
},
|
|
get(id: string): Promise<Environment> {
|
|
return request(`/environments/${id}`);
|
|
},
|
|
create(name: string, urls: Environment['urls']): Promise<Environment> {
|
|
return request('/environments', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name, urls }),
|
|
});
|
|
},
|
|
update(id: string, patch: Partial<Pick<Environment, 'name' | 'urls'>>): Promise<Environment> {
|
|
return request(`/environments/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(patch),
|
|
});
|
|
},
|
|
remove(id: string): Promise<void> {
|
|
return request(`/environments/${id}`, { method: 'DELETE' });
|
|
},
|
|
};
|
|
|
|
// ── Keys ──────────────────────────────────────────────────────────────────────
|
|
|
|
export const keys = {
|
|
list(): Promise<KeysResponse> {
|
|
return request('/keys');
|
|
},
|
|
};
|
|
|
|
// ── Sessions ──────────────────────────────────────────────────────────────────
|
|
|
|
export const sessions = {
|
|
list(page = 1, limit = 50): Promise<PaginatedResponse<Session>> {
|
|
return request(`/sessions?page=${page}&limit=${limit}`);
|
|
},
|
|
get(id: string): Promise<Session> {
|
|
return request(`/sessions/${id}`);
|
|
},
|
|
remove(id: string): Promise<void> {
|
|
return request(`/sessions/${id}`, { method: 'DELETE' });
|
|
},
|
|
};
|
|
|
|
// ── Scenarios ─────────────────────────────────────────────────────────────────
|
|
|
|
export const scenarios = {
|
|
list(page = 1, limit = 50): Promise<PaginatedResponse<Scenario>> {
|
|
return request(`/scenarios?page=${page}&limit=${limit}&orderBy=updatedAt&orderDir=DESC`);
|
|
},
|
|
get(id: string): Promise<Scenario & { steps: ScenarioStep[] }> {
|
|
return request(`/scenarios/${id}`);
|
|
},
|
|
create(name: string): Promise<Scenario> {
|
|
return request('/scenarios', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
},
|
|
update(id: string, patch: Partial<Pick<Scenario, 'name'>>): Promise<Scenario> {
|
|
return request(`/scenarios/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(patch),
|
|
});
|
|
},
|
|
remove(id: string): Promise<void> {
|
|
return request(`/scenarios/${id}`, { method: 'DELETE' });
|
|
},
|
|
run(id: string): Promise<ScenarioRun> {
|
|
return request(`/scenarios/${id}/run`, { method: 'POST' });
|
|
},
|
|
getRun(scenarioId: string, runId: string): Promise<ScenarioRunDetail> {
|
|
return request(`/scenarios/${scenarioId}/run/${runId}`);
|
|
},
|
|
waitForRun(scenarioId: string, runId: string): Promise<ScenarioRunDetail> {
|
|
return request(`/scenarios/${scenarioId}/run/${runId}/wait`, { method: 'POST' });
|
|
},
|
|
listRuns(scenarioId: string, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
|
|
return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}`);
|
|
},
|
|
exportScenario(id: string): Promise<unknown> {
|
|
return request(`/scenarios/${id}/export`);
|
|
},
|
|
importScenario(payload: unknown): Promise<Scenario> {
|
|
return request('/scenarios/import', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
},
|
|
};
|
|
|
|
// ── Runs ─────────────────────────────────────────────────────────────────────
|
|
|
|
export const runs = {
|
|
listAll(page = 1, limit = 20, status?: string): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[]; scenario: { id: string; 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: string, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
|
|
return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}`);
|
|
},
|
|
get(scenarioId: string, runId: string, q?: string): Promise<ScenarioRunDetail> {
|
|
const qs = q?.trim() ? `?q=${encodeURIComponent(q.trim())}` : '';
|
|
return request(`/scenarios/${scenarioId}/run/${runId}${qs}`);
|
|
},
|
|
};
|
|
|
|
// ── Scenario Credentials ──────────────────────────────────────────────────────
|
|
|
|
export const scenarioCredentials = {
|
|
list(scenarioId: string): Promise<ScenarioCredential[]> {
|
|
return request(`/scenarios/${scenarioId}/credentials`);
|
|
},
|
|
add(scenarioId: string, credentialId: string, alias: string): Promise<ScenarioCredential> {
|
|
return request(`/scenarios/${scenarioId}/credentials`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ credentialId, alias }),
|
|
});
|
|
},
|
|
remove(scenarioId: string, scCredId: string): Promise<void> {
|
|
return request(`/scenarios/${scenarioId}/credentials/${scCredId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
};
|
|
|
|
// ── Scenario Steps ────────────────────────────────────────────────────────────
|
|
export interface CreateStepPayload {
|
|
title?: string;
|
|
order: number;
|
|
type: 'login' | 'exec' | 'sign';
|
|
execCode?: string;
|
|
validateCode?: string;
|
|
}
|
|
|
|
export interface UpdateStepPayload {
|
|
title?: string;
|
|
order?: number;
|
|
type?: 'login' | 'exec' | 'sign';
|
|
execCode?: string;
|
|
validateCode?: string;
|
|
}
|
|
|
|
export const steps = {
|
|
get(scenarioId: string, stepId: string): Promise<ScenarioStep> {
|
|
return request(`/scenarios/${scenarioId}/steps/${stepId}`);
|
|
},
|
|
create(scenarioId: string, payload: CreateStepPayload): Promise<ScenarioStep> {
|
|
return request(`/scenarios/${scenarioId}/steps`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
},
|
|
update(scenarioId: string, stepId: string, payload: UpdateStepPayload): Promise<ScenarioStep> {
|
|
return request(`/scenarios/${scenarioId}/steps/${stepId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
},
|
|
remove(scenarioId: string, stepId: string): Promise<void> {
|
|
return request(`/scenarios/${scenarioId}/steps/${stepId}`, { method: 'DELETE' });
|
|
},
|
|
};
|