feat(scenarios): overhaul export/import with multi-entity YAML format

- export now accepts includeEnvironment and credentialIds query params
- output is a YAML stream with comment-delimited environment, credential, and scenario sections
- scenario entity includes credentials array with alias mappings
- import accepts both old single-object and new array format, upserts envs/creds before linking
- new ExportScenarioModal with env and per-credential checkboxes replaces direct download
This commit is contained in:
2026-04-15 00:35:14 +03:00
parent a779ab4667
commit f268d0e3eb
10 changed files with 489 additions and 29 deletions
@@ -41,6 +41,7 @@ import {
useToast,
type TableColumn,
} from '../../ui';
import { ExportScenarioModal } from '../../components/modals';
import styles from '../Page.module.css';
export function ScenarioDetailPage() {
@@ -74,6 +75,12 @@ export function ScenarioDetailPage() {
>(null);
const [deleting, setDeleting] = useState(false);
// Export modal state
const [exportModalOpen, setExportModalOpen] = useState(false);
const [includeEnvironment, setIncludeEnvironment] = useState(true);
const [includedCredentialIds, setIncludedCredentialIds] = useState<Set<string>>(new Set());
const [isExporting, setIsExporting] = useState(false);
useEffect(() => {
if (!id) return;
void scenarios
@@ -137,17 +144,36 @@ export function ScenarioDetailPage() {
navigate('/scenarios');
};
const handleOpenExportModal = () => {
if (!scenario) return;
// Initialise all credential checkboxes to checked
setIncludedCredentialIds(new Set(scenarioCreds.map((sc) => sc.credentialId)));
setIncludeEnvironment(true);
setExportModalOpen(true);
};
const handleExport = async () => {
if (!scenario) return;
const data = await scenarios.exportScenario(scenario.id);
const yamlContent = typeof data === 'string' ? data : yamlStringify(data);
const blob = new Blob([yamlContent], { type: 'application/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${scenario.name}.yaml`;
a.click();
URL.revokeObjectURL(url);
setIsExporting(true);
try {
const credIds = Array.from(includedCredentialIds);
const yamlContent = await scenarios.exportScenario(scenario.id, {
includeEnvironment,
credentialIds: credIds,
});
const blob = new Blob([yamlContent], { type: 'application/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${scenario.name}.yaml`;
a.click();
URL.revokeObjectURL(url);
setExportModalOpen(false);
} catch (err) {
toast.error((err as Error).message);
} finally {
setIsExporting(false);
}
};
const handleDeleteStep = async (stepId: string) => {
@@ -368,7 +394,7 @@ export function ScenarioDetailPage() {
<History size={14} />
{t('scenarios.action_runs')}
</Button>
<Button variant="secondary" size="sm" onClick={handleExport}>
<Button variant="secondary" size="sm" onClick={handleOpenExportModal}>
<Upload size={14} />
{t('scenarios.action_export')}
</Button>
@@ -628,6 +654,29 @@ export function ScenarioDetailPage() {
</>
)}
</Modal>
<ExportScenarioModal
open={exportModalOpen}
scenario={scenario}
scenarioCredentials={scenarioCreds}
environment={
scenario?.environment ?? envs.find((e) => e.id === scenario?.environmentId) ?? null
}
includeEnvironment={includeEnvironment}
onIncludeEnvironmentChange={setIncludeEnvironment}
includedCredentialIds={includedCredentialIds}
onCredentialToggle={(credId, checked) => {
setIncludedCredentialIds((prev) => {
const next = new Set(prev);
if (checked) next.add(credId);
else next.delete(credId);
return next;
});
}}
onClose={() => setExportModalOpen(false)}
onExport={handleExport}
isExporting={isExporting}
/>
</div>
);
}