UI: instances + expand operations, create separate, only dummy, v1.0.40

This commit is contained in:
2026-07-26 08:31:27 +04:00
parent b0ef0c98fe
commit 9348c4089f
2 changed files with 70 additions and 66 deletions
+69 -65
View File
@@ -13,8 +13,10 @@
.col-main { flex:1; min-width:0; }
.svc-item { cursor:pointer; padding:4px 8px; font-size:12px; border-radius:4px; }
.svc-item:hover, .svc-item.active { background:var(--brand-grey-light); }
.op-item { display:flex; align-items:center; gap:6px; padding:3px 8px; font-size:12px; }
.op-item.disabled { opacity:0.4; }
.inst-item { cursor:pointer; display:flex; align-items:center; gap:6px; padding:4px 8px; font-size:12px; border-radius:4px; }
.inst-item:hover, .inst-item.active { background:var(--brand-grey-light); }
.inst-ops { display:none; margin-left:16px; }
.inst-ops.open { display:block; }
.btn-sm { height:24px; font-size:11px; padding:0 8px; }
.param-row { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
.param-row label { width:160px; font-size:11px; text-align:right; flex-shrink:0; }
@@ -68,17 +70,19 @@
<div class="col-svc">
<div class="card">
<div class="card-header" style="font-size:12px;">Сервисы</div>
<div class="card-body" style="padding:4px;max-height:70vh;overflow-y:auto;" id="svc-list">
<span style="color:var(--muted);font-size:11px;">Загрузка...</span>
<div class="card-body" style="padding:4px;">
<div class="svc-item active" onclick="selectService(1)">1. Болванка</div>
</div>
</div>
</div>
<!-- Операции + Параметры + Результат -->
<!-- Инстансы + Операции + Параметры -->
<div class="col-main">
<div class="card" id="ops-card" style="display:none;">
<div class="card-header" style="font-size:12px;" id="ops-title">Операции</div>
<div class="card-body" style="padding:4px;" id="ops-list"></div>
<div class="card">
<div class="card-header" style="font-size:12px;">Инстансы</div>
<div class="card-body" style="padding:4px;" id="inst-list">
<span style="color:var(--muted);font-size:11px;">Загрузка...</span>
</div>
</div>
<div class="card" id="params-card" style="margin-top:8px;display:none;">
<div class="card-header" style="font-size:12px;">Параметры</div>
@@ -92,21 +96,14 @@
</div>
<script>
let selectedSvc=null, selectedOp=null, svcInstances=[], selectedInst=null;
let svcInstances=[], selectedInst=null, selectedOp=null;
const SVC_ID=1;
(async function(){
const el=document.getElementById('svc-list');
try{
const r=await fetch('/api/services');
const data=await r.json();
el.innerHTML=data.map(s=>`<div class="svc-item" data-svc-id="${s.svcId}" onclick="selectService(${s.svcId})">${s.svcId}. ${s.svc}</div>`).join('');
if(data.length>0) selectService(data[0].svcId);
}catch(e){el.innerHTML='<span style="color:var(--destructive);font-size:11px;">Ошибка загрузки</span>';}
})();
// Загрузка при старте
selectService(SVC_ID);
async function selectService(svcId){
selectedSvc=svcId; selectedOp=null; selectedInst=null;
document.querySelectorAll('.svc-item').forEach(e=>e.classList.toggle('active',e.dataset.svcId==String(svcId)));
selectedInst=null; selectedOp=null;
document.getElementById('params-card').style.display='none';
document.getElementById('btn-test').style.display='none';
@@ -114,46 +111,60 @@ async function selectService(svcId){
const d=await r.json();
svcInstances=d.instances||[];
document.getElementById('ops-card').style.display='block';
document.getElementById('ops-title').textContent=d.svc+' — операции';
let html='';
// Инстансы с именами и статусами
if(svcInstances.length>0){
html+='<div style="font-size:11px;font-weight:600;padding:4px 8px;color:var(--muted);">Инстансы</div>';
svcInstances.forEach(i=>{
const sc=i.explainedStatus==='running'?'badge-success':'';
html+=`<div class="op-item" style="cursor:pointer;" onclick="selectInstance('${i.instanceUid}')" data-iuid="${i.instanceUid}">
<span style="flex:1;font-size:12px;">${i.displayName}</span>
<span class="badge ${sc}" style="font-size:9px;">${i.explainedStatus||'?'}</span>
</div>`;
});
}
// Операции
html+='<div style="font-size:11px;font-weight:600;padding:4px 8px;color:var(--muted);margin-top:4px;">Операции</div>';
d.operations.filter(o=>o.operation!=='reconcile').forEach(o=>{
const isCreate=o.operation==='create';
const disabled=!isCreate&&!selectedInst;
const btn=disabled?'':`<button class="btn btn-primary btn-sm" onclick="selectOperation(${o.svcOperationId},'${o.operation}',${svcId})">Тест</button>`;
const hint=isCreate&&svcInstances.length>0?'<span style="font-size:10px;color:var(--muted);">уже есть</span>':
(!isCreate&&!selectedInst?'<span style="font-size:10px;color:var(--muted);">выберите инстанс</span>':'');
html+=`<div class="op-item${disabled?' disabled':''}" id="op-${o.operation}">${btn} ${o.operation} ${hint}</div>`;
svcInstances.forEach(i=>{
const sc=i.explainedStatus==='running'?'badge-success':'';
html+=`<div class="inst-item" onclick="toggleInstance('${i.instanceUid}')" data-iuid="${i.instanceUid}">
<span style="flex:1;">${i.displayName}</span>
<span class="badge ${sc}" style="font-size:9px;">${i.explainedStatus||'?'}</span>
</div>`;
html+=`<div class="inst-ops" id="ops-${i.instanceUid}"></div>`;
});
document.getElementById('ops-list').innerHTML=html;
// Кнопка создать
html+=`<div class="inst-item" style="margin-top:8px;border-top:1px solid var(--brand-gray);padding-top:8px;" onclick="startCreate()">
<span style="flex:1;color:var(--brand-primary);">+ Создать новый инстанс</span>
</div>`;
document.getElementById('inst-list').innerHTML=html;
}
function selectInstance(iuid){
async function toggleInstance(iuid){
selectedInst=iuid;
document.querySelectorAll('[data-iuid]').forEach(e=>e.classList.toggle('active',e.dataset.iuid===iuid));
document.querySelectorAll('#ops-list .op-item').forEach(e=>{
if(e.id!=='op-create') e.classList.remove('disabled');
});
document.querySelectorAll('#op-create').forEach(e=>e.classList.add('disabled'));
const opsEl=document.getElementById('ops-'+iuid);
if(opsEl.classList.contains('open')){
opsEl.classList.remove('open');
document.getElementById('params-card').style.display='none';
return;
}
// Закрыть все
document.querySelectorAll('.inst-ops').forEach(e=>e.classList.remove('open'));
const r=await fetch('/api/operations/'+SVC_ID);
const d=await r.json();
const ops=d.operations.filter(o=>o.operation!=='reconcile'&&o.operation!=='create');
opsEl.innerHTML=ops.map(o=>
`<div class="inst-item" style="margin-left:0;">
<span style="flex:1;">${o.operation}</span>
<button class="btn btn-primary btn-sm" onclick="startOperation(${o.svcOperationId},'${o.operation}')">Тест</button>
</div>`
).join('');
opsEl.classList.add('open');
}
async function selectOperation(opId,opName,svcId){
selectedOp={opId,opName,svcId};
function startCreate(){
selectedInst=null;
document.querySelectorAll('[data-iuid]').forEach(e=>e.classList.remove('active'));
document.querySelectorAll('.inst-ops').forEach(e=>e.classList.remove('open'));
loadParams(18,'create'); // svcOperationId for create = 18
}
function startOperation(opId,opName){
loadParams(opId,opName);
}
async function loadParams(opId,opName){
selectedOp={opId,opName,svcId:SVC_ID};
document.getElementById('params-card').style.display='block';
document.getElementById('btn-test').style.display='inline-flex';
@@ -161,8 +172,8 @@ async function selectOperation(opId,opName,svcId){
const params=await r.json();
const form=document.getElementById('params-form');
const displayName='autotest-'+svcId+'-'+Date.now().toString(36);
form.innerHTML='<div class="param-row"><label class="req">displayName</label><input type="text" id="param-displayname" value="'+displayName+'"></div>'
const displayName=opName==='create'?('autotest-1-'+Date.now().toString(36)):'';
form.innerHTML=(opName==='create'?`<div class="param-row"><label class="req">displayName</label><input type="text" id="param-displayname" value="${displayName}"></div>`:'')
+ params.map(p=>{
const req=p.isRequired;
const hasDfl=p.defaultValue!==null&&p.defaultValue!==undefined&&p.defaultValue!=='';
@@ -179,7 +190,6 @@ async function selectOperation(opId,opName,svcId){
}
return `<div class="param-row"><label class="${labelCls}">${p.name}</label>${input}</div>`;
}).join('');
document.getElementById('test-status').textContent='';
}
@@ -198,25 +208,19 @@ async function runTest(){
params[el.name.replace('p_','')]=v;
});
let instUid=selectedInst||'';
if(selectedOp.opName==='create') instUid='';
try{
const r=await fetch('/api/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
serviceId:selectedOp.svcId,
serviceId:SVC_ID,
operation:selectedOp.opName,
svcOperationId:selectedOp.opId,
params,
instanceUid:instUid,
displayName:document.getElementById('param-displayname')?.value||('autotest-'+selectedOp.svcId)
instanceUid:selectedInst||'',
displayName:document.getElementById('param-displayname')?.value||('autotest-1')
})});
const d=await r.json();
const cls=d.status==='OK'?'badge badge-success':'badge';
document.getElementById('test-status').innerHTML=` <span class="${cls}">${d.status}</span> ${d.error||''} ${d.duration||''}s`;
if(d.status==='OK'&&selectedOp.opName==='create'){
// refresh services to update instance state
selectService(selectedOp.svcId);
}
if(d.status==='OK'){await selectService(SVC_ID);}
}catch(e){
document.getElementById('test-status').textContent=' Ошибка: '+e.message;
}