v9.50: platforms row0, infra row1, grid row2+, Manhattan colored edges

This commit is contained in:
Naeel
2026-04-17 10:26:55 +03:00
parent da06f64baf
commit 256e00181b
2 changed files with 217 additions and 222 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ spec:
spec:
containers:
- name: cloud-dashboard
image: naeel/cloud-dashboard:v9.49
image: naeel/cloud-dashboard:v9.50
imagePullPolicy: Always
ports:
- containerPort: 8080
+216 -221
View File
@@ -1,265 +1,260 @@
// graph.js — Simple grid layout, max 5 columns, arrows between deps
// graph.js — v9.50: platform row | infra row | grid | Manhattan edges
(function () {
"use strict";
var COLS = 5;
var NODE_W = 170;
var NODE_H = 58;
var GAP_X = 30;
var GAP_Y = 80;
var PAD = 40;
var COLS=5, NW=170, NH=56, RW=160, RH=44, GX=30, GY=70, PX=40, PY=30;
var SVC_COLORS = [
["s3", "#1d4e6e", "#38bdf8"],
["postgresql", "#1a3d2b", "#4ade80"],
["postgres", "#1a3d2b", "#4ade80"],
["redis", "#4a1c1c", "#f87171"],
["kubernetes", "#1a2e4a", "#60a5fa"],
["штурвал", "#1a2e4a", "#60a5fa"],
["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"],
["dns", "#1a2040", "#818cf8"],
["nodered", "#3b1500", "#fdba74"]
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_COLORS.length; i++) {
if (s.indexOf(SVC_COLORS[i][0]) >= 0)
return { bg: SVC_COLORS[i][1], border: SVC_COLORS[i][2] };
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" };
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,"&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 ─────────────────────────────────────────────────────────────────
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};
}
function statusColor(status) {
var m = { running: "#56d364", suspended: "#e3b341", deleted: "#f85149",
deleting: "#f85149", pending: "#58a6ff", creating: "#58a6ff" };
return m[(status || "").toLowerCase()] || "#6e7681";
// ── 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;
}
function esc(v) {
return String(v == null ? "" : v)
.replace(/&/g, "&amp;").replace(/</g, "&lt;")
.replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
// ── 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;});
function trunc(s, n) {
if (!s) return "";
return s.length > n ? s.slice(0, n - 1) + "…" : s;
}
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;
// ── State ────────────────────────────────────────────────────────────────
var graphCache = null;
var building = false;
// Collect unique edge colors for arrowhead markers
var colors={"#334155":1};
depEdges.forEach(function(e){if(e.color)colors[e.color]=1;});
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();
}
// ── Grid layout ──────────────────────────────────────────────────────────
function gridPos(idx) {
var col = idx % COLS;
var row = Math.floor(idx / COLS);
return {
x: PAD + col * (NODE_W + GAP_X),
y: PAD + row * (NODE_H + GAP_Y),
cx: PAD + col * (NODE_W + GAP_X) + NODE_W / 2,
cy: PAD + row * (NODE_H + GAP_Y) + NODE_H / 2
};
}
// ── SVG render ───────────────────────────────────────────────────────────
function renderSVG(instances, edges) {
var n = instances.length;
var cols = Math.min(n, COLS);
var rows = Math.ceil(n / COLS);
var W = PAD * 2 + cols * NODE_W + (cols - 1) * GAP_X;
var H = PAD * 2 + rows * NODE_H + (rows - 1) * GAP_Y;
// index map: uid → index
var idx = {};
instances.forEach(function (inst, i) { idx[inst.instanceUid] = i; });
var parts = [
'<svg id="graph-svg" xmlns="http://www.w3.org/2000/svg" width="' + W + '" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '">',
'<defs>',
'<marker id="arr" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto">',
'<path d="M0,0 L0,6 L8,3 z" fill="#64748b"/>',
'</marker>',
'</defs>'
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>');
// Draw arrows first (under nodes)
edges.forEach(function (e) {
var fi = idx[e.from], ti = idx[e.to];
if (fi === undefined || ti === undefined) return;
var fp = gridPos(fi), tp = gridPos(ti);
// line from bottom-center of source to top-center of target (or center-to-center)
parts.push(
'<line x1="' + fp.cx + '" y1="' + fp.cy + '" x2="' + tp.cx + '" y2="' + tp.cy + '"' +
' stroke="#64748b" stroke-width="1.5" opacity="0.7" marker-end="url(#arr)"/>'
);
// 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"/>');
});
// Draw nodes
instances.forEach(function (inst, i) {
var p = gridPos(i);
var st = svcStyle(inst.svc);
var dot = statusColor(inst.explainedStatus);
var lbl = trunc(inst.displayName || inst.instanceUid, 18);
var svc = trunc(inst.svc || "", 24);
parts.push(
'<rect x="' + p.x + '" y="' + p.y + '" width="' + NODE_W + '" height="' + NODE_H + '"' +
' rx="6" fill="' + esc(st.bg) + '" stroke="' + esc(st.border) + '" stroke-width="1.5"/>'
);
parts.push(
'<circle cx="' + (p.x + 12) + '" cy="' + (p.y + NODE_H / 2) + '" r="4" fill="' + esc(dot) + '"/>'
);
parts.push(
'<text x="' + (p.x + 24) + '" y="' + (p.y + NODE_H / 2 - 6) + '"' +
' font-family="sans-serif" font-size="12" font-weight="600" fill="#f1f5f9">' + esc(lbl) + '</text>'
);
parts.push(
'<text x="' + (p.x + 24) + '" y="' + (p.y + NODE_H / 2 + 11) + '"' +
' font-family="sans-serif" font-size="10" fill="' + esc(st.border) + '">' + esc(svc) + '</text>'
);
// 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+')"/>');
});
parts.push('</svg>');
return parts.join("\n");
// 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 });
// ── 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;
// ── Build ────────────────────────────────────────────────────────────────
function buildGraph(){
if(building)return;
building=true; graphCache=null;
var canvas=getCanvas(); if(!canvas)return;
canvas.innerHTML=""; setStatus("Загрузка\u2026");
var canvas = getCanvas();
if (!canvas) return;
canvas.innerHTML = "";
setStatus("Загрузка…");
var instances=(typeof window._getVisibleInstances==="function")?window._getVisibleInstances()
:(typeof window._getInstances==="function"?window._getInstances():[]);
var instances = (typeof window._getInstances === "function") ? window._getInstances() : [];
if (!instances || !instances.length) {
if(!instances||!instances.length){
setStatus("Нет данных. Обновите таблицу и откройте схему снова.");
building = false;
return;
building=false; return;
}
// Render grid immediately without edges
canvas.innerHTML = renderSVG(instances, []);
attachPanzoom();
setStatus("Загрузка зависимостей…");
canvas.innerHTML=renderSVG(instances,[]); attachPanzoom();
setStatus("Загрузка зависимостей\u2026");
// Load all deps, then re-render once
var edges = [];
var total = instances.length;
var done = 0;
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;
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;
fetch("/dashboard/api/instances/" + uid, {
headers: {
"X-Deck-Token": localStorage.getItem("deck_token") || "",
"X-Deck-Env": localStorage.getItem("deck_env") || "test"
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;
var deps = [].concat(inst.dependencies || [], inst.deps || []);
deps.forEach(function (d) {
var to = d.uid || d.instanceUid || d.dependencyUid;
if (to) edges.push({ from: uid, to: to });
.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)});
});
var dInst = [].concat(inst.dependentInstances || [], inst.dependsOn || []);
dInst.forEach(function (d) {
var from = d.uid || d.instanceUid || d.dependencyUid;
if (from) edges.push({ from: from, to: uid });
[].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);
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);
}
// ── Init ─────────────────────────────────────────────────────────────────
function init() {
var btnOpen = document.getElementById("btn-graph-open");
var btnClose = document.getElementById("btn-graph-close");
var btnRefresh = document.getElementById("btn-graph-refresh");
var overlay = document.getElementById("graph-overlay");
if (btnOpen) btnOpen.addEventListener("click", openGraph);
if (btnClose) btnClose.addEventListener("click", closeGraph);
if (btnRefresh) btnRefresh.addEventListener("click", refreshGraph);
if (overlay) overlay.addEventListener("click", function (e) {
if (e.target === overlay) closeGraph();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeGraph();
});
// ── 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 };
if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",init);}else{init();}
window.graphModule={open:openGraph,close:closeGraph,refresh:refreshGraph};
})();