// 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=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=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,"""); } 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=[ '', '' ]; Object.keys(colors).forEach(function(c){ var id="arr"+c.replace(/[^a-zA-Z0-9]/g,""); p.push(''+ ''); }); p.push(''); // 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(''); }); // 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(''); }); // Realm (platform) nodes layout.realms.forEach(function(n){ p.push(''); p.push('ПЛАТФОРМА'); p.push(''+ esc(trunc(n.label,20))+''); }); // 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(''); p.push(''); p.push(''+esc(lbl)+''); p.push(''+esc(svc)+''); }); p.push(''); 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}; })();