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:
2026-04-09 00:11:55 +03:00
parent 5cc16725fb
commit 9916ef5aaf
47 changed files with 3910 additions and 10 deletions
+109
View File
@@ -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}`);
},
};