From 15f5ebe647a743d386c5b035f7aa3581d3157f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Fri, 31 Jul 2026 10:19:26 +0400 Subject: [PATCH] v1.2.2: modal scenario editor with dropdowns + output/ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scenario-form.js — full rewrite: - Full-screen modal (backdrop + content, 800px max-width) - Services dropdown (GET /api/services, cached) - Operations dropdown (GET /api/operations/{svcId}, cached) - output field for create steps - instance_ref dropdown (previous outputs) + explicit instance_uid - [↑][↓] reorder buttons - Auto-load operations on service change index.html — modal CSS (#scenario-modal, .modal-backdrop, .modal-content) --- site/app.py | 2 +- site/static/js/scenario-form.js | 198 ++++++++++++++++++++++++-------- site/templates/index.html | 3 + 3 files changed, 157 insertions(+), 46 deletions(-) diff --git a/site/app.py b/site/app.py index e60515a..c771804 100644 --- a/site/app.py +++ b/site/app.py @@ -19,7 +19,7 @@ from routes.api_scenario_run import bp_run as api_scenario_run_bp from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp # Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI. -VERSION = "1.2.1" +VERSION = "1.2.2" app = Flask(__name__, template_folder="templates", static_folder="static") app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc") diff --git a/site/static/js/scenario-form.js b/site/static/js/scenario-form.js index 704555d..0454b7f 100644 --- a/site/static/js/scenario-form.js +++ b/site/static/js/scenario-form.js @@ -1,56 +1,130 @@ -// scenario-form.js — общий редактор шагов сценария (create + edit) -let scenarioEditorState = null; // {defId, version, name, steps:[]} -// steps: [{service_id, operation, params: [["key","val"],...]}] +// scenario-form.js — модальный редактор сценариев +// Использует: params-render.js (renderParamRow, renderMapFixedRow, collectParams) +// Зависит от: utils.js (_esc), instances.js (currentSvcId) -function showScenarioEditor(def) { - const body = document.getElementById('scenario-body'); +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, + 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 || {}) - })) - } : { defId: null, version: 1, name: '', steps: [] }; + })), + services: [], opsCache: {} + } : { defId: null, version: 1, name: '', steps: [], services: [], opsCache: {} }; + + // Загрузить список сервисов + try { + const r = await fetch('/api/services'); + scenarioEditorState.services = await r.json(); + } catch (e) { scenarioEditorState.services = []; } 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 body = document.getElementById('scenario-body'); const st = scenarioEditorState; - let html = '
'; - html += '

'; - html += ``; + let html = '
'; + html += '

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

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

'; + html += ``; html += '
'; - html += '
Шаги:
'; - st.steps.forEach((s, idx) => { - html += renderStepRow(idx, s); - }); - html += ``; - html += '
'; - html += ` `; - html += ''; + // Шаги + html += '
Шаги
'; + st.steps.forEach((s, idx) => { html += renderStepRow(idx, s); }); + html += ``; + // Кнопки + html += '
'; + html += ``; + html += ``; html += '
'; - body.innerHTML = html; + openModal(html); } function renderStepRow(idx, step) { - const svcId = step.service_id || ''; - const op = step.operation || ''; - const params = step.params || []; - let html = `
`; - html += `
Шаг ${idx+1}`; - html += ` `; + 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 += `
`; - // Сервис — текстовое поле с подсказкой (ID или имя) - html += ` `; - // Операция - html += ` `; - html += '
'; + + // Сервис + Операция в одной строке + html += `
`; + html += ``; + html += ``; + html += `
`; + + // Output / instance_ref / instance_uid + html += '
'; + if (op === 'create') { + html += ``; + } else if (op) { + let refOpts = ''; + prevOutputs.forEach(o => { refOpts += ``; }); + html += ``; + html += ``; + } + html += '
'; + + // Параметры + html += `
`; + const params = step.params || []; params.forEach(([k, v], pi) => { html += `
`; - html += ``; + html += ``; html += `=`; - html += ``; + html += ``; html += ``; html += `
`; }); @@ -59,8 +133,27 @@ function renderStepRow(idx, step) { 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: 1, operation: 'create', params: [] }); + scenarioEditorState.steps.push({ service_id: '', operation: '', output: '', instance_ref: '', instance_uid: '', params: [] }); renderEditor(); } @@ -69,6 +162,13 @@ function removeStep(idx) { 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(['', '']); } @@ -77,24 +177,32 @@ function removeParam(idx, pi) { scenarioEditorState.steps[idx].params.splice(pi, 1); } -function onStepChange(idx) { /* placeholder for future auto-load */ } - 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 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 || ''; + const k = document.getElementById('step-' + idx + '-pk-' + pi)?.value?.trim(); + const v = document.getElementById('step-' + idx + '-pv-' + pi)?.value || ''; if (k) params[k] = v; }); - steps.push({ service_id: svcId, operation, params }); + 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 }; } @@ -104,8 +212,8 @@ async function saveScenario() { 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) + ': укажите service_id'); return; } - if (!steps[i].operation) { alert('Шаг ' + (i+1) + ': укажите operation'); return; } + if (!steps[i].service_id) { alert('Шаг ' + (i+1) + ': выберите сервис'); return; } + if (!steps[i].operation) { alert('Шаг ' + (i+1) + ': выберите операцию'); return; } } const st = scenarioEditorState; const isNew = !st.defId; @@ -122,7 +230,7 @@ async function saveScenario() { if (d.error) { alert(d.error); return; } if (isNew && d.id) st.defId = d.id; if (d.version) st.version = d.version; - await loadScenarios(); + closeModal(); } catch (e) { alert('Ошибка сохранения: ' + e.message); } diff --git a/site/templates/index.html b/site/templates/index.html index 4cc459f..32955ab 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -38,6 +38,9 @@ .param-row label.req { font-weight:600; } .param-row label.req-nodfl { font-weight:600; color:var(--destructive); } .param-row input,.param-row select { flex:1; height:28px; font-size:12px; border:1px solid var(--brand-gray); border-radius:4px; padding:0 6px; } + #scenario-modal { display:none; position:fixed; top:0; left:0; right:0; bottom:0; z-index:10000; } + .modal-backdrop { position:absolute; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); } + .modal-content { position:relative; max-width:800px; margin:20px auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3); }