Files
app-autotest/site/static/js/scenario-list.js
T
naeel beaff3f1cf v1.2.12: scenario list — clean header row + action buttons in expanded view
- Header: name | step count | seed badge (no CRUD buttons)
- Expanded view: ▶ Запустить ✏ Редактировать 📋 Копировать 🗑 Удалить
  (same size, aligned, with labels)
- Delete hidden for seed scenarios
2026-07-31 14:02:50 +04:00

128 lines
7.7 KiB
JavaScript

// scenario-list.js — список сценариев, запуск, поллинг
let scenarioOpen=false, scenarioPollTimer=null;
async function toggleScenario(){
const body=document.getElementById('scenario-body');
scenarioOpen=!scenarioOpen;
body.style.display=scenarioOpen?'block':'none';
if(!scenarioOpen){ stopScenarioPoll(); return; }
await loadScenarios();
}
async function loadScenarios(){
const body=document.getElementById('scenario-body');
try{
const [sr, srHistory]=await Promise.all([
fetch('/api/scenarios').then(r=>r.json()),
fetch('/api/scenario/status').then(r=>r.json()),
]);
let html='';
html+=`<button class="btn btn-sm" style="font-size:11px;margin-bottom:4px;" onclick="createScenario()">+ Создать сценарий</button>`;
if(sr.length){
html+='<div style="display:flex;flex-direction:column;gap:4px;margin-bottom:4px;">';
sr.forEach(s=>{
const seed = s.is_seed;
html+=`<div style="border:1px solid var(--brand-gray);border-radius:4px;padding:4px 6px;">`;
html+=`<div style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 0;" onclick="toggleScenarioSteps(${s.id})">`;
html+=`<span style="font-size:13px;font-weight:600;flex:1;">${_esc(s.name)}</span>`;
html+=`<span style="color:var(--muted);font-size:11px;min-width:50px;">${s.steps} шаг.</span>`;
if(seed) html+=`<span style="font-size:9px;color:var(--muted);">seed</span>`;
html+=`</div>`;
html+=`<div id="scenario-steps-${s.id}" style="display:none;margin-top:4px;padding-left:8px;"></div>`;
html+=`</div>`;
});
html+='</div>';
}else{
html+='<span style="color:var(--muted);font-size:11px;">Нет сценариев в БД</span>';
}
const curVer=window.APP&&window.APP.version||'';
const verHistory=srHistory&&srHistory.filter(r=>r.app_version===curVer);
if(verHistory && verHistory.length){
const last=verHistory[0];
const icon=last.status==='OK'?'✅':last.status==='FAIL'?'❌':last.status==='RUNNING'?'⏳':'⏱';
const time=last.created_at?last.created_at.replace('T',' ').substring(0,19):'?';
const runningStyle = last.status==='RUNNING' ? 'background:#fef3c7;padding:2px 4px;border-radius:3px;' : '';
html+=`<div style="font-size:11px;margin-top:4px;color:var(--muted);${runningStyle}">Последний: ${icon} ${_esc(last.scenario_name)}${last.status} (шаг ${last.current_step}/${last.total_steps}) ${last.duration_sec!=null?last.duration_sec.toFixed(1)+'s':''}${time}</div>`;
if(last.error_log) html+=`<div style="font-size:10px;color:var(--destructive);">${_esc(last.error_log)}</div>`;
}
body.innerHTML=html;
}catch(e){body.innerHTML='<span style="color:var(--destructive);font-size:11px;">Ошибка загрузки</span>';}
}
async function toggleScenarioSteps(defId) {
const el = document.getElementById('scenario-steps-' + defId);
if (!el) return;
if (el.style.display === 'block') { el.style.display = 'none'; return; }
// Загрузить шаги
try {
const r = await fetch('/api/scenario/definitions/' + defId);
const def = await r.json();
if (def.error) return;
const steps = def.steps || [];
let html = '<div style="font-size:11px;padding:4px 0;">';
steps.forEach((s, i) => {
const svcName = s.service_id ? 'сервис ' + s.service_id : '?';
html += `<div style="padding:2px 0;">${i+1}. <b>${_esc(s.operation)}</b> → ${svcName}`;
if (s.output) html += ` <span style="color:var(--muted);">(${_esc(s.output)})</span>`;
if (s.instance_ref) html += ` <span style="color:var(--muted);">→ ${_esc(s.instance_ref)}</span>`;
if (s.instance_uid) html += ` <span style="color:var(--muted);">→ ${_esc(s.instance_uid.substring(0,8))}...</span>`;
const params = Object.entries(s.params || {});
if (params.length) html += ' <span style="color:var(--muted);">(' + params.map(([k,v]) => k+'='+v).join(', ') + ')</span>';
html += '</div>';
});
html += `<button class="btn btn-sm" style="margin-top:4px;font-size:11px;background:var(--brand-primary);color:#fff;" onclick="event.stopPropagation();runScenario(${defId})">▶ Запустить</button>`;
html += `<button class="btn btn-sm" style="margin-top:4px;margin-left:4px;font-size:11px;" onclick="event.stopPropagation();editScenario(${defId})">✏ Редактировать</button>`;
html += `<button class="btn btn-sm" style="margin-top:4px;margin-left:4px;font-size:11px;" onclick="event.stopPropagation();cloneScenario(${defId},'${_esc(def.name)}')">📋 Копировать</button>`;
if (!def.is_seed) html += `<button class="btn btn-sm" style="margin-top:4px;margin-left:4px;font-size:11px;color:var(--destructive);" onclick="event.stopPropagation();deleteScenario(${defId},'${_esc(def.name)}')">🗑 Удалить</button>`;
html += '</div>';
el.innerHTML = html;
el.style.display = 'block';
// Закрыть другие
document.querySelectorAll('[id^="scenario-steps-"]').forEach(e => { if (e !== el) e.style.display = 'none'; });
} catch (e) {}
}
async function runScenario(defId){
if(busy){ return; }
if(!confirm('Запустить сценарий?')) return;
busy=true;
stopScenarioPoll();
const body=document.getElementById('scenario-body');
body.innerHTML=`<div style="font-size:11px;">⏳ Запуск...</div>`;
try{
const r=await fetch('/api/scenario/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({definition_id:defId})});
const d=await r.json();
if(d.error){ body.innerHTML=`<span style="color:var(--destructive);">Ошибка: ${_esc(d.error)}</span>`; busy=false; return; }
const runId=d.run_id;
if(!runId){ body.innerHTML=`<span style="color:var(--destructive);">Ошибка: нет run_id в ответе</span>`; busy=false; return; }
scenarioPollTimer=setInterval(async()=>{
try{
const sr=await fetch('/api/scenario/run/'+runId);
if(!sr.ok){ return; }
const last=await sr.json();
if(!last||last.error) return;
const icon=last.status==='OK'?'✅':last.status==='FAIL'?'❌':last.status==='RUNNING'?'⏳':'⏱';
const time=last.created_at?last.created_at.replace('T',' ').substring(0,19):'?';
const runningBg = last.status==='RUNNING' ? 'background:#fef3c7;padding:2px 4px;border-radius:3px;' : '';
let html=`<div style="font-size:11px;${runningBg}">${icon} ${_esc(last.scenario_name)}${last.status} (шаг ${last.current_step}/${last.total_steps}) ${last.duration_sec!=null?last.duration_sec.toFixed(1)+'s':''}${time}</div>`;
if(last.error_log) html+=`<div style="font-size:10px;color:var(--destructive);">${_esc(last.error_log)}</div>`;
body.innerHTML=html;
if(last.status!=='RUNNING'){
stopScenarioPoll();
busy=false;
await refreshInstances();
if(historyOpen) await loadHistory();
}
}catch(e){}
},3000);
}catch(e){
body.innerHTML=`<span style="color:var(--destructive);">Ошибка: ${_esc(e.message)}</span>`;
busy=false;
}
}
function stopScenarioPoll(){
if(scenarioPollTimer){ clearInterval(scenarioPollTimer); scenarioPollTimer=null; }
}