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 = [
- '');
+ 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 };
-
})();