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
+159 -153
View File
@@ -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;
}