fix: adapt autotest to updated service APIs

This commit is contained in:
2026-10-25 16:30:25 +03:00
parent 8d956c8063
commit 4203e5674c
13 changed files with 462 additions and 292 deletions
+94 -45
View File
@@ -38,6 +38,8 @@ 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 || '',
_legacyInstanceFallback: s.operation !== 'create' && !s.instance_ref && !s.instance_uid,
_showCloud: Boolean(s.instance_uid),
params: s.params || {} // {name: value} — символьные имена
})),
services: [], cloudInstances: [], opsCache: {}, paramDefsCache: {}
@@ -58,6 +60,10 @@ async function showScenarioEditor(def) {
scenarioEditorState.services = [];
scenarioEditorState.cloudInstances = [];
}
const initialServiceIds = [...new Set(scenarioEditorState.steps
.map(step => parseInt(step.service_id, 10))
.filter(serviceId => serviceId > 0))];
await Promise.all(initialServiceIds.map(loadServiceOperations));
renderEditor();
}
@@ -138,6 +144,30 @@ function snapshotAllParams() {
// Резолв операции + загрузка параметров
// ═══════════════════════════════════════════════════════
/**
* Загрузить доступные операции сервиса и сохранить их в кеше редактора.
*
* @param {number|string} svcId — ID сервиса
* @returns {Promise<Array|null>} операции сервиса или null при ошибке загрузки
*/
async function loadServiceOperations(svcId) {
if (!svcId) return null;
const st = scenarioEditorState;
if (Object.prototype.hasOwnProperty.call(st.opsCache, svcId)) {
return st.opsCache[svcId];
}
try {
const response = await fetch('/api/operations/' + svcId);
const data = await response.json();
if (!response.ok || data.error) return null;
st.opsCache[svcId] = Array.isArray(data.operations) ? data.operations : [];
return st.opsCache[svcId];
} catch (e) {
return null;
}
}
/**
* Найти svcOperationId для операции в шаге.
*
@@ -179,15 +209,8 @@ async function resolveStepSvcOpId(idx) {
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 ops = await loadServiceOperations(svcId);
if (!ops) return null;
const found = ops.find(o => o.operation === op);
return found ? found.svcOperationId : null;
}
@@ -230,7 +253,7 @@ async function loadStepParams(idx) {
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) {}
try { const r = await fetch('/api/instances/list'); allInst = await r.json(); } catch (e) { }
}
// Рендер: клонируем def и подставляем сохранённые значения в defaultValue
@@ -320,20 +343,20 @@ function renderStepRow(idx, step) {
const st = scenarioEditorState;
const op = step.operation;
const out = step.output || '';
const ref = step.instance_ref || '';
const ref = step.instance_ref || step.instance_uid || '';
// Собрать доступные output-ы из предыдущих create-шагов (для instance_ref select)
const prevOutputs = [];
for (let i = 0; i < idx; i++) {
const s = st.steps[i];
if (s.operation === 'create' && s.output) prevOutputs.push(s.output);
if (s.operation === 'create' && s.output && Number(s.service_id) === Number(step.service_id)) prevOutputs.push(s.output);
}
let html = `<div style="border:1px solid var(--brand-gray);border-radius:6px;padding:8px;margin:6px 0;" id="step-${idx}">`;
// Заголовок шага: номер + кнопки [↑][↓][✕]
html += `<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">`;
html += `<span style="font-size:11px;font-weight:600;">Шаг ${idx+1}</span>`;
html += `<span style="font-size:11px;font-weight:600;">Шаг ${idx + 1}</span>`;
// Кнопка вверх (кроме первого)
if (idx > 0) html += `<button class="btn btn-sm" style="font-size:9px;padding:0 4px;" onclick="moveStep(${idx},-1);renderEditor();">↑</button>`;
// Кнопка вниз (кроме последнего)
@@ -342,20 +365,30 @@ function renderStepRow(idx, step) {
html += `<button class="btn btn-sm" style="font-size:9px;padding:0 4px;margin-left:auto;color:var(--destructive);" onclick="removeStep(${idx})">✕</button>`;
html += `</div>`;
// Выпадающий список операций
const allOps = ['create', 'delete', 'modify', 'suspend', 'resume', 'redeploy'];
// Список операций зависит от актуального каталога выбранного сервиса.
let svcOpts = '<option value="">— сервис —</option>';
st.services.forEach(s => {
svcOpts += `<option value="${s.svcId}" ${s.svcId == step.service_id ? 'selected' : ''}>${s.svcId}. ${_esc(s.svc)}</option>`;
});
html += `<div style="margin-bottom:4px;"><select id="step-${idx}-svc" onchange="onServiceChange(${idx})" style="padding:4px;font-size:12px;">${svcOpts}</select></div>`;
const svcId = parseInt(step.service_id, 10) || 0;
const serviceOps = svcId ? (st.opsCache[svcId] || []) : [];
const operationsLoaded = svcId && Object.prototype.hasOwnProperty.call(st.opsCache, svcId);
let opOpts = '<option value="">— операция —</option>';
allOps.forEach(o => { opOpts += `<option value="${o}" ${o == op ? 'selected' : ''}>${o}</option>`; });
html += `<div style="margin-bottom:4px;"><select id="step-${idx}-op" onchange="onOpChange(${idx})" style="padding:4px;font-size:12px;">${opOpts}</select></div>`;
serviceOps.filter(item => item.operation !== 'reconcile').forEach(item => {
const operationName = item.operation || '';
opOpts += `<option value="${_esc(operationName)}" ${operationName === op ? 'selected' : ''}>${_esc(operationName)}</option>`;
});
if (op && !serviceOps.some(item => item.operation === op)) {
opOpts += `<option value="${_esc(op)}" selected>${_esc(op)} (нет в сервисе)</option>`;
}
const opDisabled = !svcId || !operationsLoaded;
html += `<div style="margin-bottom:4px;"><select id="step-${idx}-op" onchange="onOpChange(${idx})" style="padding:4px;font-size:12px;" ${opDisabled ? 'disabled' : ''}>${opOpts}</select></div>`;
// Разное содержимое в зависимости от типа операции
if (op === 'create') {
// CREATE: выбор сервиса + output name
let svcOpts = '<option value="">— сервис —</option>';
st.services.forEach(s => {
svcOpts += `<option value="${s.svcId}" ${s.svcId == step.service_id ? 'selected' : ''}>${s.svcId}. ${_esc(s.svc)}</option>`;
});
html += `<div style="display:flex;gap:6px;align-items:center;"><select id="step-${idx}-svc" onchange="snapshotAllParams();scenarioEditorState.steps[${idx}].service_id=parseInt(this.value);renderEditor();" style="padding:4px;font-size:12px;">${svcOpts}</select></div>`;
// CREATE: output name
html += `<div style="margin-top:4px;"><input type="text" id="step-${idx}-output" value="${_esc(out)}" onchange="scenarioEditorState.steps[${idx}].output=this.value" placeholder="имя для ссылок (например: d1)" style="width:220px;padding:3px;font-size:11px;"></div>`;
} else if (op) {
@@ -371,13 +404,17 @@ function renderStepRow(idx, step) {
st.cloudInstances.forEach(i => {
const dn = i.displayName || '';
// Только autotest-*, не deleted
if (!dn.startsWith('autotest-') || i.explainedStatus === 'deleted') return;
if (!dn.startsWith('autotest-') || i.explainedStatus === 'deleted'
|| Number(i.serviceId) !== Number(step.service_id)) return;
const sel = i.instanceUid == ref ? 'selected' : '';
refOpts += `<option value="${i.instanceUid}" ${sel}>☁ ${_esc(dn)} — ${_esc(i.svc||'')} (${_esc(i.explainedStatus||'?')})</option>`;
refOpts += `<option value="${i.instanceUid}" ${sel}>☁ ${_esc(dn)} — ${_esc(i.svc || '')} (${_esc(i.explainedStatus || '?')})</option>`;
});
}
if (step.instance_uid && !st.cloudInstances.some(i => i.instanceUid === step.instance_uid)) {
refOpts += `<option value="${_esc(step.instance_uid)}" selected>☁ ${_esc(step.instance_uid)} (сохранённая ссылка)</option>`;
}
const btnLabel = step._showCloud ? '➖ Скрыть облако' : '📥 Из облака';
html += `<div style="display:flex;gap:4px;align-items:center;"><select id="step-${idx}-ref" onchange="snapshotAllParams();scenarioEditorState.steps[${idx}].instance_ref=this.value;renderEditor();" style="padding:4px;font-size:12px;max-width:500px;">${refOpts}</select>`;
html += `<div style="display:flex;gap:4px;align-items:center;"><select id="step-${idx}-ref" onchange="snapshotAllParams();scenarioEditorState.steps[${idx}].instance_ref=this.value;scenarioEditorState.steps[${idx}].instance_uid='';scenarioEditorState.steps[${idx}]._legacyInstanceFallback=false;renderEditor();" style="padding:4px;font-size:12px;max-width:500px;">${refOpts}</select>`;
html += `<button class="btn btn-sm" style="font-size:10px;padding:0 6px;" onclick="toggleCloudInstances(${idx})">${btnLabel}</button></div>`;
}
@@ -392,6 +429,24 @@ function renderStepRow(idx, step) {
// Действия редактора
// ═══════════════════════════════════════════════════════
/** Сменить сервис шага и загрузить его актуальный список операций. */
async function onServiceChange(idx) {
snapshotAllParams();
const step = scenarioEditorState.steps[idx];
const svcEl = document.getElementById('step-' + idx + '-svc');
const svcId = parseInt(svcEl?.value, 10) || '';
step.service_id = svcId;
step.operation = '';
step.instance_ref = '';
step.instance_uid = '';
step.params = {};
step._svcOpId = null;
step._legacyInstanceFallback = false;
if (svcId) await loadServiceOperations(svcId);
renderEditor();
}
/**
* Изменение операции в выпадающем списке → сохранить выбор + сброс + перерисовка.
*
@@ -407,6 +462,7 @@ function onOpChange(idx) {
step.operation = newOp; // сохранить выбранную операцию
step.params = {}; // сброс параметров (для новой операции — другие)
step._svcOpId = null; // сброс кеша svcOperationId
step._legacyInstanceFallback = false;
// Для не-create: автоматически показать облачные инстансы
if (newOp && newOp !== 'create') {
@@ -484,10 +540,11 @@ function collectFormSteps() {
let svcId = 0;
const s = { operation, params: {} };
const svcEl = document.getElementById('step-' + idx + '-svc');
svcId = parseInt(svcEl?.value) || 0;
if (operation === 'create') {
// CREATE: service_id из select
const svcEl = document.getElementById('step-' + idx + '-svc');
svcId = parseInt(svcEl?.value) || 0;
// CREATE: service_id из select + output-ссылка
const outEl = document.getElementById('step-' + idx + '-output');
const output = (outEl?.value || '').trim();
if (output) s.output = output;
@@ -498,17 +555,12 @@ function collectFormSteps() {
if (ref) {
const cloud = (st.cloudInstances || []).find(i => i.instanceUid === ref);
if (cloud) {
svcId = cloud.serviceId;
s.instance_uid = ref; // облачный UUID
} else if (ref === step.instance_uid) {
s.instance_uid = ref; // сохранить UID даже если инстанс уже не в списке
} else {
// output-ссылка — ищем service_id в предыдущих create-шагах
// output-ссылка на предыдущий create-шаг
s.instance_ref = ref;
for (let j = 0; j < idx; j++) {
if (st.steps[j].output === ref) {
svcId = st.steps[j].service_id;
break;
}
}
}
}
}
@@ -549,14 +601,11 @@ async function saveScenario() {
if (!steps.length) { alert('Добавьте хотя бы один шаг'); return; }
for (let i = 0; i < steps.length; i++) {
const s = steps[i];
if (!s.operation) { alert('Шаг ' + (i+1) + ': выберите операцию'); return; }
// CREATE: нужен service_id. Не-CREATE: service_id ИЛИ instance_ref/instance_uid
if (s.operation === 'create') {
if (!s.service_id) { alert('Шаг ' + (i+1) + ': выберите сервис'); return; }
} else {
if (!s.service_id && !s.instance_ref && !s.instance_uid) {
alert('Шаг ' + (i+1) + ': выберите инстанс (из create-шага или облака)'); return;
}
if (!s.operation) { alert('Шаг ' + (i + 1) + ': выберите операцию'); return; }
if (!s.service_id) { alert('Шаг ' + (i + 1) + ': выберите сервис'); return; }
if (s.operation !== 'create' && !s.instance_ref && !s.instance_uid
&& !scenarioEditorState.steps[i]._legacyInstanceFallback) {
alert('Шаг ' + (i + 1) + ': выберите инстанс (из create-шага или облака)'); return;
}
}