266 lines
9.9 KiB
JavaScript
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, "&").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 = [
|
|
'<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 };
|
|
})();
|