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}`);
},
};
+2
View File
@@ -0,0 +1,2 @@
export * from './client';
export * from './types';
+79
View File
@@ -0,0 +1,79 @@
// ── Shared ────────────────────────────────────────────────────────────────────
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
limit: number;
}
// ── Environments ──────────────────────────────────────────────────────────────
export interface EnvironmentUrls {
id_url?: string;
cabinet_url?: string;
admin_url?: string;
[key: string]: string | undefined;
}
export interface Environment {
id: number;
name: string;
urls: EnvironmentUrls;
createdAt: string;
updatedAt: string;
}
// ── Keys ──────────────────────────────────────────────────────────────────────
export interface KeysResponse {
keys: string[];
}
// ── Sessions ──────────────────────────────────────────────────────────────────
export type SessionStatus = 'open' | 'closed';
export interface Session {
id: number;
sessionName: string;
token: string;
status: SessionStatus;
lastUsedAt: string | null;
createdAt: string;
updatedAt: string;
}
// ── Scenarios ─────────────────────────────────────────────────────────────────
export type StepType = 'login' | 'exec' | 'sign';
export interface ScenarioStep {
id: number;
scenarioId: number;
order: number;
type: StepType;
sessionName: string;
execCode: string | null;
validateCode: string | null;
createdAt: string;
updatedAt: string;
}
export interface Scenario {
id: number;
name: string;
steps?: ScenarioStep[];
createdAt: string;
updatedAt: string;
}
export type ScenarioRunStatus = 'pending' | 'running' | 'pass' | 'fail';
export interface ScenarioRun {
id: number;
scenarioId: number;
status: ScenarioRunStatus;
createdAt: string;
updatedAt: string;
}