diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index e629b0c..6813035 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -15,7 +15,7 @@ spec: spec: containers: - name: cloud-dashboard - image: naeel/cloud-dashboard:v9.48 + image: naeel/cloud-dashboard:v9.49 imagePullPolicy: Always ports: - containerPort: 8080 diff --git a/static/graph.js b/static/graph.js index 09dab4a..97b46d5 100644 --- a/static/graph.js +++ b/static/graph.js @@ -1,18 +1,14 @@ -// graph.js — Progressive dependency graph visualization -// Uses dagre (layout) + panzoom (pan/zoom) from CDN -// Appended to index.html as separate script — reads globals: allInstances, rawDetail, getToken, getDeckEnv +// graph.js — Simple grid layout, max 5 columns, arrows between deps (function () { "use strict"; - // ── Constants ──────────────────────────────────────────────────────────── - var NODE_W = 160; - var NODE_H = 52; - var REALM_W = 180; - var REALM_H = 44; - var RANK_SEP = 90; - var NODE_SEP = 36; + var COLS = 5; + var NODE_W = 170; + var NODE_H = 58; + var GAP_X = 30; + var GAP_Y = 80; + var PAD = 40; - // Color palette per svc keyword (substring match, case-insensitive) var SVC_COLORS = [ ["s3", "#1d4e6e", "#38bdf8"], ["postgresql", "#1a3d2b", "#4ade80"], @@ -29,264 +25,147 @@ ["pgadmin", "#1a3d2b", "#6ee7b7"], ["edge", "#1e293b", "#94a3b8"], ["vdc", "#1e293b", "#94a3b8"], - ["vapp", "#1e293b", "#94a3b8"], - ["cloud direct","#0c2340", "#93c5fd"], ["dns", "#1a2040", "#818cf8"], - ["ip", "#1e293b", "#cbd5e1"], - ["http", "#1a2e1a", "#bbf7d0"], ["nodered", "#3b1500", "#fdba74"] ]; - // Edge color by source svc keyword - var EDGE_COLORS = [ - ["s3", "#38bdf8"], - ["postgresql", "#4ade80"], - ["postgres", "#4ade80"], - ["redis", "#f87171"], - ["kubernetes", "#60a5fa"], - ["штурвал", "#60a5fa"], - ["rabbitmq", "#fb923c"], - ["nodejs", "#86efac"], - ["flask", "#86efac"], - ["lucee", "#c084fc"], - ["dns", "#818cf8"], - ["edge", "#94a3b8"] - ]; - - function svcStyle(svcStr) { - var s = (svcStr || "").toLowerCase(); + function svcStyle(svc) { + var s = (svc || "").toLowerCase(); for (var i = 0; i < SVC_COLORS.length; i++) { - if (s.indexOf(SVC_COLORS[i][0]) >= 0) { + if (s.indexOf(SVC_COLORS[i][0]) >= 0) return { bg: SVC_COLORS[i][1], border: SVC_COLORS[i][2] }; - } } return { bg: "#1e293b", border: "#475569" }; } - function edgeColor(svcStr) { - var s = (svcStr || "").toLowerCase(); - for (var i = 0; i < EDGE_COLORS.length; i++) { - if (s.indexOf(EDGE_COLORS[i][0]) >= 0) return EDGE_COLORS[i][1]; - } - return "#475569"; + function statusColor(status) { + var m = { running: "#56d364", suspended: "#e3b341", deleted: "#f85149", + deleting: "#f85149", pending: "#58a6ff", creating: "#58a6ff" }; + return m[(status || "").toLowerCase()] || "#6e7681"; + } + + function esc(v) { + return String(v == null ? "" : v) + .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + + function trunc(s, n) { + if (!s) return ""; + return s.length > n ? s.slice(0, n - 1) + "…" : s; } // ── State ──────────────────────────────────────────────────────────────── - var graphCache = null; // built SVG string, null = not built yet - var building = false; // loading in progress flag + var graphCache = null; + var building = false; - // ── DOM helpers ────────────────────────────────────────────────────────── 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; } - // ── Open / Close ───────────────────────────────────────────────────────── function openGraph() { - var overlay = getOverlay(); - if (!overlay) { console.error("[graph] #graph-overlay not found"); return; } - overlay.style.display = "flex"; - if (graphCache) { - showCached(); - } else if (!building) { - buildGraph(); - } + var ov = getOverlay(); + if (!ov) return; + ov.style.display = "flex"; + if (graphCache) { getCanvas().innerHTML = graphCache; attachPanzoom(); } + else if (!building) buildGraph(); } function closeGraph() { - var overlay = getOverlay(); - if (overlay) overlay.style.display = "none"; + var ov = getOverlay(); + if (ov) ov.style.display = "none"; } function refreshGraph() { - graphCache = null; - building = false; - var canvas = getCanvas(); - if (canvas) canvas.innerHTML = ""; + graphCache = null; building = false; + var c = getCanvas(); if (c) c.innerHTML = ""; buildGraph(); } - function showCached() { - var canvas = getCanvas(); - if (!canvas) return; - canvas.innerHTML = graphCache; - attachPanzoom(); - setStatus(""); + // ── 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 + }; } - function setStatus(msg) { - var el = getStatus(); - if (el) el.textContent = msg; - } + // ── 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; - // ── Layout helpers ─────────────────────────────────────────────────────── + // index map: uid → index + var idx = {}; + instances.forEach(function (inst, i) { idx[inst.instanceUid] = i; }); - // Simple dagre-based layout using the global dagre object from CDN - function layoutGraph(nodes, edges) { - var g = new dagre.graphlib.Graph(); - g.setGraph({ rankdir: "TB", ranksep: RANK_SEP, nodesep: NODE_SEP, marginx: 40, marginy: 40 }); - g.setDefaultEdgeLabel(function () { return {}; }); - - nodes.forEach(function (n) { - g.setNode(n.id, { width: n.w || NODE_W, height: n.h || NODE_H, label: n.id }); - }); - edges.forEach(function (e) { - if (g.hasNode(e.from) && g.hasNode(e.to)) g.setEdge(e.from, e.to); - }); - - dagre.layout(g); - return g; - } - - // ── SVG rendering ──────────────────────────────────────────────────────── - - function escX(v) { - return String(v == null ? "" : v) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); - } - - function truncate(str, maxLen) { - if (!str) return ""; - return str.length > maxLen ? str.slice(0, maxLen - 1) + "…" : str; - } - - function renderArrowMarker(color, id) { - return '' + - '' + - ''; - } - - function pointsOnEdge(g, fromId, toId) { - var edge = g.edge(fromId, toId); - if (!edge || !edge.points || edge.points.length < 2) { - // fallback: straight line between node centers - var nf = g.node(fromId), nt = g.node(toId); - if (!nf || !nt) return null; - return [{ x: nf.x, y: nf.y + (nf.height || NODE_H) / 2 }, - { x: nt.x, y: nt.y - (nt.height || NODE_H) / 2 }]; - } - return edge.points; - } - - function polyline(pts) { - return pts.map(function (p) { return p.x + "," + p.y; }).join(" "); - } - - function renderSVG(g, nodesMeta) { - var gw = g.graph().width || 800; - var gh = g.graph().height || 600; - var svgW = gw + 80; - var svgH = gh + 80; - - // Collect unique edge colors for markers - var markerColors = {}; - nodesMeta.edges.forEach(function (e) { - var c = e.color || "#475569"; - markerColors[c] = true; - }); - - var defs = ''; - Object.keys(markerColors).forEach(function (c) { - var mid = "arr-" + c.replace(/[^a-zA-Z0-9]/g, ""); - defs += renderArrowMarker(c, mid); - }); - defs += ''; - - var svgParts = [ - '', - defs + var parts = [ + '', + '', + '', + '', + '', + '' ]; - // Draw edges first (under nodes) - nodesMeta.edges.forEach(function (e) { - var pts = pointsOnEdge(g, e.from, e.to); - if (!pts) return; - var c = e.color || "#475569"; - var mid = "arr-" + c.replace(/[^a-zA-Z0-9]/g, ""); - svgParts.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( + '' ); }); // Draw nodes - g.nodes().forEach(function (id) { - var pos = g.node(id); - if (!pos) return; - var meta = nodesMeta.map[id]; - if (!meta) return; - var x = pos.x - pos.width / 2; - var y = pos.y - pos.height / 2; - var w = pos.width; - var h = pos.height; + 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); - if (meta.type === "realm") { - // Platform node — dashed border, distinct style - svgParts.push( - '' - ); - svgParts.push( - 'ПЛАТФОРМА' - ); - svgParts.push( - '' + escX(truncate(meta.label, 22)) + '' - ); - } else { - // Instance node - var st = svcStyle(meta.svc); - svgParts.push( - '' - ); - // Status dot - var dotColor = meta.statusColor || "#6e7681"; - svgParts.push( - '' - ); - // Name - svgParts.push( - '' + escX(truncate(meta.label, 18)) + '' - ); - // Svc - svgParts.push( - '' + escX(truncate(meta.svc, 22)) + '' - ); - } + parts.push( + '' + ); + parts.push( + '' + ); + parts.push( + '' + esc(lbl) + '' + ); + parts.push( + '' + esc(svc) + '' + ); }); - svgParts.push(''); - return svgParts.join("\n"); + parts.push(''); + return parts.join("\n"); } - // ── Status color helper ────────────────────────────────────────────────── - function statusDotColor(status) { - var m = { - running: "#56d364", - suspended: "#e3b341", - deleted: "#f85149", - deleting: "#f85149", - pending: "#58a6ff", - creating: "#58a6ff", - "not created": "#6e7681" - }; - return m[(status || "").toLowerCase()] || "#6e7681"; + // ── 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 }); } - // ── Main build pipeline ────────────────────────────────────────────────── + // ── Build ──────────────────────────────────────────────────────────────── function buildGraph() { if (building) return; building = true; @@ -295,85 +174,35 @@ var canvas = getCanvas(); if (!canvas) return; canvas.innerHTML = ""; - setStatus("Строю схему: загрузка инстансов…"); + setStatus("Загрузка…"); - // Step 1: Use allInstances exposed from IIFE via window._getInstances() var instances = (typeof window._getInstances === "function") ? window._getInstances() : []; if (!instances || !instances.length) { - setStatus("Нет данных. Нажмите «Обновить» в таблице, затем откройте схему снова."); + setStatus("Нет данных. Обновите таблицу и откройте схему снова."); building = false; return; } - // Step 2: Collect unique realms - var realmSet = {}; - instances.forEach(function (inst) { - var r = inst.resourceRealm; - if (r) realmSet[r] = true; - }); - - // Build node list: realm nodes + instance nodes - var nodes = []; - var nodesMeta = { map: {}, edges: [] }; - - Object.keys(realmSet).forEach(function (r) { - var id = "realm:" + r; - nodes.push({ id: id, w: REALM_W, h: REALM_H }); - nodesMeta.map[id] = { type: "realm", label: r }; - }); - - instances.forEach(function (inst) { - var id = inst.instanceUid; - if (!id) return; - nodes.push({ id: id, w: NODE_W, h: NODE_H }); - nodesMeta.map[id] = { - type: "instance", - label: inst.displayName || id, - svc: inst.svc || "", - statusColor: statusDotColor(inst.explainedStatus) - }; - }); - - // Step 3: Realm → instance edges (data already available) - instances.forEach(function (inst) { - var r = inst.resourceRealm; - if (r && inst.instanceUid) { - nodesMeta.edges.push({ - from: "realm:" + r, - to: inst.instanceUid, - color: "#334155" - }); - } - }); - - // Step 4: Draw nodes + realm edges immediately - setStatus("Строю схему: расстановка узлов…"); - var g = layoutGraph(nodes, nodesMeta.edges); - var partialSvg = renderSVG(g, nodesMeta); - canvas.innerHTML = partialSvg; + // Render grid immediately without edges + canvas.innerHTML = renderSVG(instances, []); attachPanzoom(); + setStatus("Загрузка зависимостей…"); - // Step 5: Load dependencies for each instance one by one - var uidList = instances.map(function (i) { return i.instanceUid; }).filter(Boolean); - var total = uidList.length; - var done = 0; + // Load all deps, then re-render once + var edges = []; + var total = instances.length; + var done = 0; function loadNext(idx) { - if (idx >= uidList.length) { - // All done — re-render with all edges - setStatus("Строю финальную схему…"); - var gFinal = layoutGraph(nodes, nodesMeta.edges); - var finalSvg = renderSVG(gFinal, nodesMeta); - canvas.innerHTML = finalSvg; + if (idx >= instances.length) { + canvas.innerHTML = renderSVG(instances, edges); attachPanzoom(); graphCache = canvas.innerHTML; building = false; setStatus(""); return; } - - var uid = uidList[idx]; - + var uid = instances[idx].instanceUid; fetch("/dashboard/api/instances/" + uid, { headers: { "X-Deck-Token": localStorage.getItem("deck_token") || "", @@ -383,65 +212,32 @@ .then(function (r) { return r.ok ? r.json() : null; }) .then(function (data) { done++; - setStatus("Загружаю зависимости: " + done + " / " + total); - + setStatus("Зависимости: " + done + "/" + total); if (data) { var inst = data.instance || data; - var deps = inst.dependencies || inst.deps || []; - var depInstances = inst.dependentInstances || inst.dependsOn || []; - - deps.forEach(function (dep) { - var toUid = dep.uid || dep.instanceUid || dep.dependencyUid; - if (toUid && nodesMeta.map[toUid]) { - var c = edgeColor(dep.svc || ""); - nodesMeta.edges.push({ from: uid, to: toUid, color: c }); - } + 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 }); }); - - depInstances.forEach(function (dep) { - var fromUid = dep.uid || dep.instanceUid || dep.dependencyUid; - if (fromUid && nodesMeta.map[fromUid]) { - var c = edgeColor(dep.svc || ""); - nodesMeta.edges.push({ from: fromUid, to: uid, color: c }); - } + 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 }); }); - - // Re-render SVG incrementally every 10 instances - if (done % 10 === 0) { - var gNow = layoutGraph(nodes, nodesMeta.edges); - canvas.innerHTML = renderSVG(gNow, nodesMeta); - attachPanzoom(); - } } - - // Small delay to avoid hammering the API - setTimeout(function () { loadNext(idx + 1); }, 30); + setTimeout(function () { loadNext(idx + 1); }, 20); }) .catch(function () { done++; - setTimeout(function () { loadNext(idx + 1); }, 30); + setTimeout(function () { loadNext(idx + 1); }, 20); }); } loadNext(0); } - // ── Panzoom attachment ─────────────────────────────────────────────────── - function attachPanzoom() { - var svg = document.getElementById("graph-svg"); - if (!svg || typeof panzoom === "undefined") return; - // Destroy previous instance if any - if (svg._panzoom) { - try { svg._panzoom.dispose(); } catch (e) {} - } - svg._panzoom = panzoom(svg, { - maxZoom: 4, - minZoom: 0.1, - smoothScroll: false - }); - } - - // ── Wire up after DOM ready ────────────────────────────────────────────── + // ── Init ───────────────────────────────────────────────────────────────── function init() { var btnOpen = document.getElementById("btn-graph-open"); var btnClose = document.getElementById("btn-graph-close"); @@ -454,8 +250,6 @@ if (overlay) overlay.addEventListener("click", function (e) { if (e.target === overlay) closeGraph(); }); - - // Keyboard Escape document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeGraph(); }); @@ -467,7 +261,5 @@ init(); } - // Expose for manual testing window.graphModule = { open: openGraph, close: closeGraph, refresh: refreshGraph }; - })();