v9.51: infra row grid max 5 cols (fix sausage layout)

This commit is contained in:
Naeel
2026-04-17 10:38:09 +03:00
parent 256e00181b
commit 1e229f70d1
2 changed files with 89 additions and 113 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ spec:
spec: spec:
containers: containers:
- name: cloud-dashboard - name: cloud-dashboard
image: naeel/cloud-dashboard:v9.50 image: naeel/cloud-dashboard:v9.51
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8080 - containerPort: 8080
+88 -112
View File
@@ -5,9 +5,9 @@
var COLS=5, NW=170, NH=56, RW=160, RH=44, GX=30, GY=70, PX=40, PY=30; var COLS=5, NW=170, NH=56, RW=160, RH=44, GX=30, GY=70, PX=40, PY=30;
var INFRA = ["s3","vcloud","штурвал","cloud direct","edge","vdc","vapp"]; var INFRA = ["s3","vcloud","штурвал","cloud direct","edge","vdc","vapp"];
function isInfra(svc){ function isInfra(svc) {
var s=(svc||"").toLowerCase(); var s=(svc||"").toLowerCase();
for(var i=0;i<INFRA.length;i++){if(s.indexOf(INFRA[i])>=0)return true;} for(var i=0;i<INFRA.length;i++){ if(s.indexOf(INFRA[i])>=0) return true; }
return false; return false;
} }
@@ -22,182 +22,161 @@
]; ];
function svcStyle(svc){ function svcStyle(svc){
var s=(svc||"").toLowerCase(); var s=(svc||"").toLowerCase();
for(var i=0;i<SVC_STYLE.length;i++){ for(var i=0;i<SVC_STYLE.length;i++){ if(s.indexOf(SVC_STYLE[i][0])>=0) return {bg:SVC_STYLE[i][1],border:SVC_STYLE[i][2]}; }
if(s.indexOf(SVC_STYLE[i][0])>=0)return{bg:SVC_STYLE[i][1],border:SVC_STYLE[i][2]}; return {bg:"#1e293b",border:"#475569"};
}
return{bg:"#1e293b",border:"#475569"};
} }
function svcColor(svc){return svcStyle(svc).border;} function svcColor(svc){ return svcStyle(svc).border; }
function statusDot(s){ function statusDot(s){
var m={running:"#56d364",suspended:"#e3b341",deleted:"#f85149", var m={running:"#56d364",suspended:"#e3b341",deleted:"#f85149",deleting:"#f85149",pending:"#58a6ff",creating:"#58a6ff"};
deleting:"#f85149",pending:"#58a6ff",creating:"#58a6ff"};
return m[(s||"").toLowerCase()]||"#6e7681"; return m[(s||"").toLowerCase()]||"#6e7681";
} }
function esc(v){ function esc(v){ return String(v==null?"":v).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"); }
return String(v==null?"":v) function trunc(s,n){ return (!s)?"": s.length>n?s.slice(0,n-1)+"\u2026":s; }
.replace(/&/g,"&amp;").replace(/</g,"&lt;")
.replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}
function trunc(s,n){return(!s)?"":s.length>n?s.slice(0,n-1)+"\u2026":s;}
// ── State ──────────────────────────────────────────────────────────────── // ── State ────────────────────────────────────────────────────────────────
var graphCache=null, building=false; var graphCache=null, building=false;
function getOverlay(){return document.getElementById("graph-overlay");} function getOverlay(){ return document.getElementById("graph-overlay"); }
function getCanvas() {return document.getElementById("graph-canvas");} function getCanvas() { return document.getElementById("graph-canvas"); }
function getStatus() {return document.getElementById("graph-status");} function getStatus() { return document.getElementById("graph-status"); }
function setStatus(m){var el=getStatus();if(el)el.textContent=m;} function setStatus(m){ var el=getStatus(); if(el) el.textContent=m; }
function openGraph(){ function openGraph(){
var ov=getOverlay();if(!ov)return; var ov=getOverlay(); if(!ov) return;
ov.style.display="flex"; ov.style.display="flex";
if(graphCache){getCanvas().innerHTML=graphCache;attachPanzoom();} if(graphCache){ getCanvas().innerHTML=graphCache; attachPanzoom(); }
else if(!building)buildGraph(); else if(!building) buildGraph();
} }
function closeGraph(){var ov=getOverlay();if(ov)ov.style.display="none";} function closeGraph() { var ov=getOverlay(); if(ov) ov.style.display="none"; }
function refreshGraph(){graphCache=null;building=false;var c=getCanvas();if(c)c.innerHTML="";buildGraph();} function refreshGraph(){ graphCache=null; building=false; var c=getCanvas(); if(c) c.innerHTML=""; buildGraph(); }
// ── Layout ─────────────────────────────────────────────────────────────── // ── Layout ───────────────────────────────────────────────────────────────
function computeLayout(instances){ function computeLayout(instances){
var realms=[],seen={}; var realms=[], seen={};
instances.forEach(function(i){ instances.forEach(function(i){ var r=i.resourceRealm||""; if(r&&!seen[r]){seen[r]=true;realms.push(r);} });
var r=i.resourceRealm||"";
if(r&&!seen[r]){seen[r]=true;realms.push(r);} var infra = instances.filter(function(i){ return isInfra(i.svc); });
}); var others = instances.filter(function(i){ return !isInfra(i.svc); });
var infra = instances.filter(function(i){return isInfra(i.svc);});
var others = instances.filter(function(i){return !isInfra(i.svc);});
var canvasW = PX*2 + COLS*NW + (COLS-1)*GX; var canvasW = PX*2 + COLS*NW + (COLS-1)*GX;
// Row 0: platforms — spread evenly across full width // Row 0: platforms — spread evenly
var realmNodes = realms.map(function(r,i){ var realmNodes = realms.map(function(r,i){
var sp=canvasW/realms.length, cx=sp*i+sp/2; var sp=canvasW/realms.length, cx=sp*i+sp/2;
return{id:"realm:"+r,label:r,x:cx-RW/2,y:PY,w:RW,h:RH,cx:cx,cy:PY+RH/2}; return {id:"realm:"+r, label:r, x:cx-RW/2, y:PY, w:RW, h:RH, cx:cx, cy:PY+RH/2};
}); });
// Row 1: infra (S3, Cloud Director, Edge) — spread evenly // Row 1: infra (S3, CloudDir, Edge) — grid max COLS
var row1Y = PY+RH+GY; var row1Y = PY+RH+GY;
var infraRows = Math.ceil(Math.max(infra.length,1)/COLS);
var infraNodes = infra.map(function(inst,i){ var infraNodes = infra.map(function(inst,i){
var sp=canvasW/Math.max(infra.length,1), cx=sp*i+sp/2; var col=i%COLS, row=Math.floor(i/COLS);
return{uid:inst.instanceUid,inst:inst,x:cx-NW/2,y:row1Y,w:NW,h:NH,cx:cx,cy:row1Y+NH/2}; var x=PX+col*(NW+GX), y=row1Y+row*(NH+GY);
return {uid:inst.instanceUid, inst:inst, x:x, y:y, w:NW, h:NH, cx:x+NW/2, cy:y+NH/2};
}); });
// Row 2+: other instances — grid, max COLS per row // Row 2+: others — grid max COLS
var row2Y = row1Y+(infra.length>0?NH+GY:0); var row2Y = row1Y + (infra.length>0 ? infraRows*(NH+GY) : 0);
var otherNodes = others.map(function(inst,i){ var otherNodes = others.map(function(inst,i){
var col=i%COLS, row=Math.floor(i/COLS); var col=i%COLS, row=Math.floor(i/COLS);
var x=PX+col*(NW+GX), y=row2Y+row*(NH+GY); var x=PX+col*(NW+GX), y=row2Y+row*(NH+GY);
return{uid:inst.instanceUid,inst:inst,x:x,y:y,w:NW,h:NH,cx:x+NW/2,cy:y+NH/2}; return {uid:inst.instanceUid, inst:inst, x:x, y:y, w:NW, h:NH, cx:x+NW/2, cy:y+NH/2};
}); });
return{realms:realmNodes,infra:infraNodes,others:otherNodes};
return {realms:realmNodes, infra:infraNodes, others:otherNodes};
} }
// ── Manhattan (orthogonal) path ─────────────────────────────────────────── // ── Manhattan path ───────────────────────────────────────────────────────
// bottom-center of src → horizontal mid → top-center of tgt
function mpath(src,tgt){ function mpath(src,tgt){
var x1=src.cx, y1=src.y+src.h; // source bottom-center var x1=src.cx, y1=src.y+src.h;
var x2=tgt.cx, y2=tgt.y; // target top-center var x2=tgt.cx, y2=tgt.y;
if(Math.abs(y1-y2)<8){ if(Math.abs(y1-y2)<8){
// same row: dip below both nodes var dip=Math.max(src.y+src.h, tgt.y+tgt.h)+24;
var dip=Math.max(src.y+src.h,tgt.y+tgt.h)+24; return "M"+x1+","+y1+" L"+x1+","+dip+" L"+x2+","+dip+" L"+x2+","+y2;
return"M"+x1+","+y1+" L"+x1+","+dip+" L"+x2+","+dip+" L"+x2+","+y2;
} }
var my=(y1+y2)/2; var my=(y1+y2)/2;
return"M"+x1+","+y1+" L"+x1+","+my+" L"+x2+","+my+" L"+x2+","+y2; return "M"+x1+","+y1+" L"+x1+","+my+" L"+x2+","+my+" L"+x2+","+y2;
} }
// ── SVG render ─────────────────────────────────────────────────────────── // ── SVG render ───────────────────────────────────────────────────────────
function renderSVG(instances, depEdges){ function renderSVG(instances, depEdges){
var layout=computeLayout(instances); var layout=computeLayout(instances);
var nodeMap={}; var nodeMap={};
layout.realms.forEach(function(n){nodeMap[n.id]=n;}); layout.realms.forEach(function(n){ nodeMap[n.id]=n; });
layout.infra.forEach(function(n){nodeMap[n.uid]=n;}); layout.infra.forEach(function(n){ nodeMap[n.uid]=n; });
layout.others.forEach(function(n){nodeMap[n.uid]=n;}); layout.others.forEach(function(n){ nodeMap[n.uid]=n; });
var allN=layout.realms.concat(layout.infra).concat(layout.others); var allN=layout.realms.concat(layout.infra).concat(layout.others);
var maxX=0, maxY=0; var maxX=0, maxY=0;
allN.forEach(function(n){ allN.forEach(function(n){ if(n.x+n.w>maxX)maxX=n.x+n.w; if(n.y+n.h>maxY)maxY=n.y+n.h; });
if(n.x+n.w>maxX)maxX=n.x+n.w;
if(n.y+n.h>maxY)maxY=n.y+n.h;
});
var W=maxX+PX, H=maxY+PY; var W=maxX+PX, H=maxY+PY;
// Collect unique edge colors for arrowhead markers
var colors={"#334155":1}; var colors={"#334155":1};
depEdges.forEach(function(e){if(e.color)colors[e.color]=1;}); depEdges.forEach(function(e){ if(e.color) colors[e.color]=1; });
var p=[ var p=['<svg id="graph-svg" xmlns="http://www.w3.org/2000/svg" width="'+W+'" height="'+H+'" viewBox="0 0 '+W+' '+H+'">','<defs>'];
'<svg id="graph-svg" xmlns="http://www.w3.org/2000/svg" width="'+W+'" height="'+H+'" viewBox="0 0 '+W+' '+H+'">',
'<defs>'
];
Object.keys(colors).forEach(function(c){ Object.keys(colors).forEach(function(c){
var id="arr"+c.replace(/[^a-zA-Z0-9]/g,""); var id="arr"+c.replace(/[^a-zA-Z0-9]/g,"");
p.push('<marker id="'+id+'" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto">'+ p.push('<marker id="'+id+'" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="'+esc(c)+'"/></marker>');
'<path d="M0,0 L0,6 L8,3 z" fill="'+esc(c)+'"/></marker>');
}); });
p.push('</defs>'); p.push('</defs>');
// Platform → instance lines (thin dashed, gray) // Platform → instance (thin dashed)
instances.forEach(function(inst){ instances.forEach(function(inst){
var r=inst.resourceRealm; if(!r)return; var r=inst.resourceRealm; if(!r) return;
var src=nodeMap["realm:"+r], tgt=nodeMap[inst.instanceUid]; var src=nodeMap["realm:"+r], tgt=nodeMap[inst.instanceUid];
if(!src||!tgt)return; if(!src||!tgt) return;
p.push('<path d="'+esc(mpath(src,tgt))+'" fill="none" stroke="#334155" stroke-width="1" opacity="0.35" stroke-dasharray="4,3"/>'); p.push('<path d="'+esc(mpath(src,tgt))+'" fill="none" stroke="#334155" stroke-width="1" opacity="0.35" stroke-dasharray="4,3"/>');
}); });
// Dependency edges (colored Manhattan lines with arrow) // Dependency edges (colored Manhattan)
depEdges.forEach(function(e){ depEdges.forEach(function(e){
var src=nodeMap[e.from], tgt=nodeMap[e.to]; var src=nodeMap[e.from], tgt=nodeMap[e.to];
if(!src||!tgt)return; if(!src||!tgt) return;
var c=e.color||"#475569"; var c=e.color||"#475569";
var id="arr"+c.replace(/[^a-zA-Z0-9]/g,""); var id="arr"+c.replace(/[^a-zA-Z0-9]/g,"");
p.push('<path d="'+esc(mpath(src,tgt))+'" fill="none" stroke="'+esc(c)+ p.push('<path d="'+esc(mpath(src,tgt))+'" fill="none" stroke="'+esc(c)+'" stroke-width="2" opacity="0.85" marker-end="url(#'+id+')"/>');
'" stroke-width="2" opacity="0.85" marker-end="url(#'+id+')"/>');
}); });
// Realm (platform) nodes // Realm nodes
layout.realms.forEach(function(n){ layout.realms.forEach(function(n){
p.push('<rect x="'+n.x+'" y="'+n.y+'" width="'+n.w+'" height="'+n.h+ p.push('<rect x="'+n.x+'" y="'+n.y+'" width="'+n.w+'" height="'+n.h+'" rx="6" fill="#0f172a" stroke="#475569" stroke-width="1.5" stroke-dasharray="5,3"/>');
'" rx="6" fill="#0f172a" stroke="#475569" stroke-width="1.5" stroke-dasharray="5,3"/>'); p.push('<text x="'+n.cx+'" y="'+(n.y+13)+'" text-anchor="middle" font-family="sans-serif" font-size="9" font-weight="600" fill="#64748b">ПЛАТФОРМА</text>');
p.push('<text x="'+n.cx+'" y="'+(n.y+13)+'" text-anchor="middle" '+ p.push('<text x="'+n.cx+'" y="'+(n.y+n.h-10)+'" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="#e2e8f0">'+esc(trunc(n.label,20))+'</text>');
'font-family="sans-serif" font-size="9" font-weight="600" fill="#64748b">ПЛАТФОРМА</text>');
p.push('<text x="'+n.cx+'" y="'+(n.y+n.h-10)+'" text-anchor="middle" '+
'font-family="sans-serif" font-size="12" font-weight="700" fill="#e2e8f0">'+
esc(trunc(n.label,20))+'</text>');
}); });
// Instance nodes (infra row + grid rows) // Instance nodes
layout.infra.concat(layout.others).forEach(function(n){ layout.infra.concat(layout.others).forEach(function(n){
var st=svcStyle(n.inst.svc), dot=statusDot(n.inst.explainedStatus); var st=svcStyle(n.inst.svc), dot=statusDot(n.inst.explainedStatus);
var lbl=trunc(n.inst.displayName||n.inst.instanceUid,18); var lbl=trunc(n.inst.displayName||n.inst.instanceUid,18);
var svc=trunc(n.inst.svc||"",22); var svc=trunc(n.inst.svc||"",22);
p.push('<rect x="'+n.x+'" y="'+n.y+'" width="'+n.w+'" height="'+n.h+ p.push('<rect x="'+n.x+'" y="'+n.y+'" width="'+n.w+'" height="'+n.h+'" rx="6" fill="'+esc(st.bg)+'" stroke="'+esc(st.border)+'" stroke-width="1.5"/>');
'" rx="6" fill="'+esc(st.bg)+'" stroke="'+esc(st.border)+'" stroke-width="1.5"/>');
p.push('<circle cx="'+(n.x+12)+'" cy="'+(n.y+n.h/2)+'" r="4" fill="'+esc(dot)+'"/>'); p.push('<circle cx="'+(n.x+12)+'" cy="'+(n.y+n.h/2)+'" r="4" fill="'+esc(dot)+'"/>');
p.push('<text x="'+(n.x+24)+'" y="'+(n.y+n.h/2-5)+ p.push('<text x="'+(n.x+24)+'" y="'+(n.y+n.h/2-5)+'" font-family="sans-serif" font-size="12" font-weight="600" fill="#f1f5f9">'+esc(lbl)+'</text>');
'" font-family="sans-serif" font-size="12" font-weight="600" fill="#f1f5f9">'+esc(lbl)+'</text>'); p.push('<text x="'+(n.x+24)+'" y="'+(n.y+n.h/2+11)+'" font-family="sans-serif" font-size="10" fill="'+esc(st.border)+'">'+esc(svc)+'</text>');
p.push('<text x="'+(n.x+24)+'" y="'+(n.y+n.h/2+11)+
'" font-family="sans-serif" font-size="10" fill="'+esc(st.border)+'">'+esc(svc)+'</text>');
}); });
p.push('</svg>'); p.push('</svg>');
return p.join("\n"); return p.join("\n");
} }
// ── Panzoom ────────────────────────────────────────────────────────────── // ── Panzoom ──────────────────────────────────────────────────────────────
function attachPanzoom(){ function attachPanzoom(){
var svg=document.getElementById("graph-svg"); var svg=document.getElementById("graph-svg");
if(!svg||typeof panzoom==="undefined")return; if(!svg||typeof panzoom==="undefined") return;
if(svg._pz){try{svg._pz.dispose();}catch(e){}} if(svg._pz){try{svg._pz.dispose();}catch(e){}}
svg._pz=panzoom(svg,{maxZoom:4,minZoom:0.1,smoothScroll:false}); svg._pz=panzoom(svg,{maxZoom:4,minZoom:0.1,smoothScroll:false});
} }
// ── Build ──────────────────────────────────────────────────────────────── // ── Build ────────────────────────────────────────────────────────────────
function buildGraph(){ function buildGraph(){
if(building)return; if(building) return;
building=true; graphCache=null; building=true; graphCache=null;
var canvas=getCanvas(); if(!canvas)return; var canvas=getCanvas(); if(!canvas) return;
canvas.innerHTML=""; setStatus("Загрузка\u2026"); canvas.innerHTML=""; setStatus("Загрузка\u2026");
var instances=(typeof window._getVisibleInstances==="function")?window._getVisibleInstances() var instances=(typeof window._getVisibleInstances==="function") ? window._getVisibleInstances()
:(typeof window._getInstances==="function"?window._getInstances():[]); :(typeof window._getInstances==="function" ? window._getInstances() : []);
if(!instances||!instances.length){ if(!instances||!instances.length){
setStatus("Нет данных. Обновите таблицу и откройте схему снова."); setStatus("Нет данных. Обновите таблицу и откройте схему снова.");
@@ -216,45 +195,42 @@
} }
var uid=instances[idx].instanceUid, srcSvc=instances[idx].svc||""; var uid=instances[idx].instanceUid, srcSvc=instances[idx].svc||"";
fetch("/dashboard/api/instances/"+uid,{ fetch("/dashboard/api/instances/"+uid,{
headers:{ headers:{"X-Deck-Token":localStorage.getItem("deck_token")||"","X-Deck-Env":localStorage.getItem("deck_env")||"test"}
"X-Deck-Token":localStorage.getItem("deck_token")||"",
"X-Deck-Env":localStorage.getItem("deck_env")||"test"
}
}) })
.then(function(r){return r.ok?r.json():null;}) .then(function(r){ return r.ok?r.json():null; })
.then(function(data){ .then(function(data){
done++; setStatus("Зависимости: "+done+"/"+total); done++; setStatus("Зависимости: "+done+"/"+total);
if(data){ if(data){
var inst=data.instance||data; var inst=data.instance||data;
[].concat(inst.dependencies||[],inst.deps||[]).forEach(function(d){ [].concat(inst.dependencies||[],inst.deps||[]).forEach(function(d){
var to=d.uid||d.instanceUid||d.dependencyUid; var to=d.uid||d.instanceUid||d.dependencyUid;
if(to)edges.push({from:uid,to:to,color:svcColor(d.svc||srcSvc)}); if(to) edges.push({from:uid,to:to,color:svcColor(d.svc||srcSvc)});
}); });
[].concat(inst.dependentInstances||[],inst.dependsOn||[]).forEach(function(d){ [].concat(inst.dependentInstances||[],inst.dependsOn||[]).forEach(function(d){
var from=d.uid||d.instanceUid||d.dependencyUid; var from=d.uid||d.instanceUid||d.dependencyUid;
if(from)edges.push({from:from,to:uid,color:svcColor(d.svc||srcSvc)}); if(from) edges.push({from:from,to:uid,color:svcColor(d.svc||srcSvc)});
}); });
} }
setTimeout(function(){loadNext(idx+1);},20); setTimeout(function(){loadNext(idx+1);},20);
}) })
.catch(function(){done++;setTimeout(function(){loadNext(idx+1);},20);}); .catch(function(){ done++; setTimeout(function(){loadNext(idx+1);},20); });
} }
loadNext(0); loadNext(0);
} }
// ── Init ────────────────────────────────────────────────────────────────── // ── Init ────────────────────────────────────────────────────────────────
function init(){ function init(){
var a=document.getElementById("btn-graph-open"); var btnOpen=document.getElementById("btn-graph-open");
var b=document.getElementById("btn-graph-close"); var btnClose=document.getElementById("btn-graph-close");
var c=document.getElementById("btn-graph-refresh"); var btnRefresh=document.getElementById("btn-graph-refresh");
var ov=document.getElementById("graph-overlay"); var overlay=document.getElementById("graph-overlay");
if(a)a.addEventListener("click",openGraph); if(btnOpen) btnOpen.addEventListener("click",openGraph);
if(b)b.addEventListener("click",closeGraph); if(btnClose) btnClose.addEventListener("click",closeGraph);
if(c)c.addEventListener("click",refreshGraph); if(btnRefresh) btnRefresh.addEventListener("click",refreshGraph);
if(ov)ov.addEventListener("click",function(e){if(e.target===ov)closeGraph();}); if(overlay) overlay.addEventListener("click",function(e){if(e.target===overlay)closeGraph();});
document.addEventListener("keydown",function(e){if(e.key==="Escape")closeGraph();}); document.addEventListener("keydown",function(e){if(e.key==="Escape")closeGraph();});
} }
if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",init);}else{init();} if(document.readyState==="loading"){ document.addEventListener("DOMContentLoaded",init); } else { init(); }
window.graphModule={open:openGraph,close:closeGraph,refresh:refreshGraph}; window.graphModule={open:openGraph,close:closeGraph,refresh:refreshGraph};
})(); })();