diff --git a/site/app.py b/site/app.py index c92df55..6fbbeb2 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.4" +VERSION = "1.2.5" 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 a30a395..6772a36 100644 --- a/site/static/js/scenario-form.js +++ b/site/static/js/scenario-form.js @@ -1,9 +1,10 @@ // scenario-form.js — модальный редактор сценариев // Использует: params-render.js (renderParamRow, renderMapFixedRow, collectParams) -// Зависит от: utils.js (_esc), instances.js (currentSvcId) +// Зависит от: utils.js (_esc) -let scenarioEditorState = null; // {defId, version, name, steps, services:[], opsCache:{}} -// step: {service_id, operation, output, instance_ref, instance_uid, params: [["k","v"],...]} +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 ? { @@ -12,12 +13,12 @@ async function showScenarioEditor(def) { 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 || {}) + params: s.params || {} // {name: value} — символические имена })), - services: [], opsCache: {} - } : { defId: null, version: 1, name: '', steps: [], services: [], opsCache: {} }; + 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()), @@ -47,36 +48,142 @@ function closeModal() { 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 = '
d1). Другие шаги смогут ссылаться на него.durationMs = 5000d1).