diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 6813035..83f4886 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -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 diff --git a/static/graph.js b/static/graph.js index 97b46d5..4faeb91 100644 --- a/static/graph.js +++ b/static/graph.js @@ -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=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=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,"&").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}; } - 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, "&").replace(//g, ">").replace(/"/g, """); - } + // ── 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 = [ - '', - '', - '', - '', - '', - '' + var p=[ + '', + '' ]; + Object.keys(colors).forEach(function(c){ + var id="arr"+c.replace(/[^a-zA-Z0-9]/g,""); + p.push(''+ + ''); + }); + p.push(''); - // 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( - '' - ); + // 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(''); }); - // 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( - '' - ); - parts.push( - '' - ); - parts.push( - '' + esc(lbl) + '' - ); - parts.push( - '' + esc(svc) + '' - ); + // 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(''); }); - parts.push(''); - return parts.join("\n"); + // 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 }); + // ── 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}; })();