feat(client): add UI kit, data layer, app shell, and Table component
- design token system (Atlantis, Kimberly, Powder Ash palette) - Button, Badge, Input, Select, Card, SidePanel, Breadcrumbs components - generic typed Table<T> component with loading/empty states - API data layer: typed fetch client for environments, keys, sessions, scenarios - Vite dev proxy targeting server on port 13000 - App shell with SidePanel nav and four entity pages (Environments, Keys, Sessions, Scenarios) - Storybook config with dark/light theme toggle and a11y addon
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import type {
|
||||
PaginatedResponse,
|
||||
Environment,
|
||||
KeysResponse,
|
||||
Session,
|
||||
Scenario,
|
||||
ScenarioRun,
|
||||
ScenarioStep,
|
||||
} 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}`);
|
||||
}
|
||||
if (res.status === 204) return undefined as T;
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
// ── Environments ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const environments = {
|
||||
list(page = 1, limit = 50): Promise<PaginatedResponse<Environment>> {
|
||||
return request(`/environments?page=${page}&limit=${limit}`);
|
||||
},
|
||||
get(id: number): 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: number, patch: Partial<Pick<Environment, 'name' | 'urls'>>): Promise<Environment> {
|
||||
return request(`/environments/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
},
|
||||
remove(id: number): 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}`);
|
||||
},
|
||||
remove(id: number): 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}`);
|
||||
},
|
||||
get(id: number): Promise<Scenario & { steps: ScenarioStep[] }> {
|
||||
return request(`/scenarios/${id}`);
|
||||
},
|
||||
create(name: string): Promise<Scenario> {
|
||||
return request('/scenarios', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
},
|
||||
update(id: number, patch: Partial<Pick<Scenario, 'name'>>): Promise<Scenario> {
|
||||
return request(`/scenarios/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
},
|
||||
remove(id: number): Promise<void> {
|
||||
return request(`/scenarios/${id}`, { method: 'DELETE' });
|
||||
},
|
||||
run(id: number): Promise<ScenarioRun> {
|
||||
return request(`/scenarios/${id}/run`, { method: 'POST' });
|
||||
},
|
||||
getRun(scenarioId: number, runId: number): Promise<ScenarioRun> {
|
||||
return request(`/scenarios/${scenarioId}/run/${runId}`);
|
||||
},
|
||||
waitForRun(scenarioId: number, runId: number): Promise<ScenarioRun> {
|
||||
return request(`/scenarios/${scenarioId}/run/${runId}/wait`, { method: 'POST' });
|
||||
},
|
||||
listRuns(scenarioId: number, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun>> {
|
||||
return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}`);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user