Backend (5→7 files): - api_test.py: 622→479, terraform functions → operations/terraform.py (142) - api_scenario.py: 241→split into run (153) + defs (108) with _validate_steps() - db/scenario_defs.py: +client_id, stand, is_seed in list_definitions Frontend (1→10 files): - app.js: 554→16 (loader), split into: utils.js (45), instances.js (89), operations.js (249), history.js (35), scenario-list.js (90) - New CRUD: scenario-form.js (128), create (25), edit (12), delete (13) Max file size: JS 249, PY 479
129 lines
6.2 KiB
JavaScript
129 lines
6.2 KiB
JavaScript
// scenario-form.js — общий редактор шагов сценария (create + edit)
|
|
let scenarioEditorState = null; // {defId, version, name, steps:[]}
|
|
// steps: [{service_id, operation, params: [["key","val"],...]}]
|
|
|
|
function showScenarioEditor(def) {
|
|
const body = document.getElementById('scenario-body');
|
|
scenarioEditorState = def ? {
|
|
defId: def.id, version: def.version, name: def.name,
|
|
steps: (def.steps || []).map(s => ({
|
|
service_id: s.service_id, operation: s.operation,
|
|
params: Object.entries(s.params || {})
|
|
}))
|
|
} : { defId: null, version: 1, name: '', steps: [] };
|
|
renderEditor();
|
|
}
|
|
|
|
function renderEditor() {
|
|
const st = scenarioEditorState;
|
|
let html = '<div style="font-size:12px;">';
|
|
html += '<div style="margin-bottom:6px;"><label style="font-size:11px;">Название:</label><br>';
|
|
html += `<input type="text" id="scenario-name" value="${_esc(st.name)}" style="width:100%;padding:4px;" placeholder="my_test">`;
|
|
html += '</div>';
|
|
html += '<div style="margin-bottom:6px;font-size:11px;font-weight:600;">Шаги:</div>';
|
|
st.steps.forEach((s, idx) => {
|
|
html += renderStepRow(idx, s);
|
|
});
|
|
html += `<button class="btn btn-sm" style="font-size:11px;margin-top:4px;" onclick="addStep()">+ Добавить шаг</button>`;
|
|
html += '<div style="margin-top:8px;">';
|
|
html += `<button class="btn btn-sm" style="font-size:11px;background:var(--brand-primary);color:#fff;" onclick="saveScenario()">Сохранить</button> `;
|
|
html += '<button class="btn btn-sm" style="font-size:11px;" onclick="loadScenarios()">Отмена</button>';
|
|
html += '</div></div>';
|
|
body.innerHTML = html;
|
|
}
|
|
|
|
function renderStepRow(idx, step) {
|
|
const svcId = step.service_id || '';
|
|
const op = step.operation || '';
|
|
const params = step.params || [];
|
|
let html = `<div style="border:1px solid var(--brand-gray);border-radius:4px;padding:6px;margin:4px 0;" id="step-${idx}">`;
|
|
html += `<div style="font-size:10px;color:var(--muted);margin-bottom:4px;">Шаг ${idx+1}`;
|
|
html += ` <button class="btn btn-sm" style="font-size:9px;padding:0 4px;" onclick="removeStep(${idx})">✕</button>`;
|
|
html += `</div>`;
|
|
// Сервис — текстовое поле с подсказкой (ID или имя)
|
|
html += `<input type="number" id="step-${idx}-svc" value="${svcId}" placeholder="service_id" style="width:80px;padding:2px;font-size:11px;" onchange="onStepChange(${idx})"> `;
|
|
// Операция
|
|
html += `<input type="text" id="step-${idx}-op" value="${_esc(op)}" placeholder="create" style="width:100px;padding:2px;font-size:11px;"> `;
|
|
html += '<div style="margin-top:2px;">';
|
|
params.forEach(([k, v], pi) => {
|
|
html += `<div style="display:flex;gap:4px;align-items:center;margin:2px 0;">`;
|
|
html += `<input type="text" id="step-${idx}-pk-${pi}" value="${_esc(k)}" placeholder="param" style="width:100px;padding:2px;font-size:10px;">`;
|
|
html += `<span>=</span>`;
|
|
html += `<input type="text" id="step-${idx}-pv-${pi}" value="${_esc(v)}" placeholder="value" style="width:120px;padding:2px;font-size:10px;">`;
|
|
html += `<button class="btn btn-sm" style="font-size:9px;padding:0 3px;" onclick="removeParam(${idx},${pi});renderEditor();">✕</button>`;
|
|
html += `</div>`;
|
|
});
|
|
html += `<button class="btn btn-sm" style="font-size:9px;margin-top:2px;" onclick="addParam(${idx});renderEditor();">+ Параметр</button>`;
|
|
html += '</div></div>';
|
|
return html;
|
|
}
|
|
|
|
function addStep() {
|
|
scenarioEditorState.steps.push({ service_id: 1, operation: 'create', params: [] });
|
|
renderEditor();
|
|
}
|
|
|
|
function removeStep(idx) {
|
|
scenarioEditorState.steps.splice(idx, 1);
|
|
renderEditor();
|
|
}
|
|
|
|
function addParam(idx) {
|
|
scenarioEditorState.steps[idx].params.push(['', '']);
|
|
}
|
|
|
|
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 svcId = parseInt(svcEl?.value) || 0;
|
|
const operation = (opEl?.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;
|
|
});
|
|
steps.push({ service_id: svcId, operation, params });
|
|
});
|
|
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) + ': укажите service_id'); return; }
|
|
if (!steps[i].operation) { alert('Шаг ' + (i+1) + ': укажите operation'); 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;
|
|
await loadScenarios();
|
|
} catch (e) {
|
|
alert('Ошибка сохранения: ' + e.message);
|
|
}
|
|
}
|