v1.1.55: split large files + CRUD scenario editor
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
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
// operations.js — запуск операций: create, modify, delete, poll, stages
|
||||
let pollTimer=null;
|
||||
|
||||
function makeCreateDisplayName(){
|
||||
const rand = (Math.random() * 46656 | 0).toString(36).padStart(3, '0');
|
||||
return currentSvcShort ? currentSvcShort+'-'+rand : rand;
|
||||
}
|
||||
|
||||
function findInstName(){
|
||||
const i=svcInstances.find(x=>x.instanceUid===selectedInst);
|
||||
return i?i.displayName:selectedInst;
|
||||
}
|
||||
|
||||
function stopPoll(){
|
||||
if(pollTimer){clearInterval(pollTimer);pollTimer=null;}
|
||||
}
|
||||
|
||||
async function startCreate(){
|
||||
if(busy) return;
|
||||
selectedInst=null; 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;
|
||||
showParams(opId,'create');
|
||||
}catch(e){
|
||||
showParams(18,'create');
|
||||
}
|
||||
}
|
||||
|
||||
function runOp(opName,opId){
|
||||
if(busy) return;
|
||||
stopPoll();
|
||||
showParams(opId,opName);
|
||||
}
|
||||
|
||||
function showParams(opId,opName){
|
||||
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='';
|
||||
|
||||
const isCreate=opName==='create';
|
||||
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=`<span style="color:var(--destructive);">Ошибка: ${_esc(data.error)}</span>`;return;}
|
||||
const params=data.params||data;
|
||||
if(!Array.isArray(params)){document.getElementById('params-form').innerHTML=`<span style="color:var(--destructive);">Неверный формат параметров</span>`;return;}
|
||||
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 createHeader=isCreate?`<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--brand-primary);">Создание: ${_esc(currentSvcName)}</div>`:'';
|
||||
const opHeader=!isCreate?`<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--brand-primary);">${_esc(opName)} → ${_esc(findInstName())}</div>`:'';
|
||||
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('');
|
||||
const btn=document.getElementById('btn-test');
|
||||
btn.style.display='inline-flex';
|
||||
btn.textContent='Запустить';
|
||||
btn.onclick=()=>{
|
||||
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=`<span class="badge">Ошибка</span> ${bad.length} полей с невалидным JSON — поправьте и нажмите Запустить снова`;
|
||||
return;
|
||||
}
|
||||
const pp=collectParams();
|
||||
executeOp(pp);
|
||||
};
|
||||
}).catch(e=>{
|
||||
document.getElementById('params-form').innerHTML=`<span style="color:var(--destructive);">Ошибка загрузки: ${_esc(e.message)}</span>`;
|
||||
});
|
||||
}
|
||||
|
||||
function renderParamRow(p,allInst){
|
||||
const req=p.isRequired;
|
||||
const hasDfl=p.defaultValue!==null&&p.defaultValue!==undefined&&p.defaultValue!=='';
|
||||
const labelCls=req?(hasDfl?'req':'req-nodfl'):'';
|
||||
const dfl=hasDfl?p.defaultValue:(req?'':'');
|
||||
const vl=p.valueList;
|
||||
const isMap=(p.dataType||'').startsWith('map');
|
||||
let input;
|
||||
if(vl&&Array.isArray(vl)){
|
||||
input=`<select name="p_${p.svcOperationCfsParamId}" data-type="${_esc(p.dataType)}">${vl.map(v=>`<option value="${_esc(v)}" ${v==dfl?'selected':''}>${_esc(v)}</option>`).join('')}</select>`;
|
||||
}else if(p.refSvcId){
|
||||
const refInsts=allInst.filter(i=>i.serviceId===p.refSvcId&&i.explainedStatus!=='deleted');
|
||||
let opts=refInsts.map(i=>`<option value="${_esc(i.instanceUid)}" ${i.instanceUid===dfl?'selected':''}>${_esc(i.displayName)}</option>`).join('');
|
||||
if(!dfl) opts='<option value="">— выбрать —</option>'+opts;
|
||||
input=`<select name="p_${p.svcOperationCfsParamId}" data-type="ref">${opts}</select>`;
|
||||
}else if(p.dataType==='boolean'){
|
||||
input=`<select name="p_${p.svcOperationCfsParamId}" data-type="boolean"><option value="true" ${dfl==='true'||dfl===true?'selected':''}>true</option><option value="false" ${dfl==='false'||dfl===false?'selected':''}>false</option></select>`;
|
||||
}else if(p.dataDescriptor&&isMap){
|
||||
return renderMapFixedRow(p,dfl);
|
||||
}else{
|
||||
const dt=isMap?'map':'';
|
||||
input=`<input type="text" name="p_${p.svcOperationCfsParamId}" value="${_esc(dfl)}" data-type="${dt}" placeholder="${req&&!hasDfl?'обязательно':''}"${isMap?' onblur="validateJson(this)"':''}>`;
|
||||
if(isMap) input+=`<span class="json-err" style="display:none;color:var(--destructive);font-size:10px;margin-left:4px;"></span>`;
|
||||
}
|
||||
return `<div class="param-row"><label class="${labelCls}">${_esc(p.name)}</label>${input}</div>`;
|
||||
}
|
||||
|
||||
function renderMapFixedRow(p,dfl){
|
||||
const dd=p.dataDescriptor;
|
||||
let subHtml='';
|
||||
let dflObj={};
|
||||
try{dflObj=JSON.parse(dfl||'{}');}catch(e){}
|
||||
Object.keys(dd).forEach(subKey=>{
|
||||
const sub=dd[subKey];
|
||||
const subDfl=dflObj[subKey]!==undefined?dflObj[subKey]:(sub.defaultValue||'');
|
||||
const subVl=sub.valueList;
|
||||
const subReq=sub.isRequired;
|
||||
const subNoDfl=subReq&&!subDfl&&subDfl!==0&&subDfl!==false;
|
||||
const subLblCls=subNoDfl?'req-nodfl':'';
|
||||
let si;
|
||||
if(subVl&&Array.isArray(subVl)){
|
||||
si=`<select name="p_${p.svcOperationCfsParamId}.${subKey}" data-type="mapchild">${subVl.map(v=>`<option value="${_esc(v)}" ${v==subDfl?'selected':''}>${_esc(v)}</option>`).join('')}</select>`;
|
||||
}else if(sub.dataType==='boolean'){
|
||||
si=`<select name="p_${p.svcOperationCfsParamId}.${subKey}" data-type="mapchild"><option value="true" ${subDfl==='true'||subDfl===true?'selected':''}>true</option><option value="false" ${subDfl==='false'||subDfl===false?'selected':''}>false</option></select>`;
|
||||
}else{
|
||||
si=`<input type="text" name="p_${p.svcOperationCfsParamId}.${subKey}" value="${_esc(subDfl)}" data-type="mapchild">`;
|
||||
}
|
||||
subHtml+=`<div class="param-row"><label class="${subLblCls}">${_esc(subKey)}</label>${si}</div>`;
|
||||
});
|
||||
return `<div class="param-row"><label class="${labelCls}">${_esc(p.name)}</label></div><div style="border-left:2px solid var(--brand-gray);margin-left:8px;padding:0 0 4px 8px;">${subHtml}</div>`;
|
||||
}
|
||||
|
||||
function collectParams(){
|
||||
const pp={};
|
||||
const nested={};
|
||||
document.querySelectorAll('#params-form [name^="p_"]').forEach(el=>{
|
||||
let v=el.value;
|
||||
const dt=el.dataset.type||'';
|
||||
if(dt==='mapchild'){
|
||||
const name=el.name.replace('p_','');
|
||||
const dot=name.indexOf('.');
|
||||
const parentKey=name.substring(0,dot);
|
||||
const subKey=name.substring(dot+1);
|
||||
if(!nested[parentKey]) nested[parentKey]={};
|
||||
nested[parentKey][subKey]=v;
|
||||
return;
|
||||
}
|
||||
if(dt==='array'||dt.startsWith('array')) v=JSON.stringify([v]);
|
||||
if(dt==='map'||dt==='map-fixed') v=v||'{}';
|
||||
pp[el.name.replace('p_','')]=v;
|
||||
});
|
||||
for(const pk in nested) pp[pk]=JSON.stringify(nested[pk]);
|
||||
return pp;
|
||||
}
|
||||
|
||||
function setFinishedState(statusText, statusClass, statusError, statusDuration, instanceName){
|
||||
busy=false;
|
||||
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) +' выполняется...';
|
||||
}
|
||||
|
||||
async function executeOp(params){
|
||||
busy=true;
|
||||
stopPoll();
|
||||
const isCreate=selectedOp?.opName==='create';
|
||||
const displayNameInput=document.getElementById('param-displayname');
|
||||
const displayNameSuffix=(displayNameInput?.value||'').trim();
|
||||
const displayName=isCreate ? `${AUTOTEST_PREFIX}${displayNameSuffix || makeCreateDisplayName()}` : findInstName();
|
||||
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);
|
||||
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{
|
||||
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:''
|
||||
})});
|
||||
const d=await r.json();
|
||||
if(d.status==='FAIL'){busy=false;document.getElementById('test-status').innerHTML=` <span class="badge">FAIL</span> ${_esc(d.error||'')}`;btn.disabled=false;return;}
|
||||
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;
|
||||
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.duration, sd.displayName||instanceName);
|
||||
await refreshInstances();
|
||||
if(historyOpen) await loadHistory();
|
||||
}
|
||||
}catch(e){
|
||||
_pollErrors++;
|
||||
if(_pollErrors>=5){stopPoll();setFinishedState('TIMEOUT','','Связь потеряна',0,instanceName);}
|
||||
}
|
||||
},2000);
|
||||
}catch(e){
|
||||
busy=false;
|
||||
document.getElementById('test-status').textContent=' Ошибка: '+e.message;
|
||||
btn.disabled=false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user