@qaick/runner 0.29.0 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package-dist/fixtures/qaick-local-http-regression.qaick.json +5002 -0
- package/package-dist/fixtures/regression-cancel.qaick.json +314 -0
- package/package-dist/fixtures/regression-declared-inputs.qaick.json +281 -0
- package/package-dist/fixtures/regression-input-conflict.qaick.json +548 -0
- package/package-dist/fixtures/regression-script.qaick.json +58 -0
- package/package-dist/fixtures/regression-timeout.qaick.json +314 -0
- package/package-dist/fixtures/script-worker-http.qaick.json +226 -0
- package/package-dist/package.json +5 -5
- package/package-dist/src/runner/RunnerModule.d.ts +11 -2
- package/package-dist/src/runner/RunnerModule.d.ts.map +1 -1
- package/package-dist/src/runner/RunnerModule.js +77 -52
- package/package-dist/src/runner/RunnerModule.js.map +1 -1
- package/package-dist/src/runner/action-template-lowering.d.ts +18 -0
- package/package-dist/src/runner/action-template-lowering.d.ts.map +1 -0
- package/package-dist/src/runner/action-template-lowering.js +83 -0
- package/package-dist/src/runner/action-template-lowering.js.map +1 -0
- package/package-dist/src/runner/adapters.d.ts +45 -0
- package/package-dist/src/runner/adapters.d.ts.map +1 -0
- package/package-dist/src/runner/adapters.js +203 -0
- package/package-dist/src/runner/adapters.js.map +1 -0
- package/package-dist/src/runner/catalog.d.ts +4 -1
- package/package-dist/src/runner/catalog.d.ts.map +1 -1
- package/package-dist/src/runner/catalog.js +11 -6
- package/package-dist/src/runner/catalog.js.map +1 -1
- package/package-dist/src/runner/execution-context.d.ts +3 -0
- package/package-dist/src/runner/execution-context.d.ts.map +1 -0
- package/package-dist/src/runner/execution-context.js +4 -0
- package/package-dist/src/runner/execution-context.js.map +1 -0
- package/package-dist/src/runner/fixture.d.ts.map +1 -1
- package/package-dist/src/runner/fixture.js +14 -27
- package/package-dist/src/runner/fixture.js.map +1 -1
- package/package-dist/src/runner/invocation-inputs.d.ts +33 -0
- package/package-dist/src/runner/invocation-inputs.d.ts.map +1 -0
- package/package-dist/src/runner/invocation-inputs.js +85 -0
- package/package-dist/src/runner/invocation-inputs.js.map +1 -0
- package/package-dist/src/runner/legacy-types.d.ts +81 -0
- package/package-dist/src/runner/legacy-types.d.ts.map +1 -0
- package/package-dist/src/runner/legacy-types.js +7 -0
- package/package-dist/src/runner/legacy-types.js.map +1 -0
- package/package-dist/src/runner/loaded-target.d.ts +13 -0
- package/package-dist/src/runner/loaded-target.d.ts.map +1 -0
- package/package-dist/src/runner/loaded-target.js +23 -0
- package/package-dist/src/runner/loaded-target.js.map +1 -0
- package/package-dist/src/runner/package-loader.d.ts +3 -2
- package/package-dist/src/runner/package-loader.d.ts.map +1 -1
- package/package-dist/src/runner/package-loader.js +3 -112
- package/package-dist/src/runner/package-loader.js.map +1 -1
- package/package-dist/src/runner/result-export.d.ts +7 -2
- package/package-dist/src/runner/result-export.d.ts.map +1 -1
- package/package-dist/src/runner/result-export.js +3 -2
- package/package-dist/src/runner/result-export.js.map +1 -1
- package/package-dist/src/runner/runtime-client-adapter.d.ts +25 -0
- package/package-dist/src/runner/runtime-client-adapter.d.ts.map +1 -1
- package/package-dist/src/runner/runtime-client-adapter.js +44 -2
- package/package-dist/src/runner/runtime-client-adapter.js.map +1 -1
- package/package-dist/src/runner/studio-artifact-loader.d.ts +3 -5
- package/package-dist/src/runner/studio-artifact-loader.d.ts.map +1 -1
- package/package-dist/src/runner/studio-artifact-loader.js +4 -335
- package/package-dist/src/runner/studio-artifact-loader.js.map +1 -1
- package/package-dist/src/runner/studio-hierarchy-loader.d.ts +10 -0
- package/package-dist/src/runner/studio-hierarchy-loader.d.ts.map +1 -0
- package/package-dist/src/runner/studio-hierarchy-loader.js +271 -0
- package/package-dist/src/runner/studio-hierarchy-loader.js.map +1 -0
- package/package-dist/src/runner/types.d.ts +12 -0
- package/package-dist/src/runner/types.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { checkoutNestedPackage, checkoutPackage, checkoutProjectPackage } from './fixture.js';
|
|
4
|
-
import {
|
|
4
|
+
import { PackageLoadError } from './package-loader.js';
|
|
5
|
+
import { loadTarget, restoreTarget } from './loaded-target.js';
|
|
5
6
|
import { PackageCatalog } from './catalog.js';
|
|
6
7
|
import { RUNNER_CONTRACT_VERSION } from './types.js';
|
|
7
8
|
import { RunnerWalkthrough } from '../demo/RunnerWalkthrough.js';
|
|
8
9
|
import { RUNNER_BUILD_LABEL, RUNNER_UI_VERSION } from '../build-info.js';
|
|
9
10
|
import { collectSecretValues, redactExecutionEvent, redactExecutionResult, redactKeyValuePairs } from './redaction.js';
|
|
10
11
|
import { createResultExport } from './result-export.js';
|
|
12
|
+
import { inspectDeclaredInputs, inputDrafts, resolvedArguments } from './invocation-inputs.js';
|
|
11
13
|
import { persistRunHistory } from './history.js';
|
|
12
14
|
const HISTORY_KEY = 'qaick-runner.history.v1';
|
|
13
15
|
const HISTORY_RETENTION_KEY = 'qaick-runner.history-retention.v1';
|
|
14
16
|
const IMPORTED_PACKAGE_KEY = 'qaick-runner.imported-package.v1';
|
|
15
17
|
const THEME_STORAGE_KEY = 'qaick-studio-theme';
|
|
16
18
|
function readRunHistory() { try {
|
|
17
|
-
|
|
18
|
-
if (!stored)
|
|
19
|
-
return [];
|
|
20
|
-
return JSON.parse(stored).map(entry => ({ ...entry, artifactSchemaVersion: entry.artifactSchemaVersion === 6 && entry.packageId?.startsWith('studio-') ? 7 : entry.artifactSchemaVersion }));
|
|
19
|
+
return JSON.parse(localStorage.getItem(HISTORY_KEY) ?? '[]');
|
|
21
20
|
}
|
|
22
21
|
catch {
|
|
23
22
|
return [];
|
|
@@ -31,10 +30,7 @@ catch {
|
|
|
31
30
|
} }
|
|
32
31
|
function readImportedPackage() { try {
|
|
33
32
|
const stored = localStorage.getItem(IMPORTED_PACKAGE_KEY);
|
|
34
|
-
|
|
35
|
-
return undefined;
|
|
36
|
-
const pack = loadExecutionPackage(stored);
|
|
37
|
-
return pack.artifactSchemaVersion ? pack : pack.packageId.startsWith('studio-') ? { ...pack, artifactSchemaVersion: 7 } : pack;
|
|
33
|
+
return stored ? restoreTarget(stored) : undefined;
|
|
38
34
|
}
|
|
39
35
|
catch {
|
|
40
36
|
return undefined;
|
|
@@ -51,7 +47,7 @@ packageCatalog.addPackage(checkoutProjectPackage);
|
|
|
51
47
|
packageCatalog.addPackage(checkoutNestedPackage);
|
|
52
48
|
const restoredImportedPackage = readImportedPackage();
|
|
53
49
|
if (restoredImportedPackage)
|
|
54
|
-
packageCatalog.addPackage(restoredImportedPackage);
|
|
50
|
+
packageCatalog.addPackage(restoredImportedPackage.executionPackage, restoredImportedPackage.artifactProvenance);
|
|
55
51
|
export function RunnerModule({ client, showDemo = standaloneDemoRequested(), onExit }) {
|
|
56
52
|
return showDemo ? _jsx(RunnerWalkthrough, {}) : _jsx(RunnerApp, { client: client, onExit: onExit });
|
|
57
53
|
}
|
|
@@ -60,8 +56,18 @@ function RunnerApp({ client, onExit }) {
|
|
|
60
56
|
const [theme, setTheme] = useState(readTheme);
|
|
61
57
|
const [showAbout, setShowAbout] = useState(false);
|
|
62
58
|
const [environment, setEnvironment] = useState('test');
|
|
63
|
-
const [executionPackage, setExecutionPackage] = useState(restoredImportedPackage ?? checkoutPackage);
|
|
64
|
-
const [
|
|
59
|
+
const [executionPackage, setExecutionPackage] = useState(restoredImportedPackage?.executionPackage ?? checkoutPackage);
|
|
60
|
+
const [inputEdits, setInputEdits] = useState(() => inputDrafts(inspectDeclaredInputs(restoredImportedPackage?.executionPackage ?? checkoutPackage).declarations));
|
|
61
|
+
const { declarations, conflict, unresolved } = inspectDeclaredInputs(executionPackage);
|
|
62
|
+
const inputBlocker = conflict ?? unresolved;
|
|
63
|
+
let inputError;
|
|
64
|
+
let inputs = {};
|
|
65
|
+
try {
|
|
66
|
+
inputs = resolvedArguments(declarations, inputEdits);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
inputError = error instanceof Error ? error.message : 'Invalid invocation inputs';
|
|
70
|
+
}
|
|
65
71
|
const [runtimeValues, setRuntimeValues] = useState({});
|
|
66
72
|
const [timeoutMs, setTimeoutMs] = useState('none');
|
|
67
73
|
const [editingInputs, setEditingInputs] = useState(false);
|
|
@@ -72,7 +78,7 @@ function RunnerApp({ client, onExit }) {
|
|
|
72
78
|
const breakpointsRef = useRef(new Set());
|
|
73
79
|
const [stopAfterStepId, setStopAfterStepId] = useState();
|
|
74
80
|
const [stepStates, setStepStates] = useState(() => packageStepsFor(executionPackage).map(() => 'pending'));
|
|
75
|
-
const stepStateCache = useRef(Object.fromEntries(packageStepsFor(executionPackage).map(step => [step.
|
|
81
|
+
const stepStateCache = useRef(Object.fromEntries(packageStepsFor(executionPackage).map(step => [occurrenceKey(step.structuralAddress), 'pending'])));
|
|
76
82
|
const [loopProgress, setLoopProgress] = useState(() => loopProgressFor(packageStepsFor(executionPackage)));
|
|
77
83
|
const [loopStartedAt, setLoopStartedAt] = useState({});
|
|
78
84
|
const [clock, setClock] = useState(() => Date.now());
|
|
@@ -93,15 +99,20 @@ function RunnerApp({ client, onExit }) {
|
|
|
93
99
|
const allResults = result?.children?.flatMap(child => flattenResultSteps(child.steps)) ?? flattenResultSteps(result?.steps ?? []);
|
|
94
100
|
const displaySteps = orderDisplayStepsByResult(flattenDisplaySteps(packageSteps, 0, undefined, undefined, collapsedSteps), allResults);
|
|
95
101
|
const hasNestedSteps = flattenDisplaySteps(packageSteps).some(step => step.depth > 0);
|
|
96
|
-
const displayResults = displaySteps.map(step =>
|
|
102
|
+
const displayResults = displaySteps.map(step => resultForOccurrence(allResults, step.structuralAddress));
|
|
103
|
+
const selectedInvocations = resultsForOccurrence(allResults, displaySteps[selectedStep]?.structuralAddress);
|
|
97
104
|
const visibleEvents = events.filter(event => { const categoryMatches = eventFilter === 'all' || eventFilter === 'parallel' && event.parallel || eventFilter === 'steps' && event.stepId || eventFilter === 'lifecycle' && !event.stepId && !event.parallel || eventFilter === 'failures' && (event.state === 'failed' || event.type.includes('error')); const searchMatches = !eventSearch.trim() || `${event.type} ${event.state} ${event.message} ${event.stepName ?? ''}`.toLowerCase().includes(eventSearch.trim().toLowerCase()); return categoryMatches && searchMatches; });
|
|
98
105
|
async function run(dryRun = false) {
|
|
106
|
+
if (inputBlocker || inputError) {
|
|
107
|
+
setLoadError(inputBlocker?.message ?? inputError);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
99
110
|
const id = crypto.randomUUID();
|
|
100
111
|
setViewingHistoryEntry(undefined);
|
|
101
112
|
setRequestId(id);
|
|
102
113
|
setResult(undefined);
|
|
103
114
|
setEvents([]);
|
|
104
|
-
stepStateCache.current = Object.fromEntries(flattenDisplaySteps(packageSteps).map(step => [step.
|
|
115
|
+
stepStateCache.current = Object.fromEntries(flattenDisplaySteps(packageSteps).map(step => [occurrenceKey(step.structuralAddress), 'pending']));
|
|
105
116
|
setStepStates(displaySteps.map(() => 'pending'));
|
|
106
117
|
setLoopProgress(loopProgressFor(executionPackage.target.kind === 'project' ? executionPackage.flows?.flatMap(flow => flow.steps) ?? [] : executionPackage.steps));
|
|
107
118
|
setLoopStartedAt({});
|
|
@@ -123,10 +134,10 @@ function RunnerApp({ client, onExit }) {
|
|
|
123
134
|
setLoopStartedAt(previous => ({ ...previous, [event.iteration.loopId]: previous[event.iteration.loopId] ?? Date.now() }));
|
|
124
135
|
setClock(Date.now());
|
|
125
136
|
}
|
|
126
|
-
if (event.
|
|
127
|
-
const nextState = event.
|
|
128
|
-
stepStateCache.current[event.
|
|
129
|
-
setStepStates(previous => previous.map((state, index) => displaySteps[index]?.
|
|
137
|
+
if (event.structuralAddress) {
|
|
138
|
+
const nextState = event.state;
|
|
139
|
+
stepStateCache.current[occurrenceKey(event.structuralAddress)] = nextState;
|
|
140
|
+
setStepStates(previous => previous.map((state, index) => occurrenceKey(displaySteps[index]?.structuralAddress) === occurrenceKey(event.structuralAddress) ? nextState : state));
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
})();
|
|
@@ -134,14 +145,14 @@ function RunnerApp({ client, onExit }) {
|
|
|
134
145
|
const executionResult = redactExecutionResult(await client.result(id), secretValues);
|
|
135
146
|
const finalSteps = flattenResultSteps(executionResult.children?.flatMap(child => child.steps) ?? executionResult.steps);
|
|
136
147
|
const finalControlState = await Promise.resolve(client.controlState(id));
|
|
137
|
-
const finalStateById = Object.fromEntries(
|
|
148
|
+
const finalStateById = Object.fromEntries(flattenDisplaySteps(packageSteps).map(step => [occurrenceKey(step.structuralAddress), stateForOccurrence(finalSteps, step.structuralAddress) ?? 'pending']));
|
|
138
149
|
stepStateCache.current = { ...stepStateCache.current, ...finalStateById };
|
|
139
150
|
setResult(executionResult);
|
|
140
|
-
setStepStates(displaySteps.map(step => stepStateCache.current[step.
|
|
151
|
+
setStepStates(displaySteps.map(step => stepStateCache.current[occurrenceKey(step.structuralAddress)] ?? 'pending'));
|
|
141
152
|
setRunState(executionResult.state);
|
|
142
153
|
setControlState(finalControlState ?? (executionResult.state === 'cancelled' ? 'stopped' : executionResult.state));
|
|
143
154
|
const finishedAt = executionResult.finishedAt ?? new Date().toISOString();
|
|
144
|
-
const entry = { requestId: id, startedAt: executionResult.startedAt, finishedAt, target: executionPackage.target.name, packageId: executionPackage.packageId, artifactSchemaVersion: executionPackage.
|
|
155
|
+
const entry = { requestId: id, startedAt: executionResult.startedAt, finishedAt, target: executionPackage.target.name, packageId: executionPackage.packageId, artifactSchemaVersion: packageCatalog.artifactProvenance(executionPackage.packageId)?.originalVersion, artifactProvenance: packageCatalog.artifactProvenance(executionPackage.packageId), executionSchemaVersion: executionPackage.schemaVersion, environment: executionResult.environment, state: executionResult.state, durationMs: Math.max(0, new Date(finishedAt).getTime() - new Date(executionResult.startedAt).getTime()), result: executionResult };
|
|
145
156
|
setRunHistory(previous => persistRunHistory(localStorage, HISTORY_KEY, [entry, ...previous.filter(item => item.requestId !== id)], historyRetention, secretValues));
|
|
146
157
|
}
|
|
147
158
|
async function stop() { if (requestId)
|
|
@@ -164,41 +175,43 @@ function RunnerApp({ client, onExit }) {
|
|
|
164
175
|
else
|
|
165
176
|
next.add(stepId); breakpointsRef.current = next; return next; }); }
|
|
166
177
|
function toggleStopAfter(stepId) { setStopAfterStepId(previous => previous === stepId ? undefined : stepId); }
|
|
167
|
-
function exportResult(value
|
|
168
|
-
return; const exportPayload = createResultExport(value, packageId, schemaVersion, collectSecretValues(inputs, runtimeValues)); const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${value.target.name.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}-${value.requestId}.result.json`; link.click(); URL.revokeObjectURL(url); }
|
|
178
|
+
function exportResult(value, packageId, schemaVersion, provenance, executionVersion) { if (!value)
|
|
179
|
+
return; const exportPayload = createResultExport(value, packageId ?? 'unknown', schemaVersion, collectSecretValues(inputs, runtimeValues), provenance, executionVersion); const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${value.target.name.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}-${value.requestId}.result.json`; link.click(); URL.revokeObjectURL(url); }
|
|
169
180
|
function reloadHistoryEntry(entry) { let loadedPackage = executionPackage; if (entry.packageId) {
|
|
170
181
|
try {
|
|
171
182
|
loadedPackage = packageCatalog.get(entry.packageId);
|
|
172
183
|
setExecutionPackage(loadedPackage);
|
|
173
184
|
}
|
|
174
185
|
catch { /* The result remains viewable when its package is no longer loaded. */ }
|
|
175
|
-
} const steps = loadedPackage.target.kind === 'project' ? loadedPackage.flows?.flatMap(flow => flow.steps) ?? [] : loadedPackage.steps; const storedSteps = flattenResultSteps(entry.result.children?.flatMap(child => child.steps) ?? entry.result.steps); const states = new Map(
|
|
186
|
+
} const steps = loadedPackage.target.kind === 'project' ? loadedPackage.flows?.flatMap(flow => flow.steps) ?? [] : loadedPackage.steps; const storedSteps = flattenResultSteps(entry.result.children?.flatMap(child => child.steps) ?? entry.result.steps); const states = new Map(flattenDisplaySteps(steps).map(step => [occurrenceKey(step.structuralAddress), stateForOccurrence(storedSteps, step.structuralAddress) ?? 'pending'])); setRequestId(entry.requestId); setEnvironment(entry.environment); setResult(entry.result); setViewingHistoryEntry(entry.requestId); setEvents([]); setSelectedStep(0); setStepStates(flattenDisplaySteps(steps).map(step => states.get(occurrenceKey(step.structuralAddress)) ?? 'pending')); setRunState(entry.state); document.querySelector('.runner-workspace')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
|
176
187
|
function clearHistory() { setRunHistory([]); localStorage.removeItem(HISTORY_KEY); }
|
|
177
188
|
function deleteHistoryEntry(requestIdToDelete) { setRunHistory(previous => { const next = previous.filter(item => item.requestId !== requestIdToDelete); localStorage.setItem(HISTORY_KEY, JSON.stringify(next)); return next; }); }
|
|
178
189
|
function changeHistoryRetention(value) { setHistoryRetention(value); localStorage.setItem(HISTORY_RETENTION_KEY, String(value)); setRunHistory(previous => { const next = previous.slice(0, value); localStorage.setItem(HISTORY_KEY, JSON.stringify(next)); return next; }); }
|
|
179
190
|
function rememberVisibleStates() { displaySteps.forEach((step, index) => { const state = stepStates[index]; if (state)
|
|
180
|
-
stepStateCache.current[step.
|
|
191
|
+
stepStateCache.current[occurrenceKey(step.structuralAddress)] = state; }); }
|
|
181
192
|
function toggleStep(stepId) { rememberVisibleStates(); const next = new Set(collapsedSteps); if (next.has(stepId))
|
|
182
193
|
next.delete(stepId);
|
|
183
194
|
else
|
|
184
|
-
next.add(stepId); const nextSteps = flattenDisplaySteps(packageSteps, 0, undefined, undefined, next); setCollapsedSteps(next); setStepStates(nextSteps.map(step => stepStateCache.current[step.
|
|
185
|
-
function setAllCollapsed(collapsed) { rememberVisibleStates(); const next = collapsed ? new Set(containerStepIds(packageSteps)) : new Set(); const nextSteps = flattenDisplaySteps(packageSteps, 0, undefined, undefined, next); setCollapsedSteps(next); setStepStates(nextSteps.map(step => stepStateCache.current[step.
|
|
195
|
+
next.add(stepId); const nextSteps = flattenDisplaySteps(packageSteps, 0, undefined, undefined, next); setCollapsedSteps(next); setStepStates(nextSteps.map(step => stepStateCache.current[occurrenceKey(step.structuralAddress)] ?? 'pending')); }
|
|
196
|
+
function setAllCollapsed(collapsed) { rememberVisibleStates(); const next = collapsed ? new Set(containerStepIds(packageSteps)) : new Set(); const nextSteps = flattenDisplaySteps(packageSteps, 0, undefined, undefined, next); setCollapsedSteps(next); setStepStates(nextSteps.map(step => stepStateCache.current[occurrenceKey(step.structuralAddress)] ?? 'pending')); }
|
|
186
197
|
async function loadPackage(file) {
|
|
187
198
|
if (!file)
|
|
188
199
|
return;
|
|
189
200
|
try {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
201
|
+
const target = loadTarget(await file.text());
|
|
202
|
+
const loaded = target.executionPackage;
|
|
203
|
+
packageCatalog.addPackage(loaded, target.artifactProvenance);
|
|
204
|
+
localStorage.setItem(IMPORTED_PACKAGE_KEY, JSON.stringify(target));
|
|
193
205
|
setCatalogVersion(version => version + 1);
|
|
194
206
|
setExecutionPackage(loaded);
|
|
195
207
|
setBreakpoints(new Set());
|
|
196
208
|
breakpointsRef.current = new Set();
|
|
197
209
|
setStopAfterStepId(undefined);
|
|
198
|
-
|
|
210
|
+
if (!inspectDeclaredInputs(loaded).conflict && !inspectDeclaredInputs(loaded).unresolved)
|
|
211
|
+
setInputEdits(inputDrafts(inspectDeclaredInputs(loaded).declarations));
|
|
199
212
|
setRuntimeValues(Object.fromEntries((loaded.runtimeRequirements ?? []).map(name => [name, ''])));
|
|
200
213
|
setCollapsedSteps(new Set());
|
|
201
|
-
stepStateCache.current = Object.fromEntries(flattenDisplaySteps(loaded.target.kind === 'project' ? loaded.flows?.flatMap(flow => flow.steps) ?? [] : loaded.steps).map(step => [step.
|
|
214
|
+
stepStateCache.current = Object.fromEntries(flattenDisplaySteps(loaded.target.kind === 'project' ? loaded.flows?.flatMap(flow => flow.steps) ?? [] : loaded.steps).map(step => [occurrenceKey(step.structuralAddress), 'pending']));
|
|
202
215
|
setStepStates((loaded.target.kind === 'project' ? loaded.flows?.flatMap(flow => flattenDisplaySteps(flow.steps)) ?? [] : flattenDisplaySteps(loaded.steps)).map(() => 'pending'));
|
|
203
216
|
setLoopProgress(loopProgressFor(loaded.target.kind === 'project' ? loaded.flows?.flatMap(flow => flow.steps) ?? [] : loaded.steps));
|
|
204
217
|
setLoopStartedAt({});
|
|
@@ -230,16 +243,17 @@ function RunnerApp({ client, onExit }) {
|
|
|
230
243
|
return undefined; const timer = window.setInterval(() => setClock(Date.now()), 250); return () => window.clearInterval(timer); }, [runState]);
|
|
231
244
|
useEffect(() => { const navigateIssue = (event) => { const target = event.target; const issue = target.closest('.validation-issue'); const stepId = issue?.dataset.stepId; const eventRow = target.closest('.event-row'); const index = stepId ? displaySteps.findIndex(step => step.id === stepId || step.id.endsWith(`.${stepId}`)) : eventRow ? displaySteps.findIndex(step => eventRow.textContent?.includes(step.name)) : -1; if (index >= 0)
|
|
232
245
|
setSelectedStep(index); }; document.addEventListener('click', navigateIssue); return () => document.removeEventListener('click', navigateIssue); }, [displaySteps]);
|
|
233
|
-
return _jsxs("div", { className: "runner-shell", children: [_jsxs("header", { className: "runner-header", children: [_jsxs("div", { className: "runner-brand", children: [_jsx("span", { className: "runner-brand-mark", children: "QAICK" }), _jsx("span", { className: "runner-brand-product", children: "Runner" }), _jsx("span", { className: "alpha-label", children: RUNNER_UI_VERSION }), onExit && _jsx("button", { className: "about-button", onClick: onExit, children: "Studio" })] }), _jsx("span", { className: "header-spacer" }), _jsx("a", { className: "demo-entry", href: "?demo", children: "View demo" }), _jsx("button", { className: "about-button", onClick: () => setShowAbout(true), children: "About" }), _jsxs("label", { className: "theme-picker", children: ["Theme", _jsxs("select", { value: theme, onChange: event => setTheme(event.target.value), children: [_jsx("option", { value: "light", children: "Light" }), _jsx("option", { value: "dark", children: "Dark" })] })] })] }), _jsxs("main", { className: "runner-main", children: [_jsxs("nav", { className: "runner-nav", children: [_jsx("p", { className: "nav-label", children: "Workspace" }), _jsx(NavItem, { active: true, icon: "\u25C8", text: "Execution Package" }), _jsx(NavItem, { icon: "\u25A1", text: "Projects" }), _jsx(NavItem, { icon: "\u2318", text: "Flows" }), _jsx(NavItem, { icon: "\u21B6", text: "Run History" }), _jsxs("div", { className: "package-card", children: [_jsx("small", { children: "Loaded package" }), _jsxs("strong", { children: [executionPackage.packageId, ".qaick.json"] }), _jsxs("small", { children: ["
|
|
246
|
+
return _jsxs("div", { className: "runner-shell", children: [_jsxs("header", { className: "runner-header", children: [_jsxs("div", { className: "runner-brand", children: [_jsx("span", { className: "runner-brand-mark", children: "QAICK" }), _jsx("span", { className: "runner-brand-product", children: "Runner" }), _jsx("span", { className: "alpha-label", children: RUNNER_UI_VERSION }), onExit && _jsx("button", { className: "about-button", onClick: onExit, children: "Studio" })] }), _jsx("span", { className: "header-spacer" }), _jsx("a", { className: "demo-entry", href: "./examples/qaick-local-http-regression.qaick.json" with { type: 'json' }, download: true, title: "Download QAick Local HTTP Regression. Start the local fixture on port 5197, then load this file. Runner executes both Flows. Editing is restricted in Studio.", children: "HTTP example" }), _jsx("a", { className: "demo-entry", href: "?demo", children: "View demo" }), _jsx("button", { className: "about-button", onClick: () => setShowAbout(true), children: "About" }), _jsxs("label", { className: "theme-picker", children: ["Theme", _jsxs("select", { value: theme, onChange: event => setTheme(event.target.value), children: [_jsx("option", { value: "light", children: "Light" }), _jsx("option", { value: "dark", children: "Dark" })] })] })] }), _jsxs("main", { className: "runner-main", children: [_jsxs("nav", { className: "runner-nav", children: [_jsx("p", { className: "nav-label", children: "Workspace" }), _jsx(NavItem, { active: true, icon: "\u25C8", text: "Execution Package" }), _jsx(NavItem, { icon: "\u25A1", text: "Projects" }), _jsx(NavItem, { icon: "\u2318", text: "Flows" }), _jsx(NavItem, { icon: "\u21B6", text: "Run History" }), _jsxs("div", { className: "package-card", children: [_jsx("small", { children: "Loaded package" }), _jsxs("strong", { children: [executionPackage.packageId, ".qaick.json"] }), _jsxs("small", { children: ["Execution v", executionPackage.schemaVersion, " \u00B7 ", executionPackage.definitions.length, " definitions"] })] })] }), _jsxs("section", { className: "runner-workspace", children: [_jsx("div", { className: "eyebrow", children: "EXECUTION PACKAGE" }), _jsx("h1", { children: executionPackage.target.name }), _jsxs("p", { className: "description", children: ["Execute the selected QAick ", executionPackage.target.kind === 'project' ? 'Project' : 'Flow', " and capture structured evidence."] }), _jsxs("div", { className: "run-controls", children: [_jsxs("label", { children: ["Target", _jsx("select", { value: executionPackage.packageId, onChange: event => { const next = packageCatalog.get(event.target.value); setExecutionPackage(next); if (!inspectDeclaredInputs(next).conflict && !inspectDeclaredInputs(next).unresolved)
|
|
247
|
+
setInputEdits(inputDrafts(inspectDeclaredInputs(next).declarations)); setBreakpoints(new Set()); breakpointsRef.current = new Set(); setStopAfterStepId(undefined); setLoadError(undefined); setLoadIssues([]); setRuntimeValues(Object.fromEntries((next.runtimeRequirements ?? []).map(name => [name, '']))); setStepStates((next.target.kind === 'project' ? next.flows?.flatMap(flow => flattenDisplaySteps(flow.steps)) ?? [] : flattenDisplaySteps(next.steps)).map(() => 'pending')); setLoopProgress(loopProgressFor(next.target.kind === 'project' ? next.flows?.flatMap(flow => flow.steps) ?? [] : next.steps)); setSelectedStep(0); setResult(undefined); setEvents([]); setRunState('ready'); }, children: packageCatalog.targets().map(item => _jsx("option", { value: item.packageId, children: item.target.name }, item.packageId)) })] }), _jsxs("label", { children: ["Environment", _jsxs("select", { value: environment, onChange: event => setEnvironment(event.target.value), children: [_jsx("option", { children: "test" }), _jsx("option", { children: "local" }), _jsx("option", { children: "staging" })] })] }), _jsxs("label", { children: ["Timeout", _jsxs("select", { value: timeoutMs, onChange: event => setTimeoutMs(event.target.value), children: [_jsx("option", { value: "none", children: "None" }), _jsx("option", { value: "1000", children: "1 second" }), _jsx("option", { value: "3000", children: "3 seconds" }), _jsx("option", { value: "10000", children: "10 seconds" })] })] }), _jsxs("label", { children: ["Run configuration", _jsxs("select", { defaultValue: "Default", children: [_jsx("option", { children: "Default" }), _jsx("option", { children: "Debug" })] })] }), _jsxs("label", { className: "file-button", children: ["Load package", _jsx("input", { type: "file", accept: "application/json,.json", onChange: event => void loadPackage(event.target.files?.[0]) })] }), _jsx("button", { className: "button", onClick: () => setEditingInputs(value => !value), children: editingInputs ? 'Done inputs' : 'Edit inputs' }), _jsx("button", { className: "button primary", onClick: () => void run(), disabled: !!inputBlocker || runState === 'preflighting' || runState === 'running', children: "\u25B6\u00A0 Run" }), _jsx("button", { className: "button dry-run", onClick: () => void run(true), disabled: !!inputBlocker || runState === "preflighting" || runState === "running", children: "\u25CE\u00A0 Pre-run validation" }), _jsx("button", { className: "button stop", onClick: () => void stop(), disabled: runState !== 'preflighting' && runState !== 'running', children: "\u25A0\u00A0 Stop" }), _jsx("button", { className: "button", onClick: () => void pause(), disabled: controlState !== 'running', children: "\u2161\u00A0 Pause" }), _jsx("button", { className: `button ${controlState === 'paused' ? 'resume-highlight' : ''}`, onClick: () => void resume(), disabled: controlState !== 'paused', children: "\u25B6\u00A0 Resume" }), _jsx("button", { className: "button", onClick: () => void stepExecution(), disabled: controlState !== 'paused', children: "\u21A6\u00A0 Step" }), breakpoints.size > 0 && _jsxs("span", { className: "debug-session-badge", children: [breakpoints.size, " breakpoint", breakpoints.size === 1 ? "" : "s", " armed"] })] }), viewingHistoryEntry && _jsxs("div", { className: "history-view-banner", children: ["Viewing stored results \u00B7 ", viewingHistoryEntry] }), conflict && _jsxs("section", { role: "alert", children: [_jsx("p", { children: conflict.message }), _jsx("ul", { children: conflict.entries.map((entry, index) => _jsxs("li", { children: [entry.owner, " \u00B7 ", entry.declaration.name, " \u00B7 ", entry.declaration.type, " \u00B7 ", entry.declaration.required ? 'required' : 'optional', " \u00B7 default: ", Object.hasOwn(entry.declaration, 'defaultValue') ? JSON.stringify(entry.declaration.defaultValue) : 'none'] }, index)) }), _jsx("p", { children: "Retained input drafts:" }), _jsx("ul", { children: Object.entries(inputEdits).map(([name, draft]) => _jsxs("li", { children: [name, " \u00B7 ", draft.supplied ? 'supplied' : 'omitted'] }, name)) })] }), unresolved && _jsxs("section", { role: "alert", children: [_jsx("p", { children: unresolved.message }), _jsx("ul", { children: unresolved.entries.map((entry, index) => _jsxs("li", { children: [entry.owner, " \u00B7 ", entry.name, " \u00B7 required \u00B7 type unresolved"] }, index)) }), _jsx("p", { children: "Retained input drafts:" }), _jsx("ul", { children: Object.entries(inputEdits).map(([name, draft]) => _jsxs("li", { children: [name, " \u00B7 ", draft.supplied ? 'supplied' : 'omitted'] }, name)) })] }), loadError && _jsx("p", { className: "load-error", children: loadError }), loadIssues.length > 0 && _jsx(PreRunValidation, { issues: loadIssues, title: "Artifact validation" }), result?.issues !== undefined && _jsx(PreRunValidation, { issues: result.issues }), result?.warnings !== undefined && result.warnings.length > 0 && _jsx(PreRunValidation, { issues: result.warnings, title: "Browser execution warnings" }), editingInputs && _jsxs("section", { className: "inputs-panel panel", children: [_jsxs("div", { className: "panel-heading", children: [_jsx("h2", { children: "Invocation Inputs" }), _jsx("span", { className: "panel-spacer" }), _jsx("span", { className: "input-hint", children: "Values are sent with the next run" })] }), _jsxs("div", { className: "input-grid", children: [inputBlocker ? _jsx("p", { children: "Invocation input editing is unavailable until requirements are resolved." }) : declarations.map(declaration => { const draft = inputEdits[declaration.name] ?? { supplied: false, text: '' }; const change = (text) => setInputEdits(previous => ({ ...previous, [declaration.name]: { supplied: true, text } })); return _jsxs("div", { children: [_jsxs("label", { children: [_jsx("input", { type: "checkbox", "aria-label": `Supply ${declaration.name}`, checked: draft.supplied, onChange: event => setInputEdits(previous => ({ ...previous, [declaration.name]: { ...draft, supplied: event.target.checked } })) }), "Supply ", declaration.name] }), declaration.type === 'boolean' ? _jsxs("label", { className: "input-field", children: [declaration.name, _jsxs("select", { value: draft.text, onChange: event => change(event.target.value), children: [_jsx("option", { value: "false", children: "false" }), _jsx("option", { value: "true", children: "true" })] })] }) : _jsx(InputField, { label: declaration.name, value: draft.text, onChange: change }), _jsxs("small", { children: [declaration.type, " \u00B7 ", draft.supplied ? 'supplied' : Object.hasOwn(declaration, 'defaultValue') ? 'omitted — declared default applies' : 'omitted', declaration.required ? ' · required' : ''] })] }, declaration.name); }), inputError && _jsx("p", { role: "alert", children: inputError }), (executionPackage.runtimeRequirements ?? []).map(name => _jsx(InputField, { label: `Runtime · ${name}`, value: runtimeValues[name] ?? '', onChange: value => setRuntimeValues(previous => ({ ...previous, [name]: value })) }, name))] })] }), _jsxs("section", { className: "panel", children: [_jsxs("div", { className: "panel-heading", children: [_jsx("h2", { children: "Live Execution" }), _jsx("span", { className: "panel-spacer" }), _jsxs("span", { className: "execution-menu-label", children: ["Right-click a row to manage its breakpoint \u00B7 Selected: ", displaySteps[selectedStep]?.name ?? 'None'] }), _jsxs("span", { className: runState === 'running' ? 'running' : runState === 'failed' ? 'stopped' : 'run-status', children: ["\u25CF ", statusLabel] })] }), hasNestedSteps && _jsxs("div", { className: "hierarchy-hint", children: [_jsx("strong", { children: "Nested execution" }), _jsx("span", { children: "Use \u2212 / + to collapse or expand nested Operations, Control Blocks, and loops." }), _jsx("button", { className: "hierarchy-action", onClick: () => setAllCollapsed(true), children: "Collapse all" }), _jsx("button", { className: "hierarchy-action", onClick: () => setAllCollapsed(false), children: "Expand all" })] }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "#" }), _jsx("th", { children: "Step hierarchy" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Start time" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: displaySteps.map((step, index) => { const progress = step.loopTotal ? loopProgress[step.id] ?? { current: 0, total: step.loopTotal } : undefined; const percent = progress ? Math.round(progress.current / progress.total * 100) : 0; const isLoop = Boolean(step.loopTotal); const displayState = displayResults[index]?.state ?? stepStates[index] ?? 'pending'; const isLoopContent = Boolean(step.loopId); const isLoopEnd = isLoopContent && displaySteps[index + 1]?.loopId !== step.loopId; const elapsed = step.loopId && loopStartedAt[step.loopId] ? formatElapsed(clock - loopStartedAt[step.loopId]) : undefined; return _jsxs("tr", { className: `${selectedStep === index ? 'selected ' : ''}${isLoop ? 'loop-row ' : ''}${isLoopContent ? 'loop-content ' : ''}${isLoopEnd ? 'loop-end ' : ''}${step.depth > 0 ? 'nested-row' : 'parent-row'}`, tabIndex: 0, onKeyDown: event => { if (event.key === "ArrowDown") {
|
|
234
248
|
event.preventDefault();
|
|
235
249
|
setSelectedStep(Math.min(displaySteps.length - 1, index + 1));
|
|
236
250
|
} if (event.key === "ArrowUp") {
|
|
237
251
|
event.preventDefault();
|
|
238
252
|
setSelectedStep(Math.max(0, index - 1));
|
|
239
|
-
} }, onClick: () => setSelectedStep(index), onContextMenu: event => { event.preventDefault(); setSelectedStep(index); setContextMenu({ x: event.clientX, y: event.clientY, stepId: step.id }); }, children: [_jsx("td", { children: index + 1 }), _jsxs("td", { children: [_jsx("span", { className: `step-icon ${isLoop ? 'loop-icon' : displayState}`, children: isLoop ? '↻' : displayState === 'finished' ? '✓' : displayState === 'waiting' ? '◷' : displayState === 'skipped' ? '–' : '' }), _jsxs("span", { className: "hierarchy-name", style: { paddingLeft: `${step.depth * 18}px` }, children: [step.depth > 0 && _jsx("span", { className: "branch-marker", children: "\u2514" }), step.hasChildren && _jsx("button", { className: "collapse-toggle", "aria-label": `${collapsedSteps.has(step.id) ? 'Expand' : 'Collapse'} ${step.name}`, onClick: event => { event.stopPropagation(); toggleStep(step.id); }, children: collapsedSteps.has(step.id) ? '+' : '−' }), _jsx("span", { children: step.name }), breakpoints.has(step.id) && _jsx("small", { className: "breakpoint-marker", "aria-label": "Breakpoint armed", title: "Breakpoint armed", children: "STOP" }), isLoop && _jsx("small", { className: "kind-label loop-label", children: "LOOP" }), step.depth > 0 ? _jsx("small", { className: "kind-label nested-label", children: "NESTED STEP" }) : !isLoop && _jsx("small", { className: "kind-label", children: step.kind.toUpperCase() })] }), selectedStep === index && _jsx("small", { className: "selected-label", children: "SELECTED" })] }), _jsxs("td", { children: [progress && _jsxs("div", { className: "loop-progress", children: [_jsxs("strong", { children: ["REPEAT LOOP \u00B7 ITERATION ", progress.current, " OF ", progress.total] }), _jsx("span", { className: "loop-bar", children: _jsx("i", { style: { width: `${percent}%` } }) }), _jsx("small", { children: elapsed ? `Elapsed ${elapsed}` : 'Waiting to start' })] }), _jsx("span", { className: `status ${displayState}`, children: isLoop ? 'LOOP' : displayState.toUpperCase() })] }), _jsx("td", { children: displayResults[index] ? new Date(result?.finishedAt ?? '').toLocaleTimeString() : '—' }), _jsx("td", { children: displayResults[index] ? `${displayResults[index].durationMs} ms` : '—' })] }, `${step.id}-${index}`); }) })] }), contextMenu && _jsxs("div", { className: "runner-context-menu", style: { left: contextMenu.x, top: contextMenu.y }, onClick: event => event.stopPropagation(), onContextMenu: event => event.preventDefault(), children: [_jsx("strong", { children: "Breakpoint" }), breakpoints.has(contextMenu.stepId) ? _jsx("button", { onClick: () => { toggleBreakpoint(contextMenu.stepId); setContextMenu(undefined); }, children: "Remove breakpoint" }) : _jsx("button", { onClick: () => { toggleBreakpoint(contextMenu.stepId); setContextMenu(undefined); }, children: "Add breakpoint" })] })] }), _jsxs("section", { className: "panel event-panel", children: [_jsxs("div", { className: "panel-heading", children: [_jsx("h2", { children: "Event Log" }), _jsx("span", { className: "panel-spacer" }), _jsx("input", { className: "event-search", "aria-label": "Search event log", placeholder: "Search events", value: eventSearch, onChange: event => setEventSearch(event.target.value) }), _jsxs("select", { className: "event-filter", value: eventFilter, onChange: event => setEventFilter(event.target.value), children: [_jsx("option", { value: "all", children: "All events" }), _jsx("option", { value: "steps", children: "Step events" }), _jsx("option", { value: "parallel", children: "Parallel groups" }), _jsx("option", { value: "lifecycle", children: "Lifecycle" }), _jsx("option", { value: "failures", children: "Failures" })] })] }), _jsx("table", { children: _jsx("tbody", { children: visibleEvents.map(event => _jsxs("tr", { className: "event-row", children: [_jsx("td", { children: new Date(event.timestamp).toLocaleTimeString() }), _jsx("td", { children: event.state.toUpperCase() }), _jsx("td", { children: _jsx("code", { children: event.type.toUpperCase() }) }), _jsxs("td", { children: [event.message, event.parallel && _jsxs("small", { className: "parallel-event", children: [" \u00B7 Parallel ", event.parallel.completed, "/", event.parallel.total, " (", event.parallel.join, ")"] })] })] }, event.sequence)) }) }), events.length > 0 && visibleEvents.length === 0 && _jsx("p", { className: "empty-state", children: "No events match the current filter." })] }), _jsx(HistoryPanel, { entries: runHistory, onReload: reloadHistoryEntry, retention: historyRetention, onRetentionChange: changeHistoryRetention, onClear: clearHistory, onDelete: deleteHistoryEntry, onExport: exportResult })] }), _jsxs("aside", { className: "inspector", children: [_jsx("h2", { children: "Selected execution" }), _jsx("p", { className: "selected-detail-name", children: _jsx("strong", { children: displaySteps[selectedStep]?.name ?? 'No step selected' }) }), _jsxs("p", { className: "run-id", children: ["Run ID: ", requestId ?? 'No run started'] }), _jsx(InspectorSection, { title: "Artifact summary", children: _jsx(KeyValues, { values: ["Target", executionPackage.target.name, "Type", executionPackage.target.kind, "Package", executionPackage.packageId, "Artifact schema", String(executionPackage.schemaVersion), "Definitions", String(executionPackage.definitions.length), "Required inputs", (executionPackage.requiredInputs ?? []).join(", ") || "None", "Runtime values", (executionPackage.runtimeRequirements ?? []).join(", ") || "None"] }) }), _jsx(InspectorSection, { title: "Run outcome", children: _jsxs("div", { className: "notice", children: [_jsx("strong", { children: result?.terminalReason?.toUpperCase() ?? 'NOT STARTED' }), _jsx("br", {}), result ? `${result.environment} · ${result.state}` : 'Run the package to inspect its terminal outcome.'] }) }), _jsx(InspectorSection, { title: "Selected step inputs", children: _jsx(KeyValues, { values: Object.entries(displayResults[selectedStep]?.inputs ?? inputs).flatMap(([key, value]) => [key, String(value)]) }) }), _jsx(InspectorSection, { title: "Selected step outputs", children: _jsx(KeyValues, { values: displayResults[selectedStep]?.outputs ? Object.entries(displayResults[selectedStep]?.outputs ?? {}).flatMap(([key, value]) => [key, String(value)]) : ['—', 'Available after execution'] }) }), (result?.error || displayResults[selectedStep]?.error) && _jsx(InspectorSection, { title: "Error", children: _jsxs("div", { className: "error-notice", children: [_jsx("strong", { children: displayResults[selectedStep]?.error?.code ?? result?.error?.code }), _jsx("br", {}), displayResults[selectedStep]?.error?.message ?? result?.error?.message] }) }), result?.issues !== undefined && _jsx(InspectorSection, { title: "Pre-run validation", children: _jsx(PreRunValidation, { issues: result.issues }) }), _jsx(InspectorSection, { title: "Invocation path", children: _jsx("div", { className: "notice", children: displayResults[selectedStep]?.invocationPath?.join(" › ") ?? "No invocation path available." }) }), _jsx(InspectorSection, { title: "Iteration history", children: _jsx(IterationHistory, { result: displayResults[selectedStep] }) }), _jsx(InspectorSection, { title: "Execution timing", children: _jsxs("div", { className: "timing-details", children: [_jsxs("span", { children: ["Wall ", result?.executionTiming?.wallDurationMs ?? "—", " ms"] }), _jsxs("span", { children: ["Active ", result?.executionTiming?.activeDurationMs ?? "—", " ms"] }), _jsxs("span", { children: ["Paused ", result?.executionTiming?.pausedDurationMs ?? "—", " ms"] })] }) }), _jsx(InspectorSection, { title: "Selected step attempts", children: _jsx("div", { className: "attempt-history", children: displayResults[selectedStep]?.attempts?.map(attempt => _jsxs("div", { className: "iteration-entry", children: [_jsxs("strong", { children: ["Attempt ", attempt.attempt] }), _jsxs("span", { children: [attempt.kind.toUpperCase(), " \u00B7 ", attempt.state.toUpperCase(), " \u00B7 ", attempt.durationMs, " ms"] })] }, `${attempt.attempt}-${attempt.kind}`)) ?? _jsx("div", { className: "notice", children: "No retry attempts recorded." }) }) }), _jsxs(InspectorSection, { title: "Selected step", children: [_jsxs("div", { className: "notice", children: [_jsx("strong", { children: displaySteps[selectedStep]?.name ?? 'No step selected' }), _jsx("br", {}), displayResults[selectedStep] ? displayResults[selectedStep].error ? 'This step failed. See the error details above.' : `Completed in ${displayResults[selectedStep].durationMs} ms.` : 'Run the package to inspect execution details.', _jsxs("div", { className: "inspector-actions", children: [_jsx("button", { className: "button", onClick: () => void retrySelected(), disabled: !displayResults[selectedStep] || (!displayResults[selectedStep]?.error && runState !== "completed"), children: "Retry Action" }), _jsx("button", { className: `button ${stopAfterStepId === displaySteps[selectedStep]?.id ? "active" : ""}`, onClick: () => displaySteps[selectedStep] && toggleStopAfter(displaySteps[selectedStep].id), disabled: !displaySteps[selectedStep], children: "Stop After" }), _jsx("button", { className: "button", onClick: () => void runFromSelected(), disabled: !displayResults[selectedStep] || runState === "running", children: "Run From Here" })] })] }), _jsxs("div", { className: "selection-nav", children: [_jsx("button", { className: "button", disabled: selectedStep <= 0, onClick: () => setSelectedStep(index => Math.max(0, index - 1)), children: "\u2039 Previous" }), _jsx("span", { children: displaySteps.length ? `${selectedStep + 1} / ${displaySteps.length}` : '0 / 0' }), _jsx("button", { className: "button", disabled: selectedStep >= displaySteps.length - 1, onClick: () => setSelectedStep(index => Math.min(displaySteps.length - 1, index + 1)), children: "Next \u203A" })] })] })] })] }), showAbout && _jsx("div", { className: "about-backdrop", onClick: () => setShowAbout(false), children: _jsxs("section", { className: "about-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "about-title", onClick: event => event.stopPropagation(), children: [_jsx("button", { className: "about-close", onClick: () => setShowAbout(false), "aria-label": "Close About", children: "\u00D7" }), _jsx("div", { className: "runner-logo", children: "\u25B6" }), _jsx("h2", { id: "about-title", children: "QAick Runner" }), _jsx("p", { children: "Standalone execution and evidence viewer for QAICK packages." }), _jsxs("small", { children: [RUNNER_UI_VERSION, " \u00B7 ", RUNNER_BUILD_LABEL, " \u00B7 QAick contract"] }), _jsx("footer", { children: "\u00A9 2006\u20132026 QAick. All rights reserved." })] }) })] });
|
|
253
|
+
} }, onClick: () => setSelectedStep(index), onContextMenu: event => { event.preventDefault(); setSelectedStep(index); setContextMenu({ x: event.clientX, y: event.clientY, stepId: step.id }); }, children: [_jsx("td", { children: index + 1 }), _jsxs("td", { children: [_jsx("span", { className: `step-icon ${isLoop ? 'loop-icon' : displayState}`, children: isLoop ? '↻' : displayState === 'finished' ? '✓' : displayState === 'waiting' ? '◷' : displayState === 'skipped' ? '–' : '' }), _jsxs("span", { className: "hierarchy-name", style: { paddingLeft: `${step.depth * 18}px` }, children: [step.depth > 0 && _jsx("span", { className: "branch-marker", children: "\u2514" }), step.hasChildren && _jsx("button", { className: "collapse-toggle", "aria-label": `${collapsedSteps.has(step.id) ? 'Expand' : 'Collapse'} ${step.name}`, onClick: event => { event.stopPropagation(); toggleStep(step.id); }, children: collapsedSteps.has(step.id) ? '+' : '−' }), _jsx("span", { children: step.name }), breakpoints.has(step.id) && _jsx("small", { className: "breakpoint-marker", "aria-label": "Breakpoint armed", title: "Breakpoint armed", children: "STOP" }), isLoop && _jsx("small", { className: "kind-label loop-label", children: "LOOP" }), step.depth > 0 ? _jsx("small", { className: "kind-label nested-label", children: "NESTED STEP" }) : !isLoop && _jsx("small", { className: "kind-label", children: step.kind.toUpperCase() })] }), selectedStep === index && _jsx("small", { className: "selected-label", children: "SELECTED" })] }), _jsxs("td", { children: [progress && _jsxs("div", { className: "loop-progress", children: [_jsxs("strong", { children: ["REPEAT LOOP \u00B7 ITERATION ", progress.current, " OF ", progress.total] }), _jsx("span", { className: "loop-bar", children: _jsx("i", { style: { width: `${percent}%` } }) }), _jsx("small", { children: elapsed ? `Elapsed ${elapsed}` : 'Waiting to start' })] }), _jsx("span", { className: `status ${displayState}`, children: isLoop ? 'LOOP' : displayState.toUpperCase() })] }), _jsx("td", { children: displayResults[index] ? new Date(result?.finishedAt ?? '').toLocaleTimeString() : '—' }), _jsx("td", { children: displayResults[index] ? `${displayResults[index].durationMs} ms` : '—' })] }, `${step.id}-${index}`); }) })] }), contextMenu && _jsxs("div", { className: "runner-context-menu", style: { left: contextMenu.x, top: contextMenu.y }, onClick: event => event.stopPropagation(), onContextMenu: event => event.preventDefault(), children: [_jsx("strong", { children: "Breakpoint" }), breakpoints.has(contextMenu.stepId) ? _jsx("button", { onClick: () => { toggleBreakpoint(contextMenu.stepId); setContextMenu(undefined); }, children: "Remove breakpoint" }) : _jsx("button", { onClick: () => { toggleBreakpoint(contextMenu.stepId); setContextMenu(undefined); }, children: "Add breakpoint" })] })] }), _jsxs("section", { className: "panel event-panel", children: [_jsxs("div", { className: "panel-heading", children: [_jsx("h2", { children: "Event Log" }), _jsx("span", { className: "panel-spacer" }), _jsx("input", { className: "event-search", "aria-label": "Search event log", placeholder: "Search events", value: eventSearch, onChange: event => setEventSearch(event.target.value) }), _jsxs("select", { className: "event-filter", value: eventFilter, onChange: event => setEventFilter(event.target.value), children: [_jsx("option", { value: "all", children: "All events" }), _jsx("option", { value: "steps", children: "Step events" }), _jsx("option", { value: "parallel", children: "Parallel groups" }), _jsx("option", { value: "lifecycle", children: "Lifecycle" }), _jsx("option", { value: "failures", children: "Failures" })] })] }), _jsx("table", { children: _jsx("tbody", { children: visibleEvents.map(event => _jsxs("tr", { className: "event-row", children: [_jsx("td", { children: new Date(event.timestamp).toLocaleTimeString() }), _jsx("td", { children: event.state.toUpperCase() }), _jsx("td", { children: _jsx("code", { children: event.type.toUpperCase() }) }), _jsxs("td", { children: [event.message, event.parallel && _jsxs("small", { className: "parallel-event", children: [" \u00B7 Parallel ", event.parallel.completed, "/", event.parallel.total, " (", event.parallel.join, ")"] })] })] }, event.sequence)) }) }), events.length > 0 && visibleEvents.length === 0 && _jsx("p", { className: "empty-state", children: "No events match the current filter." })] }), _jsx(HistoryPanel, { entries: runHistory, onReload: reloadHistoryEntry, retention: historyRetention, onRetentionChange: changeHistoryRetention, onClear: clearHistory, onDelete: deleteHistoryEntry, onExport: exportResult })] }), _jsxs("aside", { className: "inspector", children: [_jsx("h2", { children: "Selected execution" }), _jsx("p", { className: "selected-detail-name", children: _jsx("strong", { children: displaySteps[selectedStep]?.name ?? 'No step selected' }) }), _jsxs("p", { className: "run-id", children: ["Run ID: ", requestId ?? 'No run started'] }), _jsx(InspectorSection, { title: "Artifact summary", children: _jsx(KeyValues, { values: ["Target", executionPackage.target.name, "Type", executionPackage.target.kind, "Package", executionPackage.packageId, "Execution format", `Execution v${executionPackage.schemaVersion}`, "Definitions", String(executionPackage.definitions.length), "Required inputs", (executionPackage.requiredInputs ?? []).join(", ") || "None", "Runtime values", (executionPackage.runtimeRequirements ?? []).join(", ") || "None"] }) }), _jsx(InspectorSection, { title: "Run outcome", children: _jsxs("div", { className: "notice", children: [_jsx("strong", { children: result?.state.toUpperCase() ?? 'NOT STARTED' }), _jsx("br", {}), result ? `${result.environment} · ${result.state}` : 'Run the package to inspect its terminal outcome.'] }) }), _jsx(InspectorSection, { title: "Selected step inputs", children: _jsx(KeyValues, { values: Object.entries(displayResults[selectedStep]?.inputs ?? (selectedInvocations.length > 1 ? {} : inputs)).flatMap(([key, value]) => [key, String(value)]) }) }), _jsx(InspectorSection, { title: "Selected step outputs", children: _jsx(KeyValues, { values: displayResults[selectedStep]?.outputs ? Object.entries(displayResults[selectedStep]?.outputs ?? {}).flatMap(([key, value]) => [key, String(value)]) : ['—', selectedInvocations.length > 1 ? `${selectedInvocations.length} recorded invocations; see iteration history` : 'Available after execution'] }) }), (result?.error || displayResults[selectedStep]?.error) && _jsx(InspectorSection, { title: "Error", children: _jsxs("div", { className: "error-notice", children: [_jsx("strong", { children: displayResults[selectedStep]?.error?.code ?? result?.error?.code }), _jsx("br", {}), displayResults[selectedStep]?.error?.message ?? result?.error?.message] }) }), result?.issues !== undefined && _jsx(InspectorSection, { title: "Pre-run validation", children: _jsx(PreRunValidation, { issues: result.issues }) }), _jsx(InspectorSection, { title: "Invocation path", children: _jsx("div", { className: "notice", children: displayResults[selectedStep]?.invocationPath?.join(" › ") ?? "No invocation path available." }) }), _jsx(InspectorSection, { title: "Iteration history", children: _jsx(RecordedInvocations, { results: selectedInvocations }) }), _jsx(InspectorSection, { title: "Execution timing", children: _jsxs("div", { className: "timing-details", children: [_jsxs("span", { children: ["Wall ", result?.executionTiming?.wallDurationMs ?? "—", " ms"] }), _jsxs("span", { children: ["Active ", result?.executionTiming?.activeDurationMs ?? "—", " ms"] }), _jsxs("span", { children: ["Paused ", result?.executionTiming?.pausedDurationMs ?? "—", " ms"] })] }) }), _jsx(InspectorSection, { title: "Selected step attempts", children: _jsx("div", { className: "attempt-history", children: displayResults[selectedStep]?.attempts?.map(attempt => _jsxs("div", { className: "iteration-entry", children: [_jsxs("strong", { children: ["Attempt ", attempt.attempt] }), _jsxs("span", { children: [(attempt.kind ?? 'automatic').toUpperCase(), " \u00B7 ", attempt.state.toUpperCase(), " \u00B7 ", attempt.durationMs, " ms"] })] }, `${attempt.attempt}-${attempt.kind}`)) ?? _jsx("div", { className: "notice", children: "No retry attempts recorded." }) }) }), _jsxs(InspectorSection, { title: "Selected step", children: [_jsxs("div", { className: "notice", children: [_jsx("strong", { children: displaySteps[selectedStep]?.name ?? 'No step selected' }), _jsx("br", {}), displayResults[selectedStep] ? displayResults[selectedStep].error ? 'This step failed. See the error details above.' : `Completed in ${displayResults[selectedStep].durationMs} ms.` : selectedInvocations.length > 1 ? `${selectedInvocations.length} invocations recorded. No single invocation selected.` : 'Run the package to inspect execution details.', _jsxs("div", { className: "inspector-actions", children: [_jsx("button", { className: "button", onClick: () => void retrySelected(), disabled: !displayResults[selectedStep] || (!displayResults[selectedStep]?.error && runState !== "completed"), children: "Retry Action" }), _jsx("button", { className: `button ${stopAfterStepId === displaySteps[selectedStep]?.id ? "active" : ""}`, onClick: () => displaySteps[selectedStep] && toggleStopAfter(displaySteps[selectedStep].id), disabled: !displaySteps[selectedStep], children: "Stop After" }), _jsx("button", { className: "button", onClick: () => void runFromSelected(), disabled: !displayResults[selectedStep] || runState === "running", children: "Run From Here" })] })] }), _jsxs("div", { className: "selection-nav", children: [_jsx("button", { className: "button", disabled: selectedStep <= 0, onClick: () => setSelectedStep(index => Math.max(0, index - 1)), children: "\u2039 Previous" }), _jsx("span", { children: displaySteps.length ? `${selectedStep + 1} / ${displaySteps.length}` : '0 / 0' }), _jsx("button", { className: "button", disabled: selectedStep >= displaySteps.length - 1, onClick: () => setSelectedStep(index => Math.min(displaySteps.length - 1, index + 1)), children: "Next \u203A" })] })] })] })] }), showAbout && _jsx("div", { className: "about-backdrop", onClick: () => setShowAbout(false), children: _jsxs("section", { className: "about-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "about-title", onClick: event => event.stopPropagation(), children: [_jsx("button", { className: "about-close", onClick: () => setShowAbout(false), "aria-label": "Close About", children: "\u00D7" }), _jsx("div", { className: "runner-logo", children: "\u25B6" }), _jsx("h2", { id: "about-title", children: "QAick Runner" }), _jsx("p", { children: "Standalone execution and evidence viewer for QAICK packages." }), _jsxs("small", { children: [RUNNER_UI_VERSION, " \u00B7 ", RUNNER_BUILD_LABEL, " \u00B7 QAick contract"] }), _jsx("footer", { children: "\u00A9 2006\u20132026 QAick. All rights reserved." })] }) })] });
|
|
240
254
|
}
|
|
241
255
|
function NavItem({ icon, text, active = false }) { const scrollTarget = text === 'Run History' ? '.history-panel' : text === 'Flows' ? '.runner-workspace table' : '.runner-workspace'; return _jsxs("a", { className: `nav-item ${active ? 'active' : ''}`, href: `#${text.toLowerCase().replaceAll(' ', '-')}`, onClick: event => { event.preventDefault(); document.querySelector(scrollTarget)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, children: [_jsx("span", { children: icon }), text] }); }
|
|
242
|
-
function HistoryPanel({ entries, retention, onRetentionChange, onClear, onDelete, onExport, onReload }) { return _jsxs("section", { className: "panel history-panel", children: [_jsxs("div", { className: "panel-heading", children: [_jsx("h2", { children: "Run History" }), _jsx("span", { className: "panel-spacer" }), _jsxs("label", { className: "history-retention", children: ["Keep", _jsxs("select", { "aria-label": "Run history retention", value: retention, onChange: event => onRetentionChange(Number(event.target.value)), children: [_jsx("option", { value: "10", children: "10 runs" }), _jsx("option", { value: "20", children: "20 runs" }), _jsx("option", { value: "50", children: "50 runs" }), _jsx("option", { value: "100", children: "100 runs" })] })] }), _jsx("button", { className: "button", onClick: onClear, disabled: !entries.length, children: "Clear history" })] }), entries.length ? _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Target" }), _jsx("th", { children: "Started" }), _jsx("th", { children: "State" }), _jsx("th", { children: "Duration" }), _jsx("th", { children: "Result" })] }) }), _jsx("tbody", { children: entries.map(entry => _jsxs("tr", { children: [_jsxs("td", { children: [entry.target, _jsxs("small", { className: "history-meta", children: [entry.result.provenance ? 'Run From Here' : entry.result.steps.some(step => step.attempts?.some(attempt => attempt.kind === 'interactive')) ? 'Interactive retry' : 'Normal run', " \u00B7 ", entry.packageId ?? 'legacy result', " \u00B7
|
|
256
|
+
function HistoryPanel({ entries, retention, onRetentionChange, onClear, onDelete, onExport, onReload }) { return _jsxs("section", { className: "panel history-panel", children: [_jsxs("div", { className: "panel-heading", children: [_jsx("h2", { children: "Run History" }), _jsx("span", { className: "panel-spacer" }), _jsxs("label", { className: "history-retention", children: ["Keep", _jsxs("select", { "aria-label": "Run history retention", value: retention, onChange: event => onRetentionChange(Number(event.target.value)), children: [_jsx("option", { value: "10", children: "10 runs" }), _jsx("option", { value: "20", children: "20 runs" }), _jsx("option", { value: "50", children: "50 runs" }), _jsx("option", { value: "100", children: "100 runs" })] })] }), _jsx("button", { className: "button", onClick: onClear, disabled: !entries.length, children: "Clear history" })] }), entries.length ? _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Target" }), _jsx("th", { children: "Started" }), _jsx("th", { children: "State" }), _jsx("th", { children: "Duration" }), _jsx("th", { children: "Result" })] }) }), _jsx("tbody", { children: entries.map(entry => _jsxs("tr", { children: [_jsxs("td", { children: [entry.target, _jsxs("small", { className: "history-meta", children: [entry.result.provenance ? 'Run From Here' : entry.result.steps.some(step => step.attempts?.some(attempt => attempt.kind === 'interactive')) ? 'Interactive retry' : 'Normal run', " \u00B7 ", entry.packageId ?? 'legacy result', " \u00B7 Artifact v", entry.artifactProvenance?.originalVersion ?? 'unknown', " \u00B7 Execution v", entry.executionSchemaVersion ?? 'unknown', _jsx("br", {}), "hash ", entry.result.artifactHash ?? 'legacy result'] })] }), _jsx("td", { children: new Date(entry.startedAt).toLocaleString() }), _jsx("td", { children: _jsx("span", { className: `status ${entry.state}`, children: entry.state.toUpperCase() }) }), _jsxs("td", { children: [entry.durationMs, " ms"] }), _jsxs("td", { children: [_jsx("button", { className: "button", onClick: () => onReload(entry), children: "View results" }), _jsx("button", { className: "button", onClick: () => onExport(entry.result, entry.packageId, entry.artifactProvenance?.originalVersion, entry.artifactProvenance, entry.executionSchemaVersion), children: "Export JSON" }), _jsx("button", { className: "button history-delete", onClick: () => onDelete(entry.requestId), children: "Delete" })] })] }, entry.requestId)) })] }) : _jsx("p", { className: "empty-state", children: "Completed and failed runs will be kept here in this browser." })] }); }
|
|
243
257
|
function InspectorSection({ title, children }) { return _jsxs("section", { className: "inspector-section", children: [_jsx("h3", { children: title }), children] }); }
|
|
244
258
|
function KeyValues({ values }) { return _jsx("div", { className: "key-values", children: redactKeyValuePairs(values).map((value, index) => _jsx("span", { children: value }, `${value}-${index}`)) }); }
|
|
245
259
|
function IterationHistory({ result }) { return _jsxs(_Fragment, { children: [_jsx("div", { className: "iteration-history", children: result?.children?.length ? result.children.map((iteration, index) => _jsxs("div", { className: "iteration-entry", children: [_jsx("strong", { children: iteration.name }), _jsxs("span", { children: [iteration.state.toUpperCase(), " \u00B7 ", iteration.durationMs, " ms"] }), _jsx("small", { children: iteration.children?.map(child => `${child.name}: ${Object.entries(child.outputs).map(([key, value]) => `${key}=${String(value)}`).join(', ') || 'no outputs'}`).join(' · ') || 'No nested output.' })] }, `${iteration.id}-${index}`)) : _jsx("div", { className: "notice", children: "No iteration history available." }) }), result?.terminalReason && _jsxs("div", { className: "timing-details", children: [_jsx("strong", { children: "Terminal reason" }), _jsx("span", { children: result.terminalReason })] }), _jsx(TransportTiming, { timing: result?.transportTiming }), _jsx(BranchExecutionPath, { result: result }), _jsx(ParallelGroupDetails, { result: result }), _jsx(ResultSummary, { result: result })] }); }
|
|
@@ -263,7 +277,7 @@ function flattenDisplaySteps(steps, depth = 0, loopId, parentId, collapsed = new
|
|
|
263
277
|
const isLoop = step.control?.type === 'repeat';
|
|
264
278
|
const currentLoopId = isLoop ? step.id : loopId;
|
|
265
279
|
const branchMode = (step.control?.type === 'branch' || step.control?.type === 'parallel') && step.control.branches?.length;
|
|
266
|
-
const row = { id: step.id, name: step.name, depth, kind: step.kind ?? (step.children ? 'operation' : 'action'), loopTotal: isLoop ? Math.max(1, Math.min(step.control?.iterations ?? 1, 32)) : undefined, loopId: currentLoopId, parentId, hasChildren: Boolean(step.children?.length || branchMode) };
|
|
280
|
+
const row = { structuralAddress: step.structuralAddress, id: step.id, name: step.name, depth, kind: step.kind ?? (step.children ? 'operation' : 'action'), loopTotal: isLoop ? Math.max(1, Math.min(step.control?.iterations ?? 1, 32)) : undefined, loopId: currentLoopId, parentId, hasChildren: Boolean(step.children?.length || branchMode) };
|
|
267
281
|
if (branchMode && !collapsed.has(step.id)) {
|
|
268
282
|
const branchRows = step.control.branches.flatMap(branch => {
|
|
269
283
|
const branchId = `${step.id}.${branch.id}`;
|
|
@@ -275,21 +289,32 @@ function flattenDisplaySteps(steps, depth = 0, loopId, parentId, collapsed = new
|
|
|
275
289
|
return [row, ...(step.children && !collapsed.has(step.id) ? flattenDisplaySteps(step.children, depth + 1, currentLoopId, step.id, collapsed) : [])];
|
|
276
290
|
});
|
|
277
291
|
}
|
|
278
|
-
export function orderDisplayStepsByResult(steps,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
292
|
+
export function orderDisplayStepsByResult(steps, _results) { return steps; }
|
|
293
|
+
export const occurrenceKey = (address) => address ? JSON.stringify([address.scope, address.segments]) : '';
|
|
294
|
+
export function resultsForOccurrence(results, address) { return results.filter(r => address && occurrenceKey(r.structuralAddress) === occurrenceKey(address)); }
|
|
295
|
+
export function resultForOccurrence(results, address) { const matches = resultsForOccurrence(results, address); return matches.length === 1 ? matches[0] : undefined; }
|
|
296
|
+
export function stateForOccurrence(results, address) {
|
|
297
|
+
const matches = resultsForOccurrence(results, address);
|
|
298
|
+
if (!matches.length)
|
|
299
|
+
return undefined;
|
|
300
|
+
if (matches.some(r => r.state === 'failed'))
|
|
301
|
+
return 'failed';
|
|
302
|
+
if (matches.some(r => r.state === 'cancelled'))
|
|
303
|
+
return 'cancelled';
|
|
304
|
+
if (matches.every(r => r.state === 'skipped'))
|
|
305
|
+
return 'skipped';
|
|
306
|
+
if (matches.every(r => r.state === 'finished' || r.state === 'skipped'))
|
|
307
|
+
return 'finished';
|
|
308
|
+
if (matches.some(r => r.state === 'running'))
|
|
309
|
+
return 'running';
|
|
310
|
+
if (matches.some(r => r.state === 'waiting'))
|
|
311
|
+
return 'waiting';
|
|
312
|
+
return 'pending';
|
|
313
|
+
}
|
|
314
|
+
export function RecordedInvocations({ results }) {
|
|
315
|
+
if (results.length < 2)
|
|
316
|
+
return _jsx(IterationHistory, { result: results[0] });
|
|
317
|
+
return _jsxs("div", { className: "iteration-history", children: [_jsxs("p", { children: [results.length, " recorded invocations; no single value selected."] }), results.map((entry, index) => _jsxs("details", { children: [_jsxs("summary", { children: ["Invocation ", index + 1, " \u00B7 ", entry.state.toUpperCase(), " \u00B7 ", entry.durationMs, " ms"] }), _jsx("pre", { children: JSON.stringify(entry.iterations ?? [], null, 2) }), _jsx("strong", { children: "Resolved inputs" }), _jsx(KeyValues, { values: Object.entries(entry.inputs).flatMap(([name, value]) => [name, JSON.stringify(value)]) }), _jsx("strong", { children: "Outputs" }), _jsx(KeyValues, { values: Object.entries(entry.outputs).flatMap(([name, value]) => [name, JSON.stringify(value)]) }), _jsx(IterationHistory, { result: entry })] }, JSON.stringify([entry.structuralAddress, entry.iterations, index])))] });
|
|
293
318
|
}
|
|
294
319
|
function formatElapsed(milliseconds) { const seconds = Math.max(0, Math.floor(milliseconds / 1000)); return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`; }
|
|
295
320
|
function containerStepIds(steps) { return steps.flatMap(step => { const branchIds = step.control?.branches?.flatMap(branch => [`${step.id}.${branch.id}`, ...containerStepIds(branch.children)]) ?? []; return step.children?.length || branchIds.length ? [step.id, ...branchIds, ...(step.children ? containerStepIds(step.children) : [])] : []; }); }
|