diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml
index e705831..3020997 100644
--- a/k8s/deployment.yaml
+++ b/k8s/deployment.yaml
@@ -15,7 +15,7 @@ spec:
spec:
containers:
- name: cloud-dashboard
- image: naeel/cloud-dashboard:v9.18
+ image: naeel/cloud-dashboard:v9.19
imagePullPolicy: Always
ports:
- containerPort: 8080
diff --git a/static/index.html b/static/index.html
index 1c4951c..a303ec0 100644
--- a/static/index.html
+++ b/static/index.html
@@ -249,6 +249,7 @@
+
@@ -266,8 +267,6 @@ var graphAllData = null;
var graphRunningData = null;
var graphFullData = null;
var graphAllKey = "";
-var graphWarmupScheduled = false;
-var graphDrawing = false;
var graphFetchInFlight = false;
var rawDetail = {};
var navBackUid = null;
@@ -295,6 +294,53 @@ function statusColor(s){
return m[s] || "#8b949e";
}
+
+// ─── GRAPH: SVG + panzoom ─────────────────────────────────────────────────
+
+// Status color for node border
+function nodeStatusColor(status){
+ var m = {
+ "running": "#238636", "suspended": "#e3b341", "deleting": "#f85149",
+ "deleted": "#6e7681", "not created": "#8b949e", "pending": "#a371f7",
+ "creating": "#1f6feb"
+ };
+ return m[status] || "#8b949e";
+}
+
+// Service → background color
+function nodeBgColor(svc){
+ var s = (svc || "").toLowerCase();
+ if(/postgres|sql/.test(s)) return "#0c1e36";
+ if(/kubernetes|k8s|шт[уy]рвал/.test(s)) return "#0f1e0f";
+ if(/s3|object storage|бакет/.test(s)) return "#1a1200";
+ if(/edge|шлюз/.test(s)) return "#200c1a";
+ if(/vm|виртуальн/.test(s)) return "#1a1014";
+ if(/redis/.test(s)) return "#200a0a";
+ if(/rabbit/.test(s)) return "#0a1a10";
+ if(/dns/.test(s)) return "#0a1220";
+ if(/cloud director|vdc|датацентр/.test(s)) return "#001430";
+ return "#161b22";
+}
+
+// Break text into lines fitting maxWidth (chars-based estimate)
+function wrapText(text, maxChars){
+ var words = text.split(/(?=[_\-])/); // split on _ and - keeping delimiter
+ var lines = [];
+ var cur = "";
+ for(var i = 0; i < words.length; i++){
+ var w = words[i];
+ if((cur + w).length > maxChars && cur.length > 0){
+ lines.push(cur);
+ cur = w;
+ } else {
+ cur += w;
+ }
+ }
+ if(cur) lines.push(cur);
+ return lines.length ? lines : [text];
+}
+
+// Fetch graph data from backend
function fetchGraph(params){
if(graphFetchInFlight) return Promise.resolve(null);
graphFetchInFlight = true;
@@ -317,351 +363,6 @@ function fetchGraph(params){
});
}
-function normGraphText(v){
- return String(v || "").trim().toLowerCase();
-}
-
-function isUserSingletonNode(n){
- var t = [n.label, n.svc, n.id].map(normGraphText).join(" ");
- return /cloud director|cloud_director|(^|\W)s3(\W|$)|(^|\W)edge(\W|$)/.test(t);
-}
-
-function isPlatformNode(n, platformNames){
- var label = normGraphText(n.label);
- var id = normGraphText(n.id);
- var svc = normGraphText(n.svc);
- if(platformNames.size > 0){
- if(platformNames.has(label) || platformNames.has(id)) return true;
- var matched = false;
- platformNames.forEach(function(p){
- if(matched || !p) return;
- if(label.indexOf(p) >= 0 || id.indexOf(p) >= 0) matched = true;
- });
- if(matched) return true;
- }
- return /platform|kuber|k8s|cluster|shturval|штурвал/.test(svc + " " + label);
-}
-
-function buildRowLayout(data, containerWidth, forceShowAux){
- var MAX_PER_ROW = 5;
- var nodes = data.nodes || [];
- var showAux = document.getElementById("chk-auxiliary");
- var hideAux = forceShowAux ? false : (showAux ? !showAux.checked : true);
-
- var filtered = hideAux ? nodes.filter(function(n){ return !n.isAuxiliary; }) : nodes;
- if(filtered.length === 0) filtered = nodes;
-
- var platforms = [];
- var singletons = [];
- var rest = [];
-
- filtered.forEach(function(n){
- var lane = n.lane || "regular";
- if(lane === "platform"){ platforms.push(n); return; }
- if(lane === "singleton"){ singletons.push(n); return; }
- rest.push(n);
- });
-
- // Helper: split array into chunks of max N
- function chunkArray(arr, n){
- var res = [];
- for(var i=0; i= 1){
- var d10 = days % 10, d100 = days % 100;
- var w = (d100 >= 11 && d100 <= 19) ? "дней" : d10 === 1 ? "день" : (d10 >= 2 && d10 <= 4) ? "дня" : "дней";
- return days + " " + w;
- }
- if(hrs >= 1) return hrs + " ч";
- return mins + " мин";
-}
-
-function enrichNodesWithMetadata(data, focusUid, laneById){
- (data.nodes || []).forEach(function(n){
- var name = (n.label || n.id || "");
- var svc = n.svc || "";
- var age = humanAge(n.created);
- var nodeFixedW = 200;
- var textMaxWidth = nodeFixedW - 24;
- var fontSize = age ? (name.length > 36 ? 10 : name.length > 24 ? 11 : 12) : (name.length > 36 ? 11 : name.length > 24 ? 12 : 13);
- var nameFs = Math.round(fontSize * 1.25);
- var charsPerLine = Math.max(8, Math.floor(textMaxWidth / (nameFs * 0.6)));
- var nameLines = Math.max(1, Math.ceil(name.length / charsPerLine));
- var nameH = nameLines * Math.round(nameFs * 1.5);
- var svcH = svc ? Math.round((fontSize - 1) * 1.5) + 6 : 0;
- var ageH = age ? 9 * 2 + 12 : 0;
- var height = Math.max(80, 16 + nameH + svcH + ageH);
- n.nodeWidth = nodeFixedW;
- n.nodeHeight = height;
- n.fontSize = fontSize;
- n.bgColor = svcBgColor(svc);
- n.statusColor = statusColor(n.status || "");
- n.age = age;
- });
- return data;
-}
-
-function graphElements(data, focusUid, laneById){
- var elems = [];
- (data.nodes || []).forEach(function(n){
- var name = (n.label || n.id || "");
- var svc = n.svc || "";
- var age = humanAge(n.created);
- // Build label: name + svc + (2 blank lines) + age (small font)
- var label = name;
- if(svc) label += "\n" + svc;
- if(age) label += "\n\n\u23f1 " + age;
- // Fixed node width; text wraps inside
- var nodeFixedW = 200;
- var textMaxWidth = nodeFixedW - 24;
- var fontSize = age ? (name.length > 36 ? 10 : name.length > 24 ? 11 : 12) : (name.length > 36 ? 11 : name.length > 24 ? 12 : 13);
- // Estimate height: nameFs = fontSize*1.25, ~0.6 px per char at that size
- var nameFs = Math.round(fontSize * 1.25);
- var charsPerLine = Math.max(8, Math.floor(textMaxWidth / (nameFs * 0.6)));
- var nameLines = Math.max(1, Math.ceil(name.length / charsPerLine));
- var nameH = nameLines * Math.round(nameFs * 1.5);
- var svcH = svc ? Math.round((fontSize - 1) * 1.5) + 6 : 0;
- var ageH = age ? 9 * 2 + 12 : 0;
- var height = Math.max(80, 16 + nameH + svcH + ageH);
- var bgColor = svcBgColor(svc);
- elems.push({
- data: {
- id: n.id,
- label: n.label || n.id,
- fullLabel: n.label || n.id,
- status: n.status || "unknown",
- color: statusColor(n.status || ""),
- bgColor: bgColor,
- width: nodeFixedW,
- height: height,
- fontSize: fontSize,
- textMaxWidth: textMaxWidth,
- svc: svc,
- age: age
- },
- classes: (focusUid && n.id === focusUid) ? "focus" : ""
- });
- });
- return elems;
-}
-
-function drawGraph(containerId, data, focusUid, forceShowAux){
- if(graphDrawing) return;
- graphDrawing = true;
- var el = document.getElementById(containerId);
- if(!el) { graphDrawing = false; return; }
- el.innerHTML = "";
- if((data.nodes || []).length === 0){
- el.innerHTML = 'Нет узлов для отображения
';
- graphDrawing = false;
- return;
- }
- var layoutMeta = buildRowLayout(data, 1200, forceShowAux);
- var posMap = layoutMeta.positions;
- var visibleNodes = data.nodes || [];
-
- // Calculate container bounds
- var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
- visibleNodes.forEach(function(n){
- var pos = posMap[n.id];
- if(!pos) return;
- var w = n.nodeWidth || 200;
- var h = n.nodeHeight || 80;
- minX = Math.min(minX, pos.x - w/2);
- maxX = Math.max(maxX, pos.x + w/2);
- minY = Math.min(minY, pos.y - h/2);
- maxY = Math.max(maxY, pos.y + h/2);
- });
-
- var padding = 40;
- var containerW = maxX - minX + 2*padding;
- var containerH = maxY - minY + 2*padding;
- var offsetX = -minX + padding;
- var offsetY = -minY + padding;
-
- // Create scrollable container
- var scrollContainer = document.createElement("div");
- scrollContainer.style.width = "100%";
- scrollContainer.style.height = "100%";
- scrollContainer.style.overflow = "auto";
- scrollContainer.style.backgroundColor = "#0d1117";
-
- // Create SVG
- var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- svg.setAttribute("width", containerW);
- svg.setAttribute("height", containerH);
- svg.setAttribute("viewBox", "0 0 " + containerW + " " + containerH);
- svg.style.display = "block";
- svg.style.minWidth = containerW + "px";
- svg.style.minHeight = containerH + "px";
-
- var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
-
- // Draw nodes with text wrapping via tspan
- visibleNodes.forEach(function(n){
- var pos = posMap[n.id];
- if(!pos) return;
- var x = pos.x + offsetX;
- var y = pos.y + offsetY;
- var w = n.nodeWidth || 200;
- var h = n.nodeHeight || 80;
- var bgColor = svcBgColor(n.svc || "");
- var borderColor = statusColor(n.status || "");
- var isFocus = focusUid && n.id === focusUid;
-
- // Rectangle
- var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
- rect.setAttribute("x", x - w/2);
- rect.setAttribute("y", y - h/2);
- rect.setAttribute("width", w);
- rect.setAttribute("height", h);
- rect.setAttribute("rx", 6);
- rect.setAttribute("fill", bgColor);
- rect.setAttribute("stroke", isFocus ? "#facc15" : borderColor);
- rect.setAttribute("stroke-width", isFocus ? 3 : 2.4);
- rect.style.cursor = "pointer";
- rect.addEventListener("click", function(){
- if(n.id) navigateToDep(n.id);
- });
- g.appendChild(rect);
-
- // Text with wrapping: name + svc + age
- var nameFs = Math.round((n.fontSize || 13) * 1.25);
- var txtX = x;
- var txtY = y - h/4;
- var lineH = nameFs * 1.5;
- var lines = [];
-
- // Split name into lines (rough estimate)
- var charsPerLine = Math.max(8, Math.floor((w - 20) / (nameFs * 0.6)));
- var nameLines = [];
- for(var i = 0; i < n.label.length; i += charsPerLine){
- nameLines.push(n.label.substring(i, i + charsPerLine));
- }
- nameLines.forEach(function(line){ lines.push({text: line, size: nameFs, weight: "bold", style: "normal", color: "#dbeafe"}); });
-
- if(n.svc){
- lines.push({text: n.svc, size: nameFs - 2, weight: "normal", style: "italic", color: "#93c5fd"});
- }
-
- if(n.age){
- lines.push({text: "⏱ " + n.age, size: 9, weight: "normal", style: "italic", color: "#9ca3af"});
- }
-
- // Render text lines
- var textG = document.createElementNS("http://www.w3.org/2000/svg", "g");
- var currentY = txtY - (lines.length * lineH / 2);
- lines.forEach(function(line){
- var text = document.createElementNS("http://www.w3.org/2000/svg", "text");
- text.setAttribute("x", txtX);
- text.setAttribute("y", currentY);
- text.setAttribute("text-anchor", "middle");
- text.setAttribute("font-size", line.size);
- text.setAttribute("font-weight", line.weight);
- text.setAttribute("font-style", line.style);
- text.setAttribute("fill", line.color);
- text.setAttribute("font-family", "system-ui, sans-serif");
- text.textContent = line.text;
- text.style.pointerEvents = "none";
- textG.appendChild(text);
- currentY += lineH;
- });
- g.appendChild(textG);
- });
-
- svg.appendChild(g);
- scrollContainer.appendChild(svg);
- el.appendChild(scrollContainer);
-
- setTimeout(function(){ graphDrawing = false; }, 50);
-}
-
function ensureAllGraphLoaded(){
var statuses = getChecked().slice().sort();
var key = statuses.join(",");
@@ -686,99 +387,206 @@ function ensureFullGraphLoaded(){
});
}
-function renderInstanceGraph(uid){
- var holderId = "g-" + uid;
- var holder = document.getElementById(holderId);
- if(holder) holder.innerHTML = 'Строю граф зависимостей...
';
- var run = function(){
- ensureFullGraphLoaded().then(function(){
- var g = buildDepSubgraphFromDetail(uid);
- enrichNodesWithMetadata(g);
- if((g.nodes || []).length > 2){
- if(holder) holder.innerHTML = 'Граф открыт почти на весь экран для удобства чтения.
';
- showGraphModal("Граф зависимостей: " + escHtml(instanceName(uid)), g, uid, true);
- } else {
- drawGraph(holderId, g, uid, true);
- }
- }).catch(function(e){
- var el = document.getElementById(holderId);
- if(el) el.innerHTML = '' + escHtml(e.message) + '
';
- });
- };
- requestAnimationFrame(function(){ requestAnimationFrame(run); });
-}
-
-function instanceName(uid){
- for(var i=0;i 2 ? 10 : nameLines.length > 1 ? 11 : 12;
+ var lineH = nameFs * 1.5;
+ var totalTextH = nameLines.length * lineH + (n.svc ? lineH : 0);
+ var startY = cy - totalTextH / 2 + nameFs;
+
+ nameLines.forEach(function(line, li){
+ var t = document.createElementNS("http://www.w3.org/2000/svg", "text");
+ t.setAttribute("x", cx);
+ t.setAttribute("y", startY + li * lineH);
+ t.setAttribute("text-anchor", "middle");
+ t.setAttribute("font-size", nameFs);
+ t.setAttribute("font-weight", "bold");
+ t.setAttribute("fill", "#dbeafe");
+ t.setAttribute("font-family", "system-ui,sans-serif");
+ t.style.pointerEvents = "none";
+ t.textContent = line;
+ g.appendChild(t);
+ });
+
+ // Service
+ if(n.svc){
+ var st = document.createElementNS("http://www.w3.org/2000/svg", "text");
+ st.setAttribute("x", cx);
+ st.setAttribute("y", startY + nameLines.length * lineH);
+ st.setAttribute("text-anchor", "middle");
+ st.setAttribute("font-size", 9);
+ st.setAttribute("font-style", "italic");
+ st.setAttribute("fill", "#93c5fd");
+ st.setAttribute("font-family", "system-ui,sans-serif");
+ st.style.pointerEvents = "none";
+ st.textContent = n.svc;
+ g.appendChild(st);
+ }
+
+ // Age
+ var age = humanAge(n.created || "");
+ if(age){
+ var at = document.createElementNS("http://www.w3.org/2000/svg", "text");
+ at.setAttribute("x", cx);
+ at.setAttribute("y", y + NODE_H - 6);
+ at.setAttribute("text-anchor", "middle");
+ at.setAttribute("font-size", 9);
+ at.setAttribute("fill", "#9ca3af");
+ at.setAttribute("font-family", "system-ui,sans-serif");
+ at.style.pointerEvents = "none";
+ at.textContent = "\u23f1 " + age;
+ g.appendChild(at);
+ }
+ });
+
+ svg.appendChild(g);
+ wrapper.appendChild(svg);
+ el.appendChild(wrapper);
+
+ // ── panzoom: pan + scroll-zoom ──
+ if(window.panzoom){
+ var instance = window.panzoom(g, {
+ smoothScroll: false,
+ bounds: false,
+ zoomDoubleClickSpeed: 1,
+ minZoom: 0.2,
+ maxZoom: 3
+ });
+ svg.addEventListener("dblclick", function(){
+ instance.moveTo(0, 0);
+ instance.zoomAbs(0, 0, 1);
+ });
+ }
+}
+
+// ── Open / Close running graph modal ──────────────────────────────────────
function openRunningGraph(){
var modal = document.getElementById("graph-modal");
modal.classList.add("open");
var titleEl = document.getElementById("graph-modal-title");
var statuses = getChecked();
- if(titleEl) titleEl.textContent = "Общий граф зависимостей" + (statuses.length ? ": " + statuses.join(", ") : ": все");
+ if(titleEl) titleEl.textContent = "Общий граф зависимостей: " + (statuses.length ? statuses.join(", ") : "все");
var target = document.getElementById("running-graph");
if(target) target.innerHTML = 'Собираю граф...
';
ensureAllGraphLoaded()
- .then(function(data){ enrichNodesWithMetadata(data || {nodes:[], edges:[]}, ""); drawGraph("running-graph", data || {nodes:[], edges:[]}, ""); })
+ .then(function(data){ drawGraph("running-graph", data || {nodes:[], edges:[]}, ""); })
.catch(function(e){
- var el = document.getElementById("running-graph");
- if(el) el.innerHTML = '' + escHtml(e.message) + '
';
+ var el2 = document.getElementById("running-graph");
+ if(el2) el2.innerHTML = '' + escHtml(e.message) + '
';
});
}
@@ -786,32 +594,32 @@ function closeRunningGraph(){
document.getElementById("graph-modal").classList.remove("open");
}
-function showGraphModal(title, data, focusUid, forceShowAux){
+function showGraphModal(title, data, focusUid){
var modal = document.getElementById("graph-modal");
var titleEl = document.getElementById("graph-modal-title");
if(titleEl) titleEl.textContent = title;
modal.classList.add("open");
- enrichNodesWithMetadata(data || {nodes:[], edges:[]}, focusUid);
- drawGraph("running-graph", data || {nodes:[], edges:[]}, focusUid || "", forceShowAux);
+ drawGraph("running-graph", data || {nodes:[], edges:[]}, focusUid || "");
}
-function scheduleGraphWarmup(){
- if(graphWarmupScheduled) return;
- graphWarmupScheduled = true;
- var warm = function(){
- // optional background warm-up: should never block UI
- fetchGraph({ status: ["running"] }).then(function(data){
- graphRunningData = data || {nodes:[], edges:[]};
- }).catch(function(){ /* ignore */ });
- };
- if(window.requestIdleCallback){
- requestIdleCallback(warm, { timeout: 3000 });
- } else {
- setTimeout(warm, 3000);
- }
+// ── Instance graph (вкладка Граф в детали) ─────────────────────────────────
+function renderInstanceGraph(uid){
+ var holderId = "g-" + uid;
+ var holder = document.getElementById(holderId);
+ if(holder) holder.innerHTML = 'Строю граф...
';
+ ensureFullGraphLoaded().then(function(){
+ var g = buildDepSubgraphFromDetail(uid);
+ if((g.nodes || []).length > 2){
+ showGraphModal("Граф зависимостей: " + escHtml(instanceName(uid)), g, uid);
+ } else {
+ drawGraph(holderId, g, uid);
+ }
+ }).catch(function(e){
+ var el = document.getElementById(holderId);
+ if(el) el.innerHTML = '' + escHtml(e.message) + '
';
+ });
}
-function getToken(){ return localStorage.getItem("deck_token") || ""; }
function getDeckEnv(){
return localStorage.getItem("deck_env") || "test";
@@ -1265,10 +1073,9 @@ function initApp(){
document.getElementById("chk-auxiliary").addEventListener("change", function(){
cache = {};
render(filteredInstances());
- if(graphRunningData){ graphDrawing = false; drawGraph("running-graph", graphRunningData, ""); }
+ if(graphRunningData){ drawGraph("running-graph", graphRunningData, ""); }
});
loadAllDetails();
- scheduleGraphWarmup();
}
document.getElementById("btn-login").addEventListener("click", doLogin);
@@ -1292,45 +1099,6 @@ if(getToken()) initApp();
window.navigateToDep = navigateToDep;
window.navBack = navBack;
-// Build instance subgraph using rawDetail dependencies (no edges needed)
-function buildDepSubgraphFromDetail(rootUid){
- var seen = new Set([rootUid]);
- var frontier = new Set([rootUid]);
- for(var i=0;i<3;i++){
- var nxt = new Set();
- frontier.forEach(function(u){
- var det = rawDetail[u];
- if(!det) return;
- (det.dependencies || []).forEach(function(dep){
- var duid = depUid(dep);
- if(duid && !seen.has(duid)) nxt.add(duid);
- });
- (det.dependentInstances || []).forEach(function(dep){
- var duid = depUid(dep);
- if(duid && !seen.has(duid)) nxt.add(duid);
- });
- });
- if(nxt.size === 0) break;
- nxt.forEach(function(x){ seen.add(x); });
- frontier = nxt;
- }
- var allNodes = (graphFullData && graphFullData.nodes) || [];
- var filtered = allNodes.filter(function(n){ return seen.has(n.id); });
- // Ensure root is present even if not in graphFullData
- if(!filtered.some(function(n){ return n.id === rootUid; })){
- var inst = null;
- for(var j=0;j