Files
dashboard/static/graph.js
T

266 lines
9.9 KiB
JavaScript

// 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, "&amp;").replace(/</g, "&lt;")
.replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
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 = [
'<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>'
];
// 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)"/>'
);
});
// 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>'
);
});
parts.push('</svg>');
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 };
})();