// scenario-form.js — модальный редактор сценариев // Использует: params-render.js (renderParamRow, renderMapFixedRow, collectParams) // Зависит от: utils.js (_esc), instances.js (currentSvcId) let scenarioEditorState = null; // {defId, version, name, steps, services:[], opsCache:{}} // step: {service_id, operation, output, instance_ref, instance_uid, params: [["k","v"],...]} 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: Object.entries(s.params || {}) })), services: [], opsCache: {} } : { defId: null, version: 1, name: '', steps: [], services: [], opsCache: {} }; // Загрузить список сервисов и облачные инстансы 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 renderEditor() { const st = scenarioEditorState; let html = '
'; html += '

' + (st.defId ? 'Редактирование' : 'Новый сценарий') + '

'; // Название html += '

'; html += ``; html += '
'; // Шаги html += '
Шаги
'; st.steps.forEach((s, idx) => { html += renderStepRow(idx, s); }); html += ``; // Кнопки html += '
'; html += ``; html += ``; html += '
'; openModal(html); } function renderStepRow(idx, step) { const st = scenarioEditorState; const svcId = step.service_id; const op = step.operation; const out = step.output || ''; const ref = step.instance_ref || ''; // Сервисы — dropdown let svcOpts = ''; st.services.forEach(s => { svcOpts += ``; }); // Операции — dropdown (из кеша или базовый) let opOpts = ''; const ops = st.opsCache[svcId] || ['create', 'delete', 'modify', 'suspend', 'resume', 'redeploy']; ops.forEach(o => { const oName = typeof o === 'string' ? o : o.operation; opOpts += ``; }); // Output refs из предыдущих create-шагов 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 += `
`; // Сервис + Операция в одной строке html += `
`; html += ``; html += ``; html += `
`; // Output / instance_ref / instance_uid html += '
'; if (op === 'create') { html += ``; } else if (op) { // Предыдущие output'ы + облачные инстансы этого сервиса let refOpts = ''; // output'ы из предыдущих create-шагов prevOutputs.forEach(o => { refOpts += ``; }); // Облачные инстансы того же сервиса const cloudInsts = (st.cloudInstances || []).filter(i => i.serviceId == svcId && i.explainedStatus !== 'deleted'); if (cloudInsts.length) refOpts += ''; cloudInsts.forEach(i => { const sel = i.instanceUid == ref ? 'selected' : ''; refOpts += ``; }); html += ``; } html += '
'; // Параметры html += `
`; const params = step.params || []; params.forEach(([k, v], pi) => { html += `
`; html += ``; html += `=`; html += ``; html += ``; html += `
`; }); html += ``; html += '
'; return html; } async function onSvcChange(idx) { const svcEl = document.getElementById('step-' + idx + '-svc'); const svcId = parseInt(svcEl?.value) || 0; if (!svcId) return; // Загрузить операции для сервиса try { const r = await fetch('/api/operations/' + svcId); const d = await r.json(); scenarioEditorState.opsCache[svcId] = d.operations || []; } catch (e) { scenarioEditorState.opsCache[svcId] = ['create', 'delete', 'modify', 'suspend', 'resume', 'redeploy']; } renderEditor(); } async function onOpChange(idx) { // Ничего не делаем — параметры редактируются вручную } function addStep() { scenarioEditorState.steps.push({ service_id: '', operation: '', output: '', instance_ref: '', instance_uid: '', params: [] }); renderEditor(); } function removeStep(idx) { scenarioEditorState.steps.splice(idx, 1); renderEditor(); } function moveStep(idx, dir) { const steps = scenarioEditorState.steps; const target = idx + dir; if (target < 0 || target >= steps.length) return; [steps[idx], steps[target]] = [steps[target], steps[idx]]; } function addParam(idx) { scenarioEditorState.steps[idx].params.push(['', '']); } function removeParam(idx, pi) { scenarioEditorState.steps[idx].params.splice(pi, 1); } function collectFormSteps() { const st = scenarioEditorState; const name = (document.getElementById('scenario-name')?.value || '').trim(); const steps = []; st.steps.forEach((_, idx) => { const svcEl = document.getElementById('step-' + idx + '-svc'); const opEl = document.getElementById('step-' + idx + '-op'); const outEl = document.getElementById('step-' + idx + '-output'); const refEl = document.getElementById('step-' + idx + '-ref'); const uidEl = document.getElementById('step-' + idx + '-uid'); const svcId = parseInt(svcEl?.value) || 0; const operation = (opEl?.value || '').trim(); const output = (outEl?.value || '').trim(); const instance_ref = (refEl?.value || '').trim(); const instance_uid = (uidEl?.value || '').trim(); const params = {}; st.steps[idx].params.forEach((__, pi) => { const k = document.getElementById('step-' + idx + '-pk-' + pi)?.value?.trim(); const v = document.getElementById('step-' + idx + '-pv-' + pi)?.value || ''; if (k) params[k] = v; }); const s = { service_id: svcId, operation, params }; if (output) s.output = output; if (instance_ref) s.instance_ref = instance_ref; if (instance_uid) s.instance_uid = instance_uid; 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); } }