diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index f2ae31b..361d304 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -15,7 +15,7 @@ spec: spec: containers: - name: cloud-dashboard - image: naeel/cloud-dashboard:v9.45 + image: naeel/cloud-dashboard:v9.46 imagePullPolicy: Always ports: - containerPort: 8080 diff --git a/static/graph.js b/static/graph.js new file mode 100644 index 0000000..dc26c27 --- /dev/null +++ b/static/graph.js @@ -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, """); + } + + 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 = [ + '', + 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( + '' + ); + }); + + // 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( + '' + ); + svgParts.push( + 'ПЛАТФОРМА' + ); + svgParts.push( + '' + escX(truncate(meta.label, 22)) + '' + ); + } else { + // Instance node + var st = svcStyle(meta.svc); + svgParts.push( + '' + ); + // Status dot + var dotColor = meta.statusColor || "#6e7681"; + svgParts.push( + '' + ); + // Name + svgParts.push( + '' + escX(truncate(meta.label, 18)) + '' + ); + // Svc + svgParts.push( + '' + escX(truncate(meta.svc, 22)) + '' + ); + } + }); + + svgParts.push(''); + 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 }; + +})(); diff --git a/static/index.html b/static/index.html index da7bd9f..ab9393c 100644 --- a/static/index.html +++ b/static/index.html @@ -212,7 +212,27 @@ .params-grid, .deps-grid { grid-template-columns: 1fr; } .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; } + +
@@ -251,6 +271,7 @@
+
@@ -1075,5 +1096,19 @@ window.navBack = navBack; })(); + + + +