// 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 += `
';
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);
}
}