fix: adapt autotest to updated service APIs
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user