feat(export-import): add yaml export/import with id upsert for credentials, snippets, and scenarios

- all entities export a kind field (credential/snippet/scenario) for safe type checking on import
- import upserts by id: overwrites if id exists, creates with explicit id otherwise
- scenario export now includes id and step ids; import deletes old steps before recreating
- add GET /:id/export and POST /import endpoints to credential and snippet controllers
- add UuidBadge ui component: shortens uuid to first+last 4 hex chars, tooltip, clipboard copy with animated ClipboardCheck icon pop
- apply UuidBadge across all entity id display sites (detail pages, card footers, table columns)
- add export/import buttons to CredentialsPage, SnippetsPage, CredentialDetailPage, SnippetDetailPage
This commit is contained in:
2026-04-10 13:09:09 +03:00
parent 1164289173
commit 32be7c0a59
54 changed files with 728 additions and 272 deletions
+55 -28
View File
@@ -39,7 +39,7 @@ export const credentials = {
list(page = 1, limit = 50): Promise<PaginatedResponse<Credential>> {
return request(`/credentials?page=${page}&limit=${limit}`);
},
get(id: number): Promise<Credential> {
get(id: string): Promise<Credential> {
return request(`/credentials/${id}`);
},
create(name: string, data?: string): Promise<Credential> {
@@ -48,15 +48,24 @@ export const credentials = {
body: JSON.stringify({ name, data }),
});
},
update(id: number, patch: Partial<Pick<Credential, 'name' | 'data'>>): Promise<Credential> {
update(id: string, patch: Partial<Pick<Credential, 'name' | 'data'>>): Promise<Credential> {
return request(`/credentials/${id}`, {
method: 'PATCH',
body: JSON.stringify(patch),
});
},
remove(id: number): Promise<void> {
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 ──────────────────────────────────────────────────────────────────
@@ -65,7 +74,7 @@ export const snippets = {
list(page = 1, limit = 50): Promise<PaginatedResponse<Snippet>> {
return request(`/snippets?page=${page}&limit=${limit}`);
},
get(id: number): Promise<Snippet> {
get(id: string): Promise<Snippet> {
return request(`/snippets/${id}`);
},
create(payload: Pick<Snippet, 'name' | 'code'> & { description?: string }): Promise<Snippet> {
@@ -74,15 +83,24 @@ export const snippets = {
body: JSON.stringify(payload),
});
},
update(id: number, patch: Partial<Pick<Snippet, 'name' | 'description' | 'code'>>): Promise<Snippet> {
update(id: string, patch: Partial<Pick<Snippet, 'name' | 'description' | 'code'>>): Promise<Snippet> {
return request(`/snippets/${id}`, {
method: 'PATCH',
body: JSON.stringify(patch),
});
},
remove(id: number): Promise<void> {
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 ──────────────────────────────────────────────────────────────
@@ -91,7 +109,7 @@ export const environments = {
list(page = 1, limit = 50): Promise<PaginatedResponse<Environment>> {
return request(`/environments?page=${page}&limit=${limit}`);
},
get(id: number): Promise<Environment> {
get(id: string): Promise<Environment> {
return request(`/environments/${id}`);
},
create(name: string, urls: Environment['urls']): Promise<Environment> {
@@ -100,13 +118,13 @@ export const environments = {
body: JSON.stringify({ name, urls }),
});
},
update(id: number, patch: Partial<Pick<Environment, 'name' | 'urls'>>): Promise<Environment> {
update(id: string, patch: Partial<Pick<Environment, 'name' | 'urls'>>): Promise<Environment> {
return request(`/environments/${id}`, {
method: 'PATCH',
body: JSON.stringify(patch),
});
},
remove(id: number): Promise<void> {
remove(id: string): Promise<void> {
return request(`/environments/${id}`, { method: 'DELETE' });
},
};
@@ -125,10 +143,10 @@ export const sessions = {
list(page = 1, limit = 50): Promise<PaginatedResponse<Session>> {
return request(`/sessions?page=${page}&limit=${limit}`);
},
get(id: number): Promise<Session> {
get(id: string): Promise<Session> {
return request(`/sessions/${id}`);
},
remove(id: number): Promise<void> {
remove(id: string): Promise<void> {
return request(`/sessions/${id}`, { method: 'DELETE' });
},
};
@@ -139,7 +157,7 @@ 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[] }> {
get(id: string): Promise<Scenario & { steps: ScenarioStep[] }> {
return request(`/scenarios/${id}`);
},
create(name: string): Promise<Scenario> {
@@ -148,41 +166,50 @@ export const scenarios = {
body: JSON.stringify({ name }),
});
},
update(id: number, patch: Partial<Pick<Scenario, 'name'>>): Promise<Scenario> {
update(id: string, patch: Partial<Pick<Scenario, 'name'>>): Promise<Scenario> {
return request(`/scenarios/${id}`, {
method: 'PATCH',
body: JSON.stringify(patch),
});
},
remove(id: number): Promise<void> {
remove(id: string): Promise<void> {
return request(`/scenarios/${id}`, { method: 'DELETE' });
},
run(id: number): Promise<ScenarioRun> {
run(id: string): Promise<ScenarioRun> {
return request(`/scenarios/${id}/run`, { method: 'POST' });
},
getRun(scenarioId: number, runId: number): Promise<ScenarioRunDetail> {
getRun(scenarioId: string, runId: string): Promise<ScenarioRunDetail> {
return request(`/scenarios/${scenarioId}/run/${runId}`);
},
waitForRun(scenarioId: number, runId: number): Promise<ScenarioRunDetail> {
waitForRun(scenarioId: string, runId: string): Promise<ScenarioRunDetail> {
return request(`/scenarios/${scenarioId}/run/${runId}/wait`, { method: 'POST' });
},
listRuns(scenarioId: number, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
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<unknown> {
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: number; name: string } }>> {
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: number, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
list(scenarioId: string, page = 1, limit = 20): Promise<PaginatedResponse<ScenarioRun & { stepRuns: ScenarioRunStep[] }>> {
return request(`/scenarios/${scenarioId}/runs?page=${page}&limit=${limit}`);
},
get(scenarioId: number, runId: number): Promise<ScenarioRunDetail> {
get(scenarioId: string, runId: string): Promise<ScenarioRunDetail> {
return request(`/scenarios/${scenarioId}/run/${runId}`);
},
};
@@ -190,16 +217,16 @@ export const runs = {
// ── Scenario Credentials ──────────────────────────────────────────────────────
export const scenarioCredentials = {
list(scenarioId: number): Promise<ScenarioCredential[]> {
list(scenarioId: string): Promise<ScenarioCredential[]> {
return request(`/scenarios/${scenarioId}/credentials`);
},
add(scenarioId: number, credentialId: number, alias: string): Promise<ScenarioCredential> {
add(scenarioId: string, credentialId: string, alias: string): Promise<ScenarioCredential> {
return request(`/scenarios/${scenarioId}/credentials`, {
method: 'POST',
body: JSON.stringify({ credentialId, alias }),
});
},
remove(scenarioId: number, scCredId: number): Promise<void> {
remove(scenarioId: string, scCredId: string): Promise<void> {
return request(`/scenarios/${scenarioId}/credentials/${scCredId}`, {
method: 'DELETE',
});
@@ -224,22 +251,22 @@ export interface UpdateStepPayload {
}
export const steps = {
get(scenarioId: number, stepId: number): Promise<ScenarioStep> {
get(scenarioId: string, stepId: string): Promise<ScenarioStep> {
return request(`/scenarios/${scenarioId}/steps/${stepId}`);
},
create(scenarioId: number, payload: CreateStepPayload): Promise<ScenarioStep> {
create(scenarioId: string, payload: CreateStepPayload): Promise<ScenarioStep> {
return request(`/scenarios/${scenarioId}/steps`, {
method: 'POST',
body: JSON.stringify(payload),
});
},
update(scenarioId: number, stepId: number, payload: UpdateStepPayload): Promise<ScenarioStep> {
update(scenarioId: string, stepId: string, payload: UpdateStepPayload): Promise<ScenarioStep> {
return request(`/scenarios/${scenarioId}/steps/${stepId}`, {
method: 'PATCH',
body: JSON.stringify(payload),
});
},
remove(scenarioId: number, stepId: number): Promise<void> {
remove(scenarioId: string, stepId: string): Promise<void> {
return request(`/scenarios/${scenarioId}/steps/${stepId}`, { method: 'DELETE' });
},
};