// graph.js — Simple grid layout, max 5 columns, arrows between deps (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 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"] ]; 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] }; } return { bg: "#1e293b", border: "#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; var 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(); } // ── 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 = [ '', '', '', '', '', '' ]; // 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 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) + '' ); }); parts.push(''); return parts.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("Загрузка…"); var instances = (typeof window._getInstances === "function") ? window._getInstances() : []; if (!instances || !instances.length) { setStatus("Нет данных. Обновите таблицу и откройте схему снова."); building = false; return; } // Render grid immediately without edges canvas.innerHTML = renderSVG(instances, []); attachPanzoom(); setStatus("Загрузка зависимостей…"); // Load all deps, then re-render once var edges = []; var total = instances.length; var 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; 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 }); }); 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 }); }); } 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(); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } window.graphModule = { open: openGraph, close: closeGraph, refresh: refreshGraph }; })();