v9.46: add dependency graph modal (dagre+panzoom, progressive render)
This commit is contained in:
+1
-1
@@ -15,7 +15,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: cloud-dashboard
|
- name: cloud-dashboard
|
||||||
image: naeel/cloud-dashboard:v9.45
|
image: naeel/cloud-dashboard:v9.46
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
|
|||||||
+475
@@ -0,0 +1,475 @@
|
|||||||
|
// 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
|
||||||
|
(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;
|
||||||
|
|
||||||
|
// Color palette per svc keyword (substring match, case-insensitive)
|
||||||
|
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"],
|
||||||
|
["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();
|
||||||
|
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 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── State ────────────────────────────────────────────────────────────────
|
||||||
|
var graphCache = null; // built SVG string, null = not built yet
|
||||||
|
var building = false; // loading in progress flag
|
||||||
|
|
||||||
|
// ── Modal DOM ────────────────────────────────────────────────────────────
|
||||||
|
function getModal() { return document.getElementById("graph-modal"); }
|
||||||
|
function getCanvas() { return document.getElementById("graph-canvas"); }
|
||||||
|
function getStatus() { return document.getElementById("graph-status"); }
|
||||||
|
|
||||||
|
// ── Open / Close ─────────────────────────────────────────────────────────
|
||||||
|
function openGraph() {
|
||||||
|
var modal = getModal();
|
||||||
|
if (!modal) return;
|
||||||
|
modal.style.display = "flex";
|
||||||
|
if (graphCache) {
|
||||||
|
showCached();
|
||||||
|
} else if (!building) {
|
||||||
|
buildGraph();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGraph() {
|
||||||
|
var modal = getModal();
|
||||||
|
if (modal) modal.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshGraph() {
|
||||||
|
graphCache = null;
|
||||||
|
building = false;
|
||||||
|
var canvas = getCanvas();
|
||||||
|
if (canvas) canvas.innerHTML = "";
|
||||||
|
buildGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCached() {
|
||||||
|
var canvas = getCanvas();
|
||||||
|
if (!canvas) return;
|
||||||
|
canvas.innerHTML = graphCache;
|
||||||
|
attachPanzoom();
|
||||||
|
setStatus("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(msg) {
|
||||||
|
var el = getStatus();
|
||||||
|
if (el) el.textContent = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Layout helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncate(str, maxLen) {
|
||||||
|
if (!str) return "";
|
||||||
|
return str.length > maxLen ? str.slice(0, maxLen - 1) + "…" : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderArrowMarker(color, id) {
|
||||||
|
return '<marker id="' + escX(id) + '" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto">' +
|
||||||
|
'<path d="M0,0 L0,6 L8,3 z" fill="' + escX(color) + '"/>' +
|
||||||
|
'</marker>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<defs>';
|
||||||
|
Object.keys(markerColors).forEach(function (c) {
|
||||||
|
var mid = "arr-" + c.replace(/[^a-zA-Z0-9]/g, "");
|
||||||
|
defs += renderArrowMarker(c, mid);
|
||||||
|
});
|
||||||
|
defs += '</defs>';
|
||||||
|
|
||||||
|
var svgParts = [
|
||||||
|
'<svg id="graph-svg" xmlns="http://www.w3.org/2000/svg"',
|
||||||
|
' width="' + svgW + '" height="' + svgH + '"',
|
||||||
|
' viewBox="0 0 ' + svgW + ' ' + svgH + '">',
|
||||||
|
defs
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
'<polyline points="' + polyline(pts) + '"' +
|
||||||
|
' fill="none" stroke="' + escX(c) + '" stroke-width="1.5" opacity="0.75"' +
|
||||||
|
' marker-end="url(#' + mid + ')"/>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if (meta.type === "realm") {
|
||||||
|
// Platform node — dashed border, distinct style
|
||||||
|
svgParts.push(
|
||||||
|
'<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '"' +
|
||||||
|
' rx="6" fill="#0f172a" stroke="#64748b" stroke-width="1.5" stroke-dasharray="5,3"/>'
|
||||||
|
);
|
||||||
|
svgParts.push(
|
||||||
|
'<text x="' + (x + w / 2) + '" y="' + (y + h / 2 - 6) + '"' +
|
||||||
|
' text-anchor="middle" font-family="sans-serif" font-size="10" font-weight="600"' +
|
||||||
|
' fill="#94a3b8">ПЛАТФОРМА</text>'
|
||||||
|
);
|
||||||
|
svgParts.push(
|
||||||
|
'<text x="' + (x + w / 2) + '" y="' + (y + h / 2 + 10) + '"' +
|
||||||
|
' text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700"' +
|
||||||
|
' fill="#e2e8f0">' + escX(truncate(meta.label, 22)) + '</text>'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Instance node
|
||||||
|
var st = svcStyle(meta.svc);
|
||||||
|
svgParts.push(
|
||||||
|
'<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '"' +
|
||||||
|
' rx="6" fill="' + escX(st.bg) + '" stroke="' + escX(st.border) + '" stroke-width="1.5"/>'
|
||||||
|
);
|
||||||
|
// Status dot
|
||||||
|
var dotColor = meta.statusColor || "#6e7681";
|
||||||
|
svgParts.push(
|
||||||
|
'<circle cx="' + (x + 12) + '" cy="' + (y + h / 2) + '" r="4" fill="' + escX(dotColor) + '"/>'
|
||||||
|
);
|
||||||
|
// Name
|
||||||
|
svgParts.push(
|
||||||
|
'<text x="' + (x + 24) + '" y="' + (y + h / 2 - 5) + '"' +
|
||||||
|
' font-family="sans-serif" font-size="12" font-weight="600"' +
|
||||||
|
' fill="#f1f5f9">' + escX(truncate(meta.label, 18)) + '</text>'
|
||||||
|
);
|
||||||
|
// Svc
|
||||||
|
svgParts.push(
|
||||||
|
'<text x="' + (x + 24) + '" y="' + (y + h / 2 + 11) + '"' +
|
||||||
|
' font-family="sans-serif" font-size="10"' +
|
||||||
|
' fill="' + escX(st.border) + '">' + escX(truncate(meta.svc, 22)) + '</text>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svgParts.push('</svg>');
|
||||||
|
return svgParts.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";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main build pipeline ──────────────────────────────────────────────────
|
||||||
|
function buildGraph() {
|
||||||
|
if (building) return;
|
||||||
|
building = true;
|
||||||
|
graphCache = null;
|
||||||
|
|
||||||
|
var canvas = getCanvas();
|
||||||
|
if (!canvas) return;
|
||||||
|
canvas.innerHTML = "";
|
||||||
|
setStatus("Строю схему: загрузка инстансов…");
|
||||||
|
|
||||||
|
// Step 1: Use allInstances already loaded in memory
|
||||||
|
var instances = (typeof allInstances !== "undefined") ? allInstances : [];
|
||||||
|
if (!instances.length) {
|
||||||
|
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;
|
||||||
|
attachPanzoom();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
attachPanzoom();
|
||||||
|
graphCache = canvas.innerHTML;
|
||||||
|
building = false;
|
||||||
|
setStatus("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uid = uidList[idx];
|
||||||
|
var token = (typeof getToken === "function") ? getToken() : (window.getToken ? window.getToken() : "");
|
||||||
|
var env = (typeof getDeckEnv === "function") ? getDeckEnv() : (window.getDeckEnv ? window.getDeckEnv() : "test");
|
||||||
|
|
||||||
|
fetch("/dashboard/api/instances/" + uid, {
|
||||||
|
headers: {
|
||||||
|
"X-Deck-Token": token,
|
||||||
|
"X-Deck-Env": env
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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 = 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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
done++;
|
||||||
|
setTimeout(function () { loadNext(idx + 1); }, 30);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ──────────────────────────────────────────────
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard Escape
|
||||||
|
document.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "Escape") closeGraph();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose for manual testing
|
||||||
|
window.graphModule = { open: openGraph, close: closeGraph, refresh: refreshGraph };
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -212,7 +212,27 @@
|
|||||||
.params-grid, .deps-grid { grid-template-columns: 1fr; }
|
.params-grid, .deps-grid { grid-template-columns: 1fr; }
|
||||||
.user-email { display: none; }
|
.user-email { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Graph Modal ──────────────────────────────────────────────────── */
|
||||||
|
#graph-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.7); z-index: 2000; display: none; align-items: center; justify-content: center; }
|
||||||
|
#graph-modal { background: #161b22; border: 1px solid #30363d; border-radius: 12px; width: 96vw; max-width: 1400px; height: 90vh; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
#graph-toolbar { padding: 10px 16px; border-bottom: 1px solid #30363d; display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
|
||||||
|
#graph-toolbar h2 { font-size: 14px; font-weight: 700; color: #e1e4e8; flex: 1; }
|
||||||
|
#graph-status { font-size: 12px; color: #8b949e; flex: 1; }
|
||||||
|
#graph-canvas { flex: 1; overflow: hidden; position: relative; background: #0d1117; }
|
||||||
|
#graph-canvas svg { display: block; }
|
||||||
|
.btn-graph-ctrl { padding: 5px 12px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; cursor: pointer; font-size: 12px; }
|
||||||
|
.btn-graph-ctrl:hover { background: #30363d; }
|
||||||
|
.btn-graph-close { color: #f85149; }
|
||||||
|
.btn-graph-close:hover { background: #3d1a1a !important; border-color: #f85149 !important; }
|
||||||
|
body.light #graph-modal { background: #f6f8fa; border-color: #d0d7de; }
|
||||||
|
body.light #graph-toolbar { border-color: #d0d7de; }
|
||||||
|
body.light #graph-canvas { background: #edf2f7; }
|
||||||
|
body.light .btn-graph-ctrl { background: #f8fafc; border-color: #cbd5e1; color: #0f172a; }
|
||||||
|
body.light .btn-graph-ctrl:hover { background: #e2e8f0; }
|
||||||
</style>
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/dagre@0.8.5/dist/dagre.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/panzoom@9.4.3/dist/panzoom.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="login-screen">
|
<div id="login-screen">
|
||||||
@@ -251,6 +271,7 @@
|
|||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<span class="data-ts" id="data-ts"></span>
|
<span class="data-ts" id="data-ts"></span>
|
||||||
<button class="btn-refresh" id="btn-refresh">↻ Обновить</button>
|
<button class="btn-refresh" id="btn-refresh">↻ Обновить</button>
|
||||||
|
<button class="btn-refresh" id="btn-graph-open" title="Схема зависимостей">☍ Схема</button>
|
||||||
<button class="btn-theme" id="btn-theme" title="Сменить тему">🌙</button>
|
<button class="btn-theme" id="btn-theme" title="Сменить тему">🌙</button>
|
||||||
<div class="header-user">
|
<div class="header-user">
|
||||||
<span class="user-email" id="user-email"></span>
|
<span class="user-email" id="user-email"></span>
|
||||||
@@ -1075,5 +1096,19 @@ window.navBack = navBack;
|
|||||||
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- ── Graph Modal ───────────────────────────────────────────────────── -->
|
||||||
|
<div id="graph-overlay" style="display:none;align-items:center;justify-content:center;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:2000;">
|
||||||
|
<div id="graph-modal">
|
||||||
|
<div id="graph-toolbar">
|
||||||
|
<h2>☍ Схема зависимостей</h2>
|
||||||
|
<span id="graph-status"></span>
|
||||||
|
<button class="btn-graph-ctrl" id="btn-graph-refresh">↻ Обновить</button>
|
||||||
|
<button class="btn-graph-ctrl btn-graph-close" id="btn-graph-close">✕ Закрыть</button>
|
||||||
|
</div>
|
||||||
|
<div id="graph-canvas"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/dashboard/static/graph.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user