// scenario-form.js — модальный редактор сценариев
// Использует: params-render.js (renderParamRow, renderMapFixedRow, collectParams)
// Зависит от: utils.js (_esc)
let scenarioEditorState = null;
// {defId, version, name, steps:[], services:[], cloudInstances:[], opsCache:{}, paramDefsCache:{}}
// step: {service_id, operation, output, instance_ref, instance_uid, params:{name:value}}
async function showScenarioEditor(def) {
scenarioEditorState = def ? {
defId: def.id, version: def.version, name: def.name,
steps: (def.steps || []).map(s => ({
service_id: s.service_id || '', operation: s.operation || '',
output: s.output || '', instance_ref: s.instance_ref || '',
instance_uid: s.instance_uid || '',
params: s.params || {} // {name: value} — символические имена
})),
services: [], cloudInstances: [], opsCache: {}, paramDefsCache: {}
} : { defId: null, version: 1, name: '', steps: [],
services: [], cloudInstances: [], opsCache: {}, paramDefsCache: {} };
try {
const [svcR, instR] = await Promise.all([
fetch('/api/services').then(r => r.json()),
fetch('/api/instances/list').then(r => r.json())
]);
scenarioEditorState.services = svcR || [];
scenarioEditorState.cloudInstances = instR || [];
} catch (e) { scenarioEditorState.services = []; scenarioEditorState.cloudInstances = []; }
renderEditor();
}
function openModal(html) {
let modal = document.getElementById('scenario-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'scenario-modal';
modal.innerHTML = '
';
document.body.appendChild(modal);
}
modal.style.display = 'block';
document.getElementById('modal-content').innerHTML = html;
}
function closeModal() {
const modal = document.getElementById('scenario-modal');
if (modal) modal.style.display = 'none';
loadScenarios();
}
// ── Снапшот параметров перед ре-рендером ──
function snapshotStepParams(idx) {
const st = scenarioEditorState;
const container = document.getElementById('step-' + idx + '-params');
if (!container || !container.innerHTML) return;
const numericParams = collectParams('#step-' + idx + '-params');
const defs = st.paramDefsCache[st.steps[idx]._svcOpId];
if (!defs) return;
// Обратный маппинг: numericId → name
const idToName = {};
defs.forEach(p => { idToName[p.svcOperationCfsParamId] = p.name; });
const result = {};
for (const [id, val] of Object.entries(numericParams)) {
const name = idToName[parseInt(id)] || id;
if (val !== '' && val !== '{}' && val !== '[]' && val !== 'false') {
result[name] = val;
}
}
st.steps[idx].params = result;
}
function snapshotAllParams() {
if (!scenarioEditorState) return;
scenarioEditorState.steps.forEach((_, idx) => snapshotStepParams(idx));
}
// ── Резолв операции + загрузка параметров ──
async function resolveStepSvcOpId(idx) {
const st = scenarioEditorState;
const step = st.steps[idx];
const op = step.operation;
if (!op) return null;
let svcId = step.service_id;
if (op !== 'create') {
// Определить svcId через выбранный инстанс
const refEl = document.getElementById('step-' + idx + '-ref');
const ref = (refEl?.value || '').trim();
if (ref) {
const cloud = (st.cloudInstances || []).find(i => i.instanceUid === ref);
if (cloud) { svcId = cloud.serviceId; }
else {
// output-ref: ищем в предыдущих create-шагах
for (let i = 0; i < idx; i++) {
if (st.steps[i].output === ref) { svcId = st.steps[i].service_id; break; }
}
}
}
}
if (!svcId) return null;
// Кеш операций
if (!st.opsCache[svcId]) {
try {
const r = await fetch('/api/operations/' + svcId);
const d = await r.json();
st.opsCache[svcId] = d.operations || [];
} catch (e) { return null; }
}
const ops = st.opsCache[svcId];
const found = ops.find(o => o.operation === op);
return found ? found.svcOperationId : null;
}
async function loadStepParams(idx) {
const st = scenarioEditorState;
const step = st.steps[idx];
if (!step.operation) return;
const svcOpId = await resolveStepSvcOpId(idx);
if (!svcOpId) return;
step._svcOpId = svcOpId;
// Кеш параметров
if (!st.paramDefsCache[svcOpId]) {
try {
const r = await fetch('/api/params/' + svcOpId);
const d = await r.json();
st.paramDefsCache[svcOpId] = d.params || d || [];
} catch (e) { return; }
}
const defs = st.paramDefsCache[svcOpId];
const savedParams = step.params || {};
// Подготовить allInst для refSvcId-параметров
let allInst = st.cloudInstances || [];
const needRefs = defs.some(p => p.refSvcId);
if (needRefs && !allInst.length) {
try { const r = await fetch('/api/instances/list'); allInst = await r.json(); } catch (e) {}
}
// Рендер: клон def с подстановкой сохранённых значений в defaultValue
const container = document.getElementById('step-' + idx + '-params');
if (!container) return;
let html = '';
defs.forEach(p => {
const clone = Object.assign({}, p);
const savedVal = savedParams[p.name];
if (savedVal !== undefined) clone.defaultValue = savedVal;
html += renderParamRow(clone, allInst);
});
container.innerHTML = html;
}
// ── Рендер ──
function renderEditor() {
snapshotAllParams(); // сохранить несохранённые правки перед перерисовкой
const st = scenarioEditorState;
let html = '';
html += '
' + (st.defId ? 'Редактирование' : 'Новый сценарий') + '
';
html += '
';
html += ``;
html += '
';
html += `
📖 Как заполнять
Шаг: выбери операцию → укажи цель.
Create: выбери сервис + задай output (имя, например d1).
Остальные: выбери инстанс — 🆕 из create-шагов или ☁ из облака.
Параметры: разверни «Параметры» — значения загружаются из API автоматически.
[↑][↓] — порядок, [✕] — удалить.
`;
html += '
Шаги
';
st.steps.forEach((s, idx) => { html += renderStepRow(idx, s); });
html += `
`;
html += '
';
html += ``;
html += ``;
html += '
';
openModal(html);
// Асинхронно загрузить параметры для шагов с выбранной операцией
st.steps.forEach((s, idx) => { if (s.operation) loadStepParams(idx); });
}
function renderStepRow(idx, step) {
const st = scenarioEditorState;
const op = step.operation;
const out = step.output || '';
const ref = step.instance_ref || '';
const prevOutputs = [];
for (let i = 0; i < idx; i++) {
const s = st.steps[i];
if (s.operation === 'create' && s.output) prevOutputs.push(s.output);
}
let html = ``;
html += `
`;
html += `Шаг ${idx+1}`;
if (idx > 0) html += ``;
if (idx < st.steps.length - 1) html += ``;
html += ``;
html += `
`;
const allOps = ['create', 'delete', 'modify', 'suspend', 'resume', 'redeploy'];
let opOpts = '
';
allOps.forEach(o => { opOpts += `
`; });
html += `
`;
if (op === 'create') {
let svcOpts = '
';
st.services.forEach(s => {
svcOpts += `
`;
});
html += `
`;
html += `
`;
} else if (op) {
let refOpts = '
';
prevOutputs.forEach(o => { refOpts += `
`; });
// Облачные autotest-* инстансы (только если раскрыты)
if (step._showCloud && st.cloudInstances) {
refOpts += '
';
st.cloudInstances.forEach(i => {
const dn = i.displayName || '';
if (!dn.startsWith('autotest-') || i.explainedStatus === 'deleted') return;
const sel = i.instanceUid == ref ? 'selected' : '';
refOpts += `
`;
});
}
const btnLabel = step._showCloud ? '➖ Скрыть облако' : '📥 Из облака';
html += `
`;
html += `
`;
}
// Параметры — свёрнутый
, заполняется асинхронно в loadStepParams
html += `Параметры
`;
html += ' ';
return html;
}
// ── Действия ──
function onOpChange(idx) {
scenarioEditorState.steps[idx].params = {};
scenarioEditorState.steps[idx]._svcOpId = null;
renderEditor();
}
function toggleCloudInstances(idx) {
snapshotAllParams();
scenarioEditorState.steps[idx]._showCloud = !scenarioEditorState.steps[idx]._showCloud;
renderEditor();
}
function addStep() {
snapshotAllParams();
scenarioEditorState.steps.push({ service_id: '', operation: '', output: '', instance_ref: '', instance_uid: '', params: {}, _showCloud: false });
renderEditor();
}
function removeStep(idx) {
snapshotAllParams();
scenarioEditorState.steps.splice(idx, 1);
renderEditor();
}
function moveStep(idx, dir) {
snapshotAllParams();
const steps = scenarioEditorState.steps;
const target = idx + dir;
if (target < 0 || target >= steps.length) return;
[steps[idx], steps[target]] = [steps[target], steps[idx]];
renderEditor();
}
function collectFormSteps() {
snapshotAllParams();
const st = scenarioEditorState;
const name = (document.getElementById('scenario-name')?.value || '').trim();
const steps = [];
st.steps.forEach((step, idx) => {
const opEl = document.getElementById('step-' + idx + '-op');
const operation = (opEl?.value || '').trim();
let svcId = 0;
const s = { operation, params: {} };
if (operation === 'create') {
const svcEl = document.getElementById('step-' + idx + '-svc');
svcId = parseInt(svcEl?.value) || 0;
const outEl = document.getElementById('step-' + idx + '-output');
const output = (outEl?.value || '').trim();
if (output) s.output = output;
} else {
const refEl = document.getElementById('step-' + idx + '-ref');
const ref = (refEl?.value || '').trim();
if (ref) {
const cloud = (st.cloudInstances || []).find(i => i.instanceUid === ref);
if (cloud) {
svcId = cloud.serviceId;
s.instance_uid = ref;
} else {
s.instance_ref = ref;
}
}
}
s.service_id = svcId;
// Собрать параметры через collectParams + обратный маппинг numeric→name
const defs = st.paramDefsCache[step._svcOpId];
if (defs) {
const numericParams = collectParams('#step-' + idx + '-params');
const idToName = {};
defs.forEach(p => { idToName[p.svcOperationCfsParamId] = p.name; });
for (const [id, val] of Object.entries(numericParams)) {
const pname = idToName[parseInt(id)] || id;
if (val !== '' && val !== '{}' && val !== '[]' && val !== 'false') {
s.params[pname] = val;
}
}
}
steps.push(s);
});
return { name, steps };
}
async function saveScenario() {
const { name, steps } = collectFormSteps();
if (!name) { alert('Введите название'); return; }
if (!steps.length) { alert('Добавьте хотя бы один шаг'); return; }
for (let i = 0; i < steps.length; i++) {
if (!steps[i].service_id) { alert('Шаг ' + (i+1) + ': выберите сервис или инстанс'); return; }
if (!steps[i].operation) { alert('Шаг ' + (i+1) + ': выберите операцию'); return; }
}
const st = scenarioEditorState;
const isNew = !st.defId;
const url = isNew ? '/api/scenario/definitions' : '/api/scenario/definitions/' + st.defId;
const method = isNew ? 'POST' : 'PUT';
const body = isNew ? { name, steps } : { name, steps, version: st.version };
try {
const r = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const d = await r.json();
if (r.status === 409) {
alert('⚠️ Конфликт версий — кто-то уже изменил сценарий. Обновите страницу.');
return;
}
if (d.error) { alert(d.error); return; }
if (isNew && d.id) st.defId = d.id;
if (d.version) st.version = d.version;
closeModal();
} catch (e) {
alert('Ошибка сохранения: ' + e.message);
}
}