261 lines
12 KiB
JavaScript
261 lines
12 KiB
JavaScript
// graph.js — v9.50: platform row | infra row | grid | Manhattan edges
|
|
(function () {
|
|
"use strict";
|
|
|
|
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"];
|
|
function isInfra(svc){
|
|
var s=(svc||"").toLowerCase();
|
|
for(var i=0;i<INFRA.length;i++){if(s.indexOf(INFRA[i])>=0)return true;}
|
|
return false;
|
|
}
|
|
|
|
var SVC_STYLE = [
|
|
["s3","#1d4e6e","#38bdf8"],["postgresql","#1a3d2b","#4ade80"],["postgres","#1a3d2b","#4ade80"],
|
|
["redis","#4a1c1c","#f87171"],["vcloud","#1a2e4a","#60a5fa"],["штурвал","#1a2e4a","#60a5fa"],
|
|
["cloud direct","#0c2340","#93c5fd"],["rabbitmq","#3b2000","#fb923c"],
|
|
["nodejs","#1a3b20","#86efac"],["flask","#1a3b20","#86efac"],["lucee","#2d1b4a","#c084fc"],
|
|
["mongodb","#1a3a1a","#4ade80"],["grafana","#2d1000","#fdba74"],["pgadmin","#1a3d2b","#6ee7b7"],
|
|
["edge","#1e293b","#94a3b8"],["vdc","#1e293b","#94a3b8"],["vapp","#1e293b","#94a3b8"],
|
|
["dns","#1a2040","#818cf8"],["nodered","#3b1500","#fdba74"],["http","#1a2e1a","#bbf7d0"]
|
|
];
|
|
function svcStyle(svc){
|
|
var s=(svc||"").toLowerCase();
|
|
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]};
|
|
}
|
|
return{bg:"#1e293b",border:"#475569"};
|
|
}
|
|
function svcColor(svc){return svcStyle(svc).border;}
|
|
function statusDot(s){
|
|
var m={running:"#56d364",suspended:"#e3b341",deleted:"#f85149",
|
|
deleting:"#f85149",pending:"#58a6ff",creating:"#58a6ff"};
|
|
return m[(s||"").toLowerCase()]||"#6e7681";
|
|
}
|
|
function esc(v){
|
|
return String(v==null?"":v)
|
|
.replace(/&/g,"&").replace(/</g,"<")
|
|
.replace(/>/g,">").replace(/"/g,""");
|
|
}
|
|
function trunc(s,n){return(!s)?"":s.length>n?s.slice(0,n-1)+"\u2026":s;}
|
|
|
|
// ── State ─────────────────────────────────────────────────────────────────
|
|
var graphCache=null, building=false;
|
|
function getOverlay(){return document.getElementById("graph-overlay");}
|
|
function getCanvas() {return document.getElementById("graph-canvas");}
|
|
function getStatus() {return document.getElementById("graph-status");}
|
|
function setStatus(m){var el=getStatus();if(el)el.textContent=m;}
|
|
|
|
function openGraph(){
|
|
var ov=getOverlay();if(!ov)return;
|
|
ov.style.display="flex";
|
|
if(graphCache){getCanvas().innerHTML=graphCache;attachPanzoom();}
|
|
else if(!building)buildGraph();
|
|
}
|
|
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();}
|
|
|
|
// ── Layout ────────────────────────────────────────────────────────────────
|
|
function computeLayout(instances){
|
|
var realms=[],seen={};
|
|
instances.forEach(function(i){
|
|
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 canvasW = PX*2 + COLS*NW + (COLS-1)*GX;
|
|
|
|
// Row 0: platforms — spread evenly across full width
|
|
var realmNodes = realms.map(function(r,i){
|
|
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};
|
|
});
|
|
|
|
// Row 1: infra (S3, Cloud Director, Edge) — spread evenly
|
|
var row1Y = PY+RH+GY;
|
|
var infraNodes = infra.map(function(inst,i){
|
|
var sp=canvasW/Math.max(infra.length,1), cx=sp*i+sp/2;
|
|
return{uid:inst.instanceUid,inst:inst,x:cx-NW/2,y:row1Y,w:NW,h:NH,cx:cx,cy:row1Y+NH/2};
|
|
});
|
|
|
|
// Row 2+: other instances — grid, max COLS per row
|
|
var row2Y = row1Y+(infra.length>0?NH+GY:0);
|
|
var otherNodes = others.map(function(inst,i){
|
|
var col=i%COLS, row=Math.floor(i/COLS);
|
|
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{realms:realmNodes,infra:infraNodes,others:otherNodes};
|
|
}
|
|
|
|
// ── Manhattan (orthogonal) path ───────────────────────────────────────────
|
|
function mpath(src,tgt){
|
|
var x1=src.cx, y1=src.y+src.h; // source bottom-center
|
|
var x2=tgt.cx, y2=tgt.y; // target top-center
|
|
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;
|
|
return"M"+x1+","+y1+" L"+x1+","+dip+" L"+x2+","+dip+" L"+x2+","+y2;
|
|
}
|
|
var my=(y1+y2)/2;
|
|
return"M"+x1+","+y1+" L"+x1+","+my+" L"+x2+","+my+" L"+x2+","+y2;
|
|
}
|
|
|
|
// ── SVG render ────────────────────────────────────────────────────────────
|
|
function renderSVG(instances, depEdges){
|
|
var layout=computeLayout(instances);
|
|
var nodeMap={};
|
|
layout.realms.forEach(function(n){nodeMap[n.id]=n;});
|
|
layout.infra.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 maxX=0, maxY=0;
|
|
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;
|
|
});
|
|
var W=maxX+PX, H=maxY+PY;
|
|
|
|
// Collect unique edge colors for arrowhead markers
|
|
var colors={"#334155":1};
|
|
depEdges.forEach(function(e){if(e.color)colors[e.color]=1;});
|
|
|
|
var p=[
|
|
'<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){
|
|
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">'+
|
|
'<path d="M0,0 L0,6 L8,3 z" fill="'+esc(c)+'"/></marker>');
|
|
});
|
|
p.push('</defs>');
|
|
|
|
// Platform → instance lines (thin dashed, gray)
|
|
instances.forEach(function(inst){
|
|
var r=inst.resourceRealm; if(!r)return;
|
|
var src=nodeMap["realm:"+r], tgt=nodeMap[inst.instanceUid];
|
|
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"/>');
|
|
});
|
|
|
|
// Dependency edges (colored Manhattan lines with arrow)
|
|
depEdges.forEach(function(e){
|
|
var src=nodeMap[e.from], tgt=nodeMap[e.to];
|
|
if(!src||!tgt)return;
|
|
var c=e.color||"#475569";
|
|
var id="arr"+c.replace(/[^a-zA-Z0-9]/g,"");
|
|
p.push('<path d="'+esc(mpath(src,tgt))+'" fill="none" stroke="'+esc(c)+
|
|
'" stroke-width="2" opacity="0.85" marker-end="url(#'+id+')"/>');
|
|
});
|
|
|
|
// Realm (platform) nodes
|
|
layout.realms.forEach(function(n){
|
|
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"/>');
|
|
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+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)
|
|
layout.infra.concat(layout.others).forEach(function(n){
|
|
var st=svcStyle(n.inst.svc), dot=statusDot(n.inst.explainedStatus);
|
|
var lbl=trunc(n.inst.displayName||n.inst.instanceUid,18);
|
|
var svc=trunc(n.inst.svc||"",22);
|
|
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"/>');
|
|
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)+
|
|
'" 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('</svg>');
|
|
return p.join("\n");
|
|
}
|
|
|
|
// ── Panzoom ───────────────────────────────────────────────────────────────
|
|
function attachPanzoom(){
|
|
var svg=document.getElementById("graph-svg");
|
|
if(!svg||typeof panzoom==="undefined")return;
|
|
if(svg._pz){try{svg._pz.dispose();}catch(e){}}
|
|
svg._pz=panzoom(svg,{maxZoom:4,minZoom:0.1,smoothScroll:false});
|
|
}
|
|
|
|
// ── Build ─────────────────────────────────────────────────────────────────
|
|
function buildGraph(){
|
|
if(building)return;
|
|
building=true; graphCache=null;
|
|
var canvas=getCanvas(); if(!canvas)return;
|
|
canvas.innerHTML=""; setStatus("Загрузка\u2026");
|
|
|
|
var instances=(typeof window._getVisibleInstances==="function")?window._getVisibleInstances()
|
|
:(typeof window._getInstances==="function"?window._getInstances():[]);
|
|
|
|
if(!instances||!instances.length){
|
|
setStatus("Нет данных. Обновите таблицу и откройте схему снова.");
|
|
building=false; return;
|
|
}
|
|
|
|
canvas.innerHTML=renderSVG(instances,[]); attachPanzoom();
|
|
setStatus("Загрузка зависимостей\u2026");
|
|
|
|
var edges=[], total=instances.length, done=0;
|
|
|
|
function loadNext(idx){
|
|
if(idx>=instances.length){
|
|
canvas.innerHTML=renderSVG(instances,edges); attachPanzoom();
|
|
graphCache=canvas.innerHTML; building=false; setStatus(""); return;
|
|
}
|
|
var uid=instances[idx].instanceUid, srcSvc=instances[idx].svc||"";
|
|
fetch("/dashboard/api/instances/"+uid,{
|
|
headers:{
|
|
"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(data){
|
|
done++; setStatus("Зависимости: "+done+"/"+total);
|
|
if(data){
|
|
var inst=data.instance||data;
|
|
[].concat(inst.dependencies||[],inst.deps||[]).forEach(function(d){
|
|
var to=d.uid||d.instanceUid||d.dependencyUid;
|
|
if(to)edges.push({from:uid,to:to,color:svcColor(d.svc||srcSvc)});
|
|
});
|
|
[].concat(inst.dependentInstances||[],inst.dependsOn||[]).forEach(function(d){
|
|
var from=d.uid||d.instanceUid||d.dependencyUid;
|
|
if(from)edges.push({from:from,to:uid,color:svcColor(d.svc||srcSvc)});
|
|
});
|
|
}
|
|
setTimeout(function(){loadNext(idx+1);},20);
|
|
})
|
|
.catch(function(){done++;setTimeout(function(){loadNext(idx+1);},20);});
|
|
}
|
|
loadNext(0);
|
|
}
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────────────
|
|
function init(){
|
|
var a=document.getElementById("btn-graph-open");
|
|
var b=document.getElementById("btn-graph-close");
|
|
var c=document.getElementById("btn-graph-refresh");
|
|
var ov=document.getElementById("graph-overlay");
|
|
if(a)a.addEventListener("click",openGraph);
|
|
if(b)b.addEventListener("click",closeGraph);
|
|
if(c)c.addEventListener("click",refreshGraph);
|
|
if(ov)ov.addEventListener("click",function(e){if(e.target===ov)closeGraph();});
|
|
document.addEventListener("keydown",function(e){if(e.key==="Escape")closeGraph();});
|
|
}
|
|
|
|
if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",init);}else{init();}
|
|
window.graphModule={open:openGraph,close:closeGraph,refresh:refreshGraph};
|
|
})();
|