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
@@ -33,14 +33,14 @@ interface BrowserHandle {
@Injectable()
export class ScenarioSchedulerService {
private readonly logger = new TraceLogger(ScenarioSchedulerService.name);
private readonly activeRuns = new Set<number>();
private readonly runBrowsers = new Map<number, BrowserHandle>();
private readonly activeRuns = new Set<string>();
private readonly runBrowsers = new Map<string, BrowserHandle>();
// Cache credential maps per run (built once when a run starts)
private readonly runCredentials = new Map<number, Record<string, unknown>>();
private readonly runCredentials = new Map<string, Record<string, unknown>>();
// Cache environment URLs per run (resolved from the first login step)
private readonly runEnvironments = new Map<number, EnvironmentUrls>();
private readonly runEnvironments = new Map<string, EnvironmentUrls>();
// Cache snippet code map per run (built once when a run starts)
private readonly runSnippets = new Map<number, Record<string, string>>();
private readonly runSnippets = new Map<string, Record<string, string>>();
constructor(
@InjectRepository(ScenarioRunEntity)
@@ -57,8 +57,8 @@ export class ScenarioSchedulerService {
) {}
private persistLog(
runId: number,
stepRunId: number | null,
runId: string,
stepRunId: string | null,
level: "log" | "warn" | "error",
message: string,
): void {
@@ -67,7 +67,7 @@ export class ScenarioSchedulerService {
);
}
private stepLogger(stepRunId: number, runId: number): ScriptLogger {
private stepLogger(stepRunId: string, runId: string): ScriptLogger {
return (level, msg) => {
this.logger[level](`StepRun #${stepRunId} script: ${msg}`);
this.persistLog(runId, stepRunId, level, msg);
@@ -98,7 +98,7 @@ export class ScenarioSchedulerService {
* no login step or the environment cannot be found.
*/
private async resolveRunEnvironment(
scenarioId: number,
scenarioId: string,
): Promise<EnvironmentUrls | null> {
try {
const scenario = await this.scenarioService.findOne(scenarioId);
@@ -147,7 +147,7 @@ export class ScenarioSchedulerService {
}
}
private async processRunToCompletion(runId: number): Promise<void> {
private async processRunToCompletion(runId: string): Promise<void> {
try {
let stepRun = await this.runStepRepo.findOne({
where: { runId, status: "pending" },
@@ -207,7 +207,7 @@ export class ScenarioSchedulerService {
// ── Shared browser per run ─────────────────────────────────────────────────
private async getOrCreateBrowserHandle(runId: number): Promise<BrowserHandle> {
private async getOrCreateBrowserHandle(runId: string): Promise<BrowserHandle> {
const existing = this.runBrowsers.get(runId);
if (existing) return existing;
@@ -224,7 +224,7 @@ export class ScenarioSchedulerService {
return handle;
}
private async closeBrowserHandle(runId: number): Promise<void> {
private async closeBrowserHandle(runId: string): Promise<void> {
const handle = this.runBrowsers.get(runId);
if (!handle) return;
this.runBrowsers.delete(runId);