Files
liqa/client/src/api/client.ts
T
ars9 0e4a2e1819 feat(app): add environment import/export and sidebar footer info
- add environment import/export API endpoints and client integration

- add environment export actions and toolbar import/export buttons in UI

- move theme switcher to sidebar bottom and show root package version

- switch snippet card menu icon to cog for consistent options affordance
2026-04-10 20:29:40 +03:00

294 lines
10 KiB
TypeScript

import type {
PaginatedResponse,
Credential,
Environment,
ScenarioCredential,
Session,
Scenario,
ScenarioRun,
ScenarioRunDetail,
ScenarioRunStep,
ScenarioStep,
Snippet,
} from './types';
// In dev, Vite proxies /environments /sessions /scenarios 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 contentType = res.headers.get('content-type')?.toLowerCase() ?? '';
const text = await res.text();
if (!text) return undefined as T;
if (contentType.includes('application/json')) {
return JSON.parse(text) as T;
}
return 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' });
},
exportEnvironment(id: string): Promise<unknown> {
return request(`/environments/${id}/export`);
},
importEnvironment(payload: unknown): Promise<Environment> {
return request('/environments/import', {
method: 'POST',
body: JSON.stringify(payload),
});
},
};
// ── 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<string> {
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;
execCode?: string;
validateCode?: string;
}
export interface UpdateStepPayload {
title?: string;
order?: number;
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' });
},
};