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}`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './client';
|
||||
export * from './types';
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user