fix: adapt autotest to updated service APIs
This commit is contained in:
+71
-70
@@ -16,12 +16,12 @@
|
||||
// busy — флаг блокировки UI (пока идёт операция)
|
||||
// AUTOTEST_PREFIX — "autotest-" (фильтр "наших" инстансов)
|
||||
|
||||
let svcInstances=[], selectedInst=null, selectedOp=null;
|
||||
let currentSvcId=(window.APP&&window.APP.firstServiceId)||1;
|
||||
let currentSvcName='';
|
||||
let currentSvcShort='';
|
||||
let busy=false;
|
||||
const AUTOTEST_PREFIX='autotest-';
|
||||
let svcInstances = [], selectedInst = null, selectedOp = null;
|
||||
let currentSvcId = (window.APP && window.APP.firstServiceId) || 1;
|
||||
let currentSvcName = '';
|
||||
let currentSvcShort = '';
|
||||
let busy = false;
|
||||
const AUTOTEST_PREFIX = 'autotest-';
|
||||
|
||||
/**
|
||||
* Выбрать сервис — загрузить его инстансы и показать в списке.
|
||||
@@ -29,37 +29,37 @@ const AUTOTEST_PREFIX='autotest-';
|
||||
*
|
||||
* @param {number} svcId — ID сервиса
|
||||
*/
|
||||
async function selectService(svcId){
|
||||
if(busy) return; // блокировка — идёт операция
|
||||
async function selectService(svcId) {
|
||||
if (busy) return; // блокировка — идёт операция
|
||||
|
||||
// Сброс предыдущего состояния
|
||||
currentSvcId=svcId;
|
||||
selectedInst=null;
|
||||
selectedOp=null;
|
||||
currentSvcId = svcId;
|
||||
selectedInst = null;
|
||||
selectedOp = null;
|
||||
stopPoll(); // остановить поллинг предыдущей операции
|
||||
|
||||
// Скрыть панель параметров и этапы
|
||||
document.getElementById('params-area').style.display='none';
|
||||
document.querySelectorAll('.stages-box').forEach(e=>e.remove());
|
||||
document.getElementById('params-area').style.display = 'none';
|
||||
document.querySelectorAll('.stages-box').forEach(e => e.remove());
|
||||
|
||||
// Подсветка активного сервиса в списке
|
||||
document.querySelectorAll('.svc-item').forEach(e=>e.classList.remove('active'));
|
||||
const svcEl=document.querySelector(`.svc-item[onclick*="${svcId}"]`);
|
||||
if(svcEl) svcEl.classList.add('active');
|
||||
document.querySelectorAll('.svc-item').forEach(e => e.classList.remove('active'));
|
||||
const svcEl = document.querySelector(`.svc-item[onclick*="${svcId}"]`);
|
||||
if (svcEl) svcEl.classList.add('active');
|
||||
|
||||
try{
|
||||
try {
|
||||
// GET /api/operations/{svcId} — возвращает операции + autotest-инстансы
|
||||
const r=await fetch('/api/operations/'+svcId);
|
||||
const d=await r.json();
|
||||
svcInstances=d.instances||[];
|
||||
currentSvcName=d.svc||'';
|
||||
currentSvcShort=d.svcShort||'';
|
||||
const r = await fetch('/api/operations/' + svcId);
|
||||
const d = await r.json();
|
||||
svcInstances = d.instances || [];
|
||||
currentSvcName = d.svc || '';
|
||||
currentSvcShort = d.svcShort || '';
|
||||
// Обновить текст кнопки «Создать»
|
||||
const btnSpan=document.querySelector('#create-btn-area span');
|
||||
if(btnSpan) btnSpan.textContent='+ Создать новый инстанс '+(currentSvcName||'сервиса');
|
||||
const btnSpan = document.querySelector('#create-btn-area span');
|
||||
if (btnSpan) btnSpan.textContent = '+ Создать новый инстанс ' + (currentSvcName || 'сервиса');
|
||||
renderInstances(); // отрисовать список
|
||||
}catch(e){
|
||||
document.getElementById('inst-list').innerHTML=
|
||||
} catch (e) {
|
||||
document.getElementById('inst-list').innerHTML =
|
||||
'<span style="color:var(--destructive);font-size:11px;">Ошибка загрузки</span>';
|
||||
}
|
||||
}
|
||||
@@ -68,37 +68,38 @@ async function selectService(svcId){
|
||||
* Отрисовать список инстансов в #inst-list.
|
||||
* Каждый инстанс — строка: имя + сервис + статус + created + modified.
|
||||
*/
|
||||
function renderInstances(){
|
||||
let html='';
|
||||
svcInstances.forEach(i=>{
|
||||
function renderInstances() {
|
||||
let html = '';
|
||||
svcInstances.forEach(i => {
|
||||
// Статус: из нашего поля status (cloud-first) или explainedStatus
|
||||
const status=i.status||i.explainedStatus||'?';
|
||||
const sc=status==='running'?'badge-success':'';
|
||||
const status = i.status || i.explainedStatus || '?';
|
||||
const sc = (status === 'running' || status === 'Выполнен') ? 'badge-success' : '';
|
||||
const created = i.instanceConfigDtCreated || i.dtCreate || '';
|
||||
// Строка инстанса — кликабельна
|
||||
html+=`<div class="inst-item" onclick="toggleInstance('${i.instanceUid}')" data-iuid="${i.instanceUid}">
|
||||
html += `<div class="inst-item" onclick="toggleInstance('${i.instanceUid}')" data-iuid="${i.instanceUid}">
|
||||
<span style="display:inline-block;width:220px;">${_esc(i.displayName)}</span>
|
||||
<span style="display:inline-block;width:140px;color:var(--muted);font-size:11px;">${_esc(i.svc||'')}</span>
|
||||
<span style="display:inline-block;width:140px;color:var(--muted);font-size:11px;">${_esc(i.svc || '')}</span>
|
||||
<span class="badge ${sc}" style="font-size:9px;">${_esc(status)}</span>
|
||||
<span style="display:inline-block;width:60px;color:var(--muted);font-size:9px;text-align:right;" title="${i.instanceConfigDtCreated||''}">${relativeTime(i.instanceConfigDtCreated)}</span>
|
||||
<span style="display:inline-block;width:60px;color:var(--muted);font-size:9px;text-align:right;" title="${i.instanceConfigDtModified||''}">${relativeTime(i.instanceConfigDtModified)}</span>
|
||||
<span style="display:inline-block;width:60px;color:var(--muted);font-size:9px;text-align:right;" title="${created}">${relativeTime(created)}</span>
|
||||
<span style="display:inline-block;width:60px;color:var(--muted);font-size:9px;text-align:right;" title="${i.instanceConfigDtModified || ''}">${relativeTime(i.instanceConfigDtModified)}</span>
|
||||
</div>`;
|
||||
// Контейнер для кнопок операций (изначально скрыт)
|
||||
html+=`<div class="inst-ops" id="ops-${i.instanceUid}"></div>`;
|
||||
html += `<div class="inst-ops" id="ops-${i.instanceUid}"></div>`;
|
||||
});
|
||||
document.getElementById('inst-list').innerHTML=html;
|
||||
document.getElementById('inst-list').innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить список инстансов (без смены сервиса).
|
||||
* Вызывается после завершения операции.
|
||||
*/
|
||||
async function refreshInstances(){
|
||||
try{
|
||||
const r=await fetch('/api/operations/'+currentSvcId);
|
||||
const d=await r.json();
|
||||
svcInstances=d.instances||[];
|
||||
async function refreshInstances() {
|
||||
try {
|
||||
const r = await fetch('/api/operations/' + currentSvcId);
|
||||
const d = await r.json();
|
||||
svcInstances = d.instances || [];
|
||||
renderInstances();
|
||||
}catch(e){}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,52 +108,52 @@ async function refreshInstances(){
|
||||
*
|
||||
* @param {string} iuid — instanceUid
|
||||
*/
|
||||
async function toggleInstance(iuid){
|
||||
if(busy) return;
|
||||
selectedInst=iuid;
|
||||
async function toggleInstance(iuid) {
|
||||
if (busy) return;
|
||||
selectedInst = iuid;
|
||||
stopPoll();
|
||||
|
||||
// Подсветка выбранного инстанса
|
||||
document.querySelectorAll('[data-iuid]').forEach(e=>
|
||||
e.classList.toggle('active',e.dataset.iuid===iuid));
|
||||
document.querySelectorAll('[data-iuid]').forEach(e =>
|
||||
e.classList.toggle('active', e.dataset.iuid === iuid));
|
||||
|
||||
// Скрыть панель параметров (если была открыта для другого инстанса)
|
||||
document.getElementById('params-area').style.display='none';
|
||||
document.getElementById('params-area').style.display = 'none';
|
||||
|
||||
const opsEl=document.getElementById('ops-'+iuid);
|
||||
const opsEl = document.getElementById('ops-' + iuid);
|
||||
// Если уже открыт — закрыть (toggle)
|
||||
if(opsEl.classList.contains('open')){opsEl.classList.remove('open');return;}
|
||||
if (opsEl.classList.contains('open')) { opsEl.classList.remove('open'); return; }
|
||||
|
||||
// Закрыть ВСЕ другие открытые панели операций
|
||||
document.querySelectorAll('.inst-ops').forEach(e=>e.classList.remove('open'));
|
||||
document.querySelectorAll('.inst-ops').forEach(e => e.classList.remove('open'));
|
||||
|
||||
// Найти инстанс в списке (может быть из другого сервиса)
|
||||
const inst=svcInstances.find(x=>x.instanceUid===iuid);
|
||||
const svcId=inst?inst.serviceId:currentSvcId;
|
||||
const inst = svcInstances.find(x => x.instanceUid === iuid);
|
||||
const svcId = inst ? inst.serviceId : currentSvcId;
|
||||
|
||||
try{
|
||||
const r=await fetch('/api/operations/'+svcId);
|
||||
const d=await r.json();
|
||||
try {
|
||||
const r = await fetch('/api/operations/' + svcId);
|
||||
const d = await r.json();
|
||||
|
||||
// Фильтруем операции: убираем reconcile (техническая) и create (уже создан)
|
||||
let ops=(d.operations||[]).filter(o=>o.operation!=='reconcile'&&o.operation!=='create');
|
||||
let ops = (d.operations || []).filter(o => o.operation !== 'reconcile' && o.operation !== 'create');
|
||||
|
||||
// Для not created инстансов — только delete
|
||||
if(inst&&(inst.status||inst.explainedStatus)==='not created'){
|
||||
ops=ops.filter(o=>o.operation==='delete');
|
||||
if (inst && (inst.status || inst.explainedStatus) === 'not created') {
|
||||
ops = ops.filter(o => o.operation === 'delete');
|
||||
}
|
||||
|
||||
// Кнопки операций с цветовой маркировкой
|
||||
// o.operation в onclick — JS-escape: \ → \\, ' → \'
|
||||
// o.operation в тексте кнопки — HTML-escape через _esc()
|
||||
opsEl.innerHTML=ops.map(o=>{
|
||||
opsEl.innerHTML = ops.map(o => {
|
||||
const escOp = _esc(o.operation);
|
||||
const jsOp = o.operation.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||
const jsOp = o.operation.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
return `<button class="btn btn-sm op-btn ${opClass(o.operation)}" onclick="runOp('${jsOp}',${o.svcOperationId})">${escOp}</button>`;
|
||||
}).join('');
|
||||
opsEl.classList.add('open');
|
||||
}catch(e){
|
||||
opsEl.innerHTML='<span style="color:var(--destructive);font-size:11px;">Ошибка загрузки</span>';
|
||||
} catch (e) {
|
||||
opsEl.innerHTML = '<span style="color:var(--destructive);font-size:11px;">Ошибка загрузки</span>';
|
||||
opsEl.classList.add('open');
|
||||
}
|
||||
}
|
||||
@@ -163,12 +164,12 @@ async function toggleInstance(iuid){
|
||||
* @param {string} opName — "modify"/"delete"/"suspend"/...
|
||||
* @returns {string} CSS-класс
|
||||
*/
|
||||
function opClass(opName){
|
||||
if(opName==='modify') return 'op-btn-modify'; // синий
|
||||
if(opName==='suspend') return 'op-btn-suspend'; // оранжевый
|
||||
if(opName==='resume') return 'op-btn-resume'; // зелёный
|
||||
if(opName==='delete') return 'op-btn-delete'; // красный
|
||||
if(opName==='redeploy') return 'op-btn-redeploy'; // фиолетовый
|
||||
function opClass(opName) {
|
||||
if (opName === 'modify') return 'op-btn-modify'; // синий
|
||||
if (opName === 'suspend') return 'op-btn-suspend'; // оранжевый
|
||||
if (opName === 'resume') return 'op-btn-resume'; // зелёный
|
||||
if (opName === 'delete') return 'op-btn-delete'; // красный
|
||||
if (opName === 'redeploy') return 'op-btn-redeploy'; // фиолетовый
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
+159
-153
@@ -12,7 +12,7 @@
|
||||
// Глобальные переменные (используются из instances.js):
|
||||
// pollTimer — ID setInterval для поллинга
|
||||
|
||||
let pollTimer=null;
|
||||
let pollTimer = null;
|
||||
|
||||
/**
|
||||
* Сгенерировать короткое имя для нового инстанса.
|
||||
@@ -21,9 +21,9 @@ let pollTimer=null;
|
||||
*
|
||||
* @returns {string} displayName suffix (без префикса autotest-)
|
||||
*/
|
||||
function makeCreateDisplayName(){
|
||||
function makeCreateDisplayName() {
|
||||
const rand = (Math.random() * 46656 | 0).toString(36).padStart(3, '0');
|
||||
return currentSvcShort ? currentSvcShort+'-'+rand : rand;
|
||||
return currentSvcShort ? currentSvcShort + '-' + rand : rand;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,39 +31,45 @@ function makeCreateDisplayName(){
|
||||
*
|
||||
* @returns {string} displayName или instanceUid (если не найден)
|
||||
*/
|
||||
function findInstName(){
|
||||
const i=svcInstances.find(x=>x.instanceUid===selectedInst);
|
||||
return i?i.displayName:selectedInst;
|
||||
function findInstName() {
|
||||
const i = svcInstances.find(x => x.instanceUid === selectedInst);
|
||||
return i ? i.displayName : selectedInst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановить поллинг текущей операции.
|
||||
*/
|
||||
function stopPoll(){
|
||||
if(pollTimer){clearInterval(pollTimer);pollTimer=null;}
|
||||
function stopPoll() {
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Начать создание нового инстанса (кнопка «+ Создать»).
|
||||
*
|
||||
* Загружает операцию create для текущего сервиса через GET /api/operations/{svcId}.
|
||||
* Если операция create не найдена — fallback на svcOperationId=18 (стандартный).
|
||||
* Числовой ID не зашивается в UI: он берётся из каталога выбранного сервиса.
|
||||
*/
|
||||
async function startCreate(){
|
||||
if(busy) return;
|
||||
selectedInst=null; // create — нет выбранного инстанса
|
||||
async function startCreate() {
|
||||
if (busy) return;
|
||||
selectedInst = null; // create — нет выбранного инстанса
|
||||
stopPoll();
|
||||
// Снять выделение со всех инстансов
|
||||
document.querySelectorAll('[data-iuid]').forEach(e=>e.classList.remove('active'));
|
||||
document.querySelectorAll('.inst-ops').forEach(e=>e.classList.remove('open'));
|
||||
try{
|
||||
const r=await fetch('/api/operations/'+currentSvcId);
|
||||
const d=await r.json();
|
||||
const createOp=d.operations.find(o=>o.operation==='create');
|
||||
const opId=createOp?createOp.svcOperationId:18; // fallback
|
||||
showParams(opId,'create');
|
||||
}catch(e){
|
||||
showParams(18,'create'); // fallback при ошибке
|
||||
document.querySelectorAll('[data-iuid]').forEach(e => e.classList.remove('active'));
|
||||
document.querySelectorAll('.inst-ops').forEach(e => e.classList.remove('open'));
|
||||
try {
|
||||
const r = await fetch('/api/operations/' + currentSvcId);
|
||||
const d = await r.json();
|
||||
if (!r.ok || d.error) throw new Error(d.error || 'Не удалось загрузить операции сервиса');
|
||||
const createOp = (Array.isArray(d.operations) ? d.operations : []).find(o => o.operation === 'create');
|
||||
if (!createOp) {
|
||||
document.getElementById('test-status').innerHTML =
|
||||
'<span style="color:var(--destructive);">У выбранного сервиса нет операции create</span>';
|
||||
return;
|
||||
}
|
||||
showParams(createOp.svcOperationId, 'create');
|
||||
} catch (e) {
|
||||
document.getElementById('test-status').innerHTML =
|
||||
`<span style="color:var(--destructive);">Ошибка загрузки операций: ${_esc(e.message || '')}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +79,10 @@ async function startCreate(){
|
||||
* @param {string} opName — "modify"/"delete"/"suspend"/"resume"/"redeploy"
|
||||
* @param {number} opId — svcOperationId
|
||||
*/
|
||||
function runOp(opName,opId){
|
||||
if(busy) return;
|
||||
function runOp(opName, opId) {
|
||||
if (busy) return;
|
||||
stopPoll();
|
||||
showParams(opId,opName);
|
||||
showParams(opId, opName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,81 +98,81 @@ function runOp(opName,opId){
|
||||
* @param {number} opId — svcOperationId
|
||||
* @param {string} opName — "create"/"modify"/...
|
||||
*/
|
||||
function showParams(opId,opName){
|
||||
function showParams(opId, opName) {
|
||||
// Сохраняем выбранную операцию
|
||||
selectedOp={opId,opName,svcId:currentSvcId};
|
||||
selectedOp = { opId, opName, svcId: currentSvcId };
|
||||
|
||||
// Показать панель параметров, скрыть старые этапы
|
||||
document.getElementById('params-area').style.display='block';
|
||||
document.querySelectorAll('.stages-box').forEach(e=>e.remove());
|
||||
document.getElementById('test-status').textContent='';
|
||||
document.getElementById('params-area').style.display = 'block';
|
||||
document.querySelectorAll('.stages-box').forEach(e => e.remove());
|
||||
document.getElementById('test-status').textContent = '';
|
||||
|
||||
const isCreate=opName==='create';
|
||||
const isCreate = opName === 'create';
|
||||
// Для create — шаблонные значения, для не-create — с instanceUid
|
||||
const qs=isCreate?'' : `?instanceUid=${selectedInst}`;
|
||||
const qs = isCreate ? '' : `?instanceUid=${selectedInst}`;
|
||||
|
||||
fetch('/api/params/'+opId+qs).then(r=>r.json()).then(async data=>{
|
||||
if(data.error){
|
||||
document.getElementById('params-form').innerHTML=
|
||||
fetch('/api/params/' + opId + qs).then(r => r.json()).then(async data => {
|
||||
if (data.error) {
|
||||
document.getElementById('params-form').innerHTML =
|
||||
`<span style="color:var(--destructive);">Ошибка: ${_esc(data.error)}</span>`;
|
||||
return;
|
||||
}
|
||||
const params=data.params||data;
|
||||
if(!Array.isArray(params)){
|
||||
document.getElementById('params-form').innerHTML=
|
||||
const params = data.params || data;
|
||||
if (!Array.isArray(params)) {
|
||||
document.getElementById('params-form').innerHTML =
|
||||
`<span style="color:var(--destructive);">Неверный формат параметров</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Если есть refSvcId-параметры — загружаем ВСЕ инстансы (для select'ов)
|
||||
let allInst=[];
|
||||
const needRefs=params.some(p=>p.refSvcId);
|
||||
if(needRefs){
|
||||
try{
|
||||
const r=await fetch('/api/instances/list');
|
||||
const list=await r.json();
|
||||
allInst=list||[];
|
||||
}catch(e){}
|
||||
let allInst = [];
|
||||
const needRefs = params.some(p => p.refSvcId);
|
||||
if (needRefs) {
|
||||
try {
|
||||
const r = await fetch('/api/instances/list');
|
||||
const list = await r.json();
|
||||
allInst = list || [];
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
const form=document.getElementById('params-form');
|
||||
const displayNameValue=isCreate?makeCreateDisplayName():'';
|
||||
const form = document.getElementById('params-form');
|
||||
const displayNameValue = isCreate ? makeCreateDisplayName() : '';
|
||||
|
||||
// Заголовок формы
|
||||
const createHeader=isCreate
|
||||
const createHeader = isCreate
|
||||
? `<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--brand-primary);">Создание: ${_esc(currentSvcName)}</div>`
|
||||
: '';
|
||||
const opHeader=!isCreate
|
||||
const opHeader = !isCreate
|
||||
? `<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--brand-primary);">${_esc(opName)} → ${_esc(findInstName())}</div>`
|
||||
: '';
|
||||
|
||||
// Форма: заголовок + displayName (только create) + параметры
|
||||
form.innerHTML=createHeader+opHeader
|
||||
+ (opName==='create'
|
||||
form.innerHTML = createHeader + opHeader
|
||||
+ (opName === 'create'
|
||||
? `<div class="param-row"><label class="req">displayName</label><span style="font-size:12px;color:var(--muted);white-space:nowrap;">${AUTOTEST_PREFIX}</span><input type="text" id="param-displayname" value="${_esc(displayNameValue)}" autocomplete="off"></div>`
|
||||
: '')
|
||||
+ params.map(p=>renderParamRow(p,allInst)).join('');
|
||||
+ params.map(p => renderParamRow(p, allInst)).join('');
|
||||
|
||||
// Кнопка «Запустить»
|
||||
const btn=document.getElementById('btn-test');
|
||||
btn.style.display='inline-flex';
|
||||
btn.textContent='Запустить';
|
||||
btn.onclick=()=>{
|
||||
const btn = document.getElementById('btn-test');
|
||||
btn.style.display = 'inline-flex';
|
||||
btn.textContent = 'Запустить';
|
||||
btn.onclick = () => {
|
||||
// Валидация JSON-полей перед отправкой
|
||||
let bad=[];
|
||||
document.querySelectorAll('#params-form [data-type="map"]').forEach(el=>{
|
||||
if(!validateJson(el,true)) bad.push(el);
|
||||
let bad = [];
|
||||
document.querySelectorAll('#params-form [data-type="map"]').forEach(el => {
|
||||
if (!validateJson(el, true)) bad.push(el);
|
||||
});
|
||||
if(bad.length>0){
|
||||
document.getElementById('test-status').innerHTML=
|
||||
if (bad.length > 0) {
|
||||
document.getElementById('test-status').innerHTML =
|
||||
`<span class="badge">Ошибка</span> ${bad.length} полей с невалидным JSON — поправьте и нажмите Запустить снова`;
|
||||
return;
|
||||
}
|
||||
const pp=collectParams(); // собрать значения из формы
|
||||
const pp = collectParams(); // собрать значения из формы
|
||||
executeOp(pp); // отправить
|
||||
};
|
||||
}).catch(e=>{
|
||||
document.getElementById('params-form').innerHTML=
|
||||
}).catch(e => {
|
||||
document.getElementById('params-form').innerHTML =
|
||||
`<span style="color:var(--destructive);">Ошибка загрузки: ${_esc(e.message)}</span>`;
|
||||
});
|
||||
}
|
||||
@@ -180,29 +186,29 @@ function showParams(opId,opName){
|
||||
* @param {number} statusDuration— длительность в секундах
|
||||
* @param {string} instanceName — displayName инстанса
|
||||
*/
|
||||
function setFinishedState(statusText, statusClass, statusError, statusDuration, instanceName){
|
||||
busy=false; // разблокировать UI
|
||||
function setFinishedState(statusText, statusClass, statusError, statusDuration, instanceName) {
|
||||
busy = false; // разблокировать UI
|
||||
// Закрыть панели операций
|
||||
document.querySelectorAll('.inst-ops.open').forEach(e=>e.classList.remove('open'));
|
||||
const btn=document.getElementById('btn-test');
|
||||
btn.disabled=false;
|
||||
btn.textContent='Готово';
|
||||
btn.onclick=null; // убрать обработчик
|
||||
const opName=selectedOp?.opName||'операция';
|
||||
const durationText=statusDuration!==undefined&&statusDuration!==null?`${statusDuration}s`:'0s';
|
||||
const extra=_esc(statusError||'');
|
||||
const instName=_esc(instanceName||findInstName()||selectedInst||'');
|
||||
document.getElementById('test-status').innerHTML=
|
||||
document.querySelectorAll('.inst-ops.open').forEach(e => e.classList.remove('open'));
|
||||
const btn = document.getElementById('btn-test');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Готово';
|
||||
btn.onclick = null; // убрать обработчик
|
||||
const opName = selectedOp?.opName || 'операция';
|
||||
const durationText = statusDuration !== undefined && statusDuration !== null ? `${statusDuration}s` : '0s';
|
||||
const extra = _esc(statusError || '');
|
||||
const instName = _esc(instanceName || findInstName() || selectedInst || '');
|
||||
document.getElementById('test-status').innerHTML =
|
||||
` <span class="badge ${statusClass}">${statusText}</span> <span style="color:var(--muted);">${opName}</span> <span style="color:var(--muted);">${durationText}</span> <span style="color:var(--muted);">${instName}</span> ${extra}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Установить состояние «выполняется».
|
||||
*/
|
||||
function setRunningState(opName, displayName){
|
||||
const btn=document.getElementById('btn-test');
|
||||
btn.textContent=opName+'...';
|
||||
document.getElementById('test-status').textContent=' ⏳ '+ (displayName||opName) +' выполняется...';
|
||||
function setRunningState(opName, displayName) {
|
||||
const btn = document.getElementById('btn-test');
|
||||
btn.textContent = opName + '...';
|
||||
document.getElementById('test-status').textContent = ' ⏳ ' + (displayName || opName) + ' выполняется...';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,106 +224,106 @@ function setRunningState(opName, displayName){
|
||||
*
|
||||
* @param {Object} params — {numeric_param_id: value}
|
||||
*/
|
||||
async function executeOp(params){
|
||||
busy=true; // заблокировать UI
|
||||
async function executeOp(params) {
|
||||
busy = true; // заблокировать UI
|
||||
stopPoll();
|
||||
|
||||
const isCreate=selectedOp?.opName==='create';
|
||||
const displayNameInput=document.getElementById('param-displayname');
|
||||
const displayNameSuffix=(displayNameInput?.value||'').trim();
|
||||
const displayName=isCreate
|
||||
const isCreate = selectedOp?.opName === 'create';
|
||||
const displayNameInput = document.getElementById('param-displayname');
|
||||
const displayNameSuffix = (displayNameInput?.value || '').trim();
|
||||
const displayName = isCreate
|
||||
? `${AUTOTEST_PREFIX}${displayNameSuffix || makeCreateDisplayName()}`
|
||||
: findInstName();
|
||||
|
||||
// Подготовка UI
|
||||
document.getElementById('params-area').style.display='block';
|
||||
document.getElementById('params-form').innerHTML=''; // очистить форму
|
||||
const btn=document.getElementById('btn-test');
|
||||
btn.style.display='inline-flex';
|
||||
btn.textContent=selectedOp.opName+'...';
|
||||
btn.disabled=true;
|
||||
document.getElementById('params-area').style.display = 'block';
|
||||
document.getElementById('params-form').innerHTML = ''; // очистить форму
|
||||
const btn = document.getElementById('btn-test');
|
||||
btn.style.display = 'inline-flex';
|
||||
btn.textContent = selectedOp.opName + '...';
|
||||
btn.disabled = true;
|
||||
setRunningState(selectedOp.opName, displayName);
|
||||
|
||||
// Создать контейнер для этапов (после кнопки create или после панели операций)
|
||||
const instId=selectedInst||'';
|
||||
const afterEl=instId?document.getElementById('ops-'+instId):null;
|
||||
document.querySelectorAll('.stages-box').forEach(e=>e.remove());
|
||||
const stagesBox=document.createElement('div');
|
||||
stagesBox.className='stages-box';
|
||||
stagesBox.style.cssText='max-height:200px;overflow-y:auto;padding:4px 8px;margin-left:16px;background:var(--brand-grey-light);border-radius:4px;';
|
||||
if(afterEl){afterEl.after(stagesBox);}
|
||||
else{document.getElementById('create-btn-area').after(stagesBox);}
|
||||
const instId = selectedInst || '';
|
||||
const afterEl = instId ? document.getElementById('ops-' + instId) : null;
|
||||
document.querySelectorAll('.stages-box').forEach(e => e.remove());
|
||||
const stagesBox = document.createElement('div');
|
||||
stagesBox.className = 'stages-box';
|
||||
stagesBox.style.cssText = 'max-height:200px;overflow-y:auto;padding:4px 8px;margin-left:16px;background:var(--brand-grey-light);border-radius:4px;';
|
||||
if (afterEl) { afterEl.after(stagesBox); }
|
||||
else { document.getElementById('create-btn-area').after(stagesBox); }
|
||||
|
||||
try{
|
||||
try {
|
||||
// POST /api/test — тело: serviceId, operation, svcOperationId, params, instanceUid, displayName
|
||||
const r=await fetch('/api/test',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({
|
||||
serviceId:currentSvcId,
|
||||
operation:selectedOp.opName,
|
||||
svcOperationId:selectedOp.opId,
|
||||
const r = await fetch('/api/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
serviceId: currentSvcId,
|
||||
operation: selectedOp.opName,
|
||||
svcOperationId: selectedOp.opId,
|
||||
params,
|
||||
instanceUid:selectedInst||'',
|
||||
displayName:isCreate?displayName:''
|
||||
instanceUid: selectedInst || '',
|
||||
displayName: isCreate ? displayName : ''
|
||||
})
|
||||
});
|
||||
const d=await r.json();
|
||||
const d = await r.json();
|
||||
|
||||
// FAIL на старте — нечего поллить
|
||||
if(d.status==='FAIL'){
|
||||
busy=false;
|
||||
document.getElementById('test-status').innerHTML=
|
||||
` <span class="badge">FAIL</span> ${_esc(d.error||'')}`;
|
||||
btn.disabled=false;
|
||||
if (d.status === 'FAIL') {
|
||||
busy = false;
|
||||
document.getElementById('test-status').innerHTML =
|
||||
` <span class="badge">FAIL</span> ${_esc(d.error || '')}`;
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// CMDB-delete (opUid начинается с "cmdb-") — мгновенный успех
|
||||
if(d.status==='OK'&&(d.opUid||'').startsWith('cmdb-')){
|
||||
busy=false;
|
||||
setFinishedState('OK','badge-success','',0,d.displayName);
|
||||
try{await refreshInstances();}catch(e){}
|
||||
if(historyOpen) try{await loadHistory();}catch(e){}
|
||||
if (d.status === 'OK' && (d.opUid || '').startsWith('cmdb-')) {
|
||||
busy = false;
|
||||
setFinishedState('OK', 'badge-success', '', 0, d.displayName);
|
||||
try { await refreshInstances(); } catch (e) { }
|
||||
if (historyOpen) try { await loadHistory(); } catch (e) { }
|
||||
return;
|
||||
}
|
||||
|
||||
const opUid=d.opUid;
|
||||
const instanceName=d.displayName||displayName;
|
||||
const opUid = d.opUid;
|
||||
const instanceName = d.displayName || displayName;
|
||||
|
||||
// Поллинг: каждые 2 секунды
|
||||
let _pollErrors=0;
|
||||
pollTimer=setInterval(async()=>{
|
||||
try{
|
||||
const sr=await fetch('/api/test/status/'+opUid);
|
||||
const sd=await sr.json();
|
||||
_pollErrors=0; // сброс счётчика ошибок
|
||||
showStages(sd.stages||[]); // обновить этапы
|
||||
if(sd.status!=='RUNNING'){
|
||||
let _pollErrors = 0;
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const sr = await fetch('/api/test/status/' + opUid);
|
||||
const sd = await sr.json();
|
||||
_pollErrors = 0; // сброс счётчика ошибок
|
||||
showStages(sd.stages || []); // обновить этапы
|
||||
if (sd.status !== 'RUNNING') {
|
||||
stopPoll();
|
||||
setFinishedState(
|
||||
sd.status,
|
||||
sd.status==='OK'?'badge-success':'badge',
|
||||
sd.error||sd.errorLog,
|
||||
sd.status === 'OK' ? 'badge-success' : 'badge',
|
||||
sd.error || sd.errorLog,
|
||||
sd.duration,
|
||||
sd.displayName||instanceName
|
||||
sd.displayName || instanceName
|
||||
);
|
||||
await refreshInstances(); // обновить список инстансов
|
||||
if(historyOpen) await loadHistory(); // обновить историю
|
||||
if (historyOpen) await loadHistory(); // обновить историю
|
||||
}
|
||||
}catch(e){
|
||||
} catch (e) {
|
||||
_pollErrors++;
|
||||
// 5 ошибок подряд (10 секунд) → считаем связь потерянной
|
||||
if(_pollErrors>=5){
|
||||
if (_pollErrors >= 5) {
|
||||
stopPoll();
|
||||
setFinishedState('TIMEOUT','','Связь потеряна',0,instanceName);
|
||||
setFinishedState('TIMEOUT', '', 'Связь потеряна', 0, instanceName);
|
||||
}
|
||||
}
|
||||
},2000);
|
||||
}catch(e){
|
||||
busy=false;
|
||||
document.getElementById('test-status').textContent=' Ошибка: '+e.message;
|
||||
btn.disabled=false;
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
busy = false;
|
||||
document.getElementById('test-status').textContent = ' Ошибка: ' + e.message;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,15 +334,15 @@ async function executeOp(params){
|
||||
*
|
||||
* @param {Array} stages — [{stage, dtStart, dtFinish, isSuccessful, duration}, ...]
|
||||
*/
|
||||
function showStages(stages){
|
||||
if(!stages||!stages.length) return;
|
||||
let html='<div style="font-size:11px;font-weight:600;color:var(--muted);margin-top:4px;">Этапы</div>';
|
||||
stages.forEach(s=>{
|
||||
const done=!!s.dtFinish; // этап завершён?
|
||||
const icon=done?(s.isSuccessful?'✅':'❌'):'⏳';
|
||||
html+=`<div style="font-size:11px;padding:2px 0;">${icon} ${_esc(s.stage)} — ${(s.duration||0).toFixed(1)}s</div>`;
|
||||
function showStages(stages) {
|
||||
if (!stages || !stages.length) return;
|
||||
let html = '<div style="font-size:11px;font-weight:600;color:var(--muted);margin-top:4px;">Этапы</div>';
|
||||
stages.forEach(s => {
|
||||
const done = !!s.dtFinish; // этап завершён?
|
||||
const icon = done ? (s.isSuccessful ? '✅' : '❌') : '⏳';
|
||||
html += `<div style="font-size:11px;padding:2px 0;">${icon} ${_esc(s.stage)} — ${(s.duration || 0).toFixed(1)}s</div>`;
|
||||
});
|
||||
// Показать в ПОСЛЕДНЕМ контейнере этапов (самом новом)
|
||||
const boxes=document.querySelectorAll('.stages-box');
|
||||
if(boxes.length) boxes[boxes.length-1].innerHTML=html;
|
||||
const boxes = document.querySelectorAll('.stages-box');
|
||||
if (boxes.length) boxes[boxes.length - 1].innerHTML = html;
|
||||
}
|
||||
|
||||
@@ -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