v9.19: rewrite graph with pure SVG + panzoom (pan/zoom), remove cytoscape/heavy code
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.18
|
image: naeel/cloud-dashboard:v9.19
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
|
|||||||
+248
-480
@@ -249,6 +249,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="js-err" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#f85149;color:#fff;padding:10px;font-family:monospace;font-size:12px;z-index:9999"></div>
|
<div id="js-err" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#f85149;color:#fff;padding:10px;font-family:monospace;font-size:12px;z-index:9999"></div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/panzoom@9.4.3/dist/panzoom.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.onerror=function(m,s,l){var e=document.getElementById("js-err");if(e){e.style.display="block";e.textContent="JS ERROR line "+l+": "+m;}};
|
window.onerror=function(m,s,l){var e=document.getElementById("js-err");if(e){e.style.display="block";e.textContent="JS ERROR line "+l+": "+m;}};
|
||||||
</script>
|
</script>
|
||||||
@@ -266,8 +267,6 @@ var graphAllData = null;
|
|||||||
var graphRunningData = null;
|
var graphRunningData = null;
|
||||||
var graphFullData = null;
|
var graphFullData = null;
|
||||||
var graphAllKey = "";
|
var graphAllKey = "";
|
||||||
var graphWarmupScheduled = false;
|
|
||||||
var graphDrawing = false;
|
|
||||||
var graphFetchInFlight = false;
|
var graphFetchInFlight = false;
|
||||||
var rawDetail = {};
|
var rawDetail = {};
|
||||||
var navBackUid = null;
|
var navBackUid = null;
|
||||||
@@ -295,6 +294,53 @@ function statusColor(s){
|
|||||||
return m[s] || "#8b949e";
|
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){
|
function fetchGraph(params){
|
||||||
if(graphFetchInFlight) return Promise.resolve(null);
|
if(graphFetchInFlight) return Promise.resolve(null);
|
||||||
graphFetchInFlight = true;
|
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<arr.length; i+=n) res.push(arr.slice(i, i+n));
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build visual rows: each row is an array of nodes, max MAX_PER_ROW per row
|
|
||||||
// Category label rows are tracked for separators
|
|
||||||
var allRows = []; // each entry: {nodes:[], category:string}
|
|
||||||
var platRows = chunkArray(platforms, MAX_PER_ROW);
|
|
||||||
for(var p=0; p<platRows.length; p++) allRows.push({nodes: platRows[p], category: "platform"});
|
|
||||||
|
|
||||||
var singleRows = chunkArray(singletons, MAX_PER_ROW);
|
|
||||||
for(var s=0; s<singleRows.length; s++) allRows.push({nodes: singleRows[s], category: "singleton"});
|
|
||||||
|
|
||||||
var restRows = chunkArray(rest, MAX_PER_ROW);
|
|
||||||
for(var rr=0; rr<restRows.length; rr++) allRows.push({nodes: restRows[rr], category: "regular"});
|
|
||||||
|
|
||||||
if(allRows.length === 0) allRows = [{nodes: nodes.slice(0, MAX_PER_ROW), category: "regular"}];
|
|
||||||
|
|
||||||
var usableWidth = Math.max(860, containerWidth || 0);
|
|
||||||
var positions = {};
|
|
||||||
var laneById = {};
|
|
||||||
var nodeW = 200;
|
|
||||||
var gapX = 50;
|
|
||||||
var gapY = 160;
|
|
||||||
var categoryGap = 80; // extra gap between categories
|
|
||||||
var currentY = 60;
|
|
||||||
var prevCategory = "";
|
|
||||||
|
|
||||||
for(var r=0; r<allRows.length; r++){
|
|
||||||
var row = allRows[r];
|
|
||||||
// Add extra gap when switching category
|
|
||||||
if(prevCategory && row.category !== prevCategory) currentY += categoryGap;
|
|
||||||
prevCategory = row.category;
|
|
||||||
|
|
||||||
var items = row.nodes;
|
|
||||||
var totalW = items.length * nodeW + (items.length - 1) * gapX;
|
|
||||||
var xStart = Math.max(40, (usableWidth - totalW) / 2);
|
|
||||||
for(var i=0; i<items.length; i++){
|
|
||||||
positions[items[i].id] = { x: xStart + i * (nodeW + gapX), y: currentY };
|
|
||||||
laneById[items[i].id] = r;
|
|
||||||
}
|
|
||||||
currentY += gapY;
|
|
||||||
}
|
|
||||||
|
|
||||||
data._filteredNodes = filtered;
|
|
||||||
return { positions: positions, laneById: laneById };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service-based background colors for graph nodes
|
|
||||||
var SVC_COLORS = {
|
|
||||||
"cloud director": "#1e3a5f",
|
|
||||||
"kubernetes": "#2d4a22",
|
|
||||||
"object storage": "#4a3728",
|
|
||||||
"s3": "#4a3728",
|
|
||||||
"dns": "#3b2d50",
|
|
||||||
"container registry": "#2a3f4f",
|
|
||||||
"apache nifi": "#4a3020",
|
|
||||||
"postgresql": "#1a3550",
|
|
||||||
"rabbitmq": "#4a3040",
|
|
||||||
"grafana": "#3a2a10",
|
|
||||||
"keycloak": "#2a3a3a",
|
|
||||||
"terraform": "#2a2050",
|
|
||||||
"fission": "#3a2040",
|
|
||||||
"edge gateway": "#1a4040"
|
|
||||||
};
|
|
||||||
function svcBgColor(svc){
|
|
||||||
var s = (svc || "").trim().toLowerCase();
|
|
||||||
for(var k in SVC_COLORS){ if(s.indexOf(k) !== -1) return SVC_COLORS[k]; }
|
|
||||||
// Hash fallback
|
|
||||||
var h = 0;
|
|
||||||
for(var i=0; i<s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
|
|
||||||
var hue = ((h % 360) + 360) % 360;
|
|
||||||
return "hsl(" + hue + ",30%,18%)";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute human-readable age from ISO date string
|
|
||||||
function humanAge(dtStr){
|
|
||||||
if(!dtStr) return "";
|
|
||||||
var d;
|
|
||||||
try { d = new Date(dtStr); } catch(e){ return ""; }
|
|
||||||
if(isNaN(d.getTime())) return "";
|
|
||||||
var diff = Date.now() - d.getTime();
|
|
||||||
if(diff < 0) return "";
|
|
||||||
var mins = Math.floor(diff / 60000);
|
|
||||||
var hrs = Math.floor(mins / 60);
|
|
||||||
var days = Math.floor(hrs / 24);
|
|
||||||
var months = Math.floor(days / 30);
|
|
||||||
var years = Math.floor(days / 365);
|
|
||||||
if(days >= 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 = '<div class="empty-msg" style="padding:14px">Нет узлов для отображения</div>';
|
|
||||||
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(){
|
function ensureAllGraphLoaded(){
|
||||||
var statuses = getChecked().slice().sort();
|
var statuses = getChecked().slice().sort();
|
||||||
var key = statuses.join(",");
|
var key = statuses.join(",");
|
||||||
@@ -686,99 +387,206 @@ function ensureFullGraphLoaded(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderInstanceGraph(uid){
|
// Build subgraph from rawDetail dependencies (BFS, no edges needed)
|
||||||
var holderId = "g-" + uid;
|
function buildDepSubgraphFromDetail(rootUid){
|
||||||
var holder = document.getElementById(holderId);
|
|
||||||
if(holder) holder.innerHTML = '<div class="graph-loading">Строю граф зависимостей...</div>';
|
|
||||||
var run = function(){
|
|
||||||
ensureFullGraphLoaded().then(function(){
|
|
||||||
var g = buildDepSubgraphFromDetail(uid);
|
|
||||||
enrichNodesWithMetadata(g);
|
|
||||||
if((g.nodes || []).length > 2){
|
|
||||||
if(holder) holder.innerHTML = '<div class="graph-inline-note">Граф открыт почти на весь экран для удобства чтения.</div>';
|
|
||||||
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 = '<div class="err-msg">' + escHtml(e.message) + '</div>';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
requestAnimationFrame(function(){ requestAnimationFrame(run); });
|
|
||||||
}
|
|
||||||
|
|
||||||
function instanceName(uid){
|
|
||||||
for(var i=0;i<allInstances.length;i++){
|
|
||||||
if(allInstances[i].instanceUid === uid) return allInstances[i].displayName || uid;
|
|
||||||
}
|
|
||||||
return uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function depUid(dep){
|
|
||||||
return (dep && (dep.uid || dep.instanceUid || dep.dependencyUid || dep.relatedUid)) || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function visibleUidSet(){
|
|
||||||
var s = new Set();
|
|
||||||
var checked = Array.from(document.querySelectorAll("#filters input[value]:checked")).map(function(i){ return i.value; });
|
|
||||||
var auxEl = document.getElementById("chk-auxiliary");
|
|
||||||
var showAux = auxEl ? auxEl.checked : true;
|
|
||||||
allInstances.forEach(function(i){
|
|
||||||
if(!i || !i.instanceUid) return;
|
|
||||||
if(checked.length && checked.indexOf(i.explainedStatus) < 0) return;
|
|
||||||
if(!showAux){
|
|
||||||
var det = rawDetail[i.instanceUid];
|
|
||||||
if(det && det.isAuxiliary) return;
|
|
||||||
}
|
|
||||||
s.add(i.instanceUid);
|
|
||||||
});
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildClientSubgraph(data, rootUid, depth){
|
|
||||||
var nodes = data.nodes || [];
|
|
||||||
var edges = data.edges || [];
|
|
||||||
var ids = new Set(nodes.map(function(n){ return n.id; }));
|
|
||||||
if(!ids.has(rootUid)) return { nodes: [], edges: [] };
|
|
||||||
var adj = {};
|
|
||||||
nodes.forEach(function(n){ adj[n.id] = new Set(); });
|
|
||||||
edges.forEach(function(e){
|
|
||||||
if(adj[e.source] && adj[e.target]){
|
|
||||||
adj[e.source].add(e.target);
|
|
||||||
adj[e.target].add(e.source);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var seen = new Set([rootUid]);
|
var seen = new Set([rootUid]);
|
||||||
var frontier = new Set([rootUid]);
|
var frontier = new Set([rootUid]);
|
||||||
for(var i=0;i<depth;i++){
|
for(var i = 0; i < 3; i++){
|
||||||
var nxt = new Set();
|
var nxt = new Set();
|
||||||
frontier.forEach(function(n){
|
frontier.forEach(function(u){
|
||||||
(adj[n] || new Set()).forEach(function(x){ if(!seen.has(x)) nxt.add(x); });
|
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;
|
if(nxt.size === 0) break;
|
||||||
nxt.forEach(function(x){ seen.add(x); });
|
nxt.forEach(function(x){ seen.add(x); });
|
||||||
frontier = nxt;
|
frontier = nxt;
|
||||||
}
|
}
|
||||||
return {
|
var allNodes = (graphFullData && graphFullData.nodes) || [];
|
||||||
nodes: nodes.filter(function(n){ return seen.has(n.id); }),
|
var filtered = allNodes.filter(function(n){ return seen.has(n.id); });
|
||||||
edges: edges.filter(function(e){ return seen.has(e.source) && seen.has(e.target); })
|
if(!filtered.some(function(n){ return n.id === rootUid; })){
|
||||||
};
|
for(var j = 0; j < allInstances.length; j++){
|
||||||
|
if(allInstances[j].instanceUid === rootUid){
|
||||||
|
var inst = allInstances[j];
|
||||||
|
filtered.push({
|
||||||
|
id: rootUid, label: inst.displayName || rootUid,
|
||||||
|
status: inst.explainedStatus || "unknown", svc: inst.svc || "",
|
||||||
|
lane: "regular", created: inst.instanceConfigDtCreated || inst.dtCreated || ""
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { nodes: filtered, edges: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── MAIN DRAW FUNCTION ────────────────────────────────────────────────────
|
||||||
|
function drawGraph(containerId, data, focusUid){
|
||||||
|
var el = document.getElementById(containerId);
|
||||||
|
if(!el) return;
|
||||||
|
el.innerHTML = "";
|
||||||
|
|
||||||
|
var nodes = data.nodes || [];
|
||||||
|
if(nodes.length === 0){
|
||||||
|
el.innerHTML = '<div class="empty-msg" style="padding:20px">Нет данных</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var NODE_W = 200;
|
||||||
|
var NODE_H = 80;
|
||||||
|
var GAP_X = 30;
|
||||||
|
var GAP_Y = 50;
|
||||||
|
var MAX_COLS = 5;
|
||||||
|
var PAD = 40;
|
||||||
|
|
||||||
|
// Sort: platforms first, singletons second, regular last
|
||||||
|
var sorted = nodes.slice().sort(function(a, b){
|
||||||
|
var rank = function(n){ return n.lane === "platform" ? 0 : n.lane === "singleton" ? 1 : 2; };
|
||||||
|
return rank(a) - rank(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign grid positions
|
||||||
|
var cols = Math.min(sorted.length, MAX_COLS);
|
||||||
|
sorted.forEach(function(n, idx){
|
||||||
|
n._col = idx % cols;
|
||||||
|
n._row = Math.floor(idx / cols);
|
||||||
|
});
|
||||||
|
|
||||||
|
var rows = Math.ceil(sorted.length / cols);
|
||||||
|
var svgW = PAD * 2 + cols * NODE_W + (cols - 1) * GAP_X;
|
||||||
|
var svgH = PAD * 2 + rows * NODE_H + (rows - 1) * GAP_Y;
|
||||||
|
|
||||||
|
// ── Create container with overflow scroll ──
|
||||||
|
var wrapper = document.createElement("div");
|
||||||
|
wrapper.style.cssText = "width:100%;height:100%;overflow:hidden;position:relative;background:#0d1117;";
|
||||||
|
|
||||||
|
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
svg.setAttribute("width", svgW);
|
||||||
|
svg.setAttribute("height", svgH);
|
||||||
|
svg.setAttribute("viewBox", "0 0 " + svgW + " " + svgH);
|
||||||
|
svg.style.cssText = "display:block;cursor:grab;user-select:none;";
|
||||||
|
|
||||||
|
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||||
|
|
||||||
|
sorted.forEach(function(n){
|
||||||
|
var x = PAD + n._col * (NODE_W + GAP_X);
|
||||||
|
var y = PAD + n._row * (NODE_H + GAP_Y);
|
||||||
|
var cx = x + NODE_W / 2;
|
||||||
|
var cy = y + NODE_H / 2;
|
||||||
|
var bg = nodeBgColor(n.svc || "");
|
||||||
|
var bc = nodeStatusColor(n.status || "");
|
||||||
|
var isFocus = focusUid && n.id === focusUid;
|
||||||
|
|
||||||
|
// Rect
|
||||||
|
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||||
|
rect.setAttribute("x", x);
|
||||||
|
rect.setAttribute("y", y);
|
||||||
|
rect.setAttribute("width", NODE_W);
|
||||||
|
rect.setAttribute("height", NODE_H);
|
||||||
|
rect.setAttribute("rx", 8);
|
||||||
|
rect.setAttribute("fill", bg);
|
||||||
|
rect.setAttribute("stroke", isFocus ? "#facc15" : bc);
|
||||||
|
rect.setAttribute("stroke-width", isFocus ? 3 : 2);
|
||||||
|
rect.style.cursor = "pointer";
|
||||||
|
rect.addEventListener("click", function(e){
|
||||||
|
e.stopPropagation();
|
||||||
|
navigateToDep(n.id);
|
||||||
|
});
|
||||||
|
g.appendChild(rect);
|
||||||
|
|
||||||
|
// Name — wrapped
|
||||||
|
var nameLines = wrapText(n.label || n.id || "", 20);
|
||||||
|
var nameFs = nameLines.length > 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(){
|
function openRunningGraph(){
|
||||||
var modal = document.getElementById("graph-modal");
|
var modal = document.getElementById("graph-modal");
|
||||||
modal.classList.add("open");
|
modal.classList.add("open");
|
||||||
var titleEl = document.getElementById("graph-modal-title");
|
var titleEl = document.getElementById("graph-modal-title");
|
||||||
var statuses = getChecked();
|
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");
|
var target = document.getElementById("running-graph");
|
||||||
if(target) target.innerHTML = '<div class="graph-loading">Собираю граф...</div>';
|
if(target) target.innerHTML = '<div class="graph-loading">Собираю граф...</div>';
|
||||||
ensureAllGraphLoaded()
|
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){
|
.catch(function(e){
|
||||||
var el = document.getElementById("running-graph");
|
var el2 = document.getElementById("running-graph");
|
||||||
if(el) el.innerHTML = '<div class="err-msg">' + escHtml(e.message) + '</div>';
|
if(el2) el2.innerHTML = '<div class="err-msg">' + escHtml(e.message) + '</div>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,32 +594,32 @@ function closeRunningGraph(){
|
|||||||
document.getElementById("graph-modal").classList.remove("open");
|
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 modal = document.getElementById("graph-modal");
|
||||||
var titleEl = document.getElementById("graph-modal-title");
|
var titleEl = document.getElementById("graph-modal-title");
|
||||||
if(titleEl) titleEl.textContent = title;
|
if(titleEl) titleEl.textContent = title;
|
||||||
modal.classList.add("open");
|
modal.classList.add("open");
|
||||||
enrichNodesWithMetadata(data || {nodes:[], edges:[]}, focusUid);
|
drawGraph("running-graph", data || {nodes:[], edges:[]}, focusUid || "");
|
||||||
drawGraph("running-graph", data || {nodes:[], edges:[]}, focusUid || "", forceShowAux);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleGraphWarmup(){
|
// ── Instance graph (вкладка Граф в детали) ─────────────────────────────────
|
||||||
if(graphWarmupScheduled) return;
|
function renderInstanceGraph(uid){
|
||||||
graphWarmupScheduled = true;
|
var holderId = "g-" + uid;
|
||||||
var warm = function(){
|
var holder = document.getElementById(holderId);
|
||||||
// optional background warm-up: should never block UI
|
if(holder) holder.innerHTML = '<div class="graph-loading">Строю граф...</div>';
|
||||||
fetchGraph({ status: ["running"] }).then(function(data){
|
ensureFullGraphLoaded().then(function(){
|
||||||
graphRunningData = data || {nodes:[], edges:[]};
|
var g = buildDepSubgraphFromDetail(uid);
|
||||||
}).catch(function(){ /* ignore */ });
|
if((g.nodes || []).length > 2){
|
||||||
};
|
showGraphModal("Граф зависимостей: " + escHtml(instanceName(uid)), g, uid);
|
||||||
if(window.requestIdleCallback){
|
} else {
|
||||||
requestIdleCallback(warm, { timeout: 3000 });
|
drawGraph(holderId, g, uid);
|
||||||
} else {
|
}
|
||||||
setTimeout(warm, 3000);
|
}).catch(function(e){
|
||||||
}
|
var el = document.getElementById(holderId);
|
||||||
|
if(el) el.innerHTML = '<div class="err-msg">' + escHtml(e.message) + '</div>';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getToken(){ return localStorage.getItem("deck_token") || ""; }
|
|
||||||
|
|
||||||
function getDeckEnv(){
|
function getDeckEnv(){
|
||||||
return localStorage.getItem("deck_env") || "test";
|
return localStorage.getItem("deck_env") || "test";
|
||||||
@@ -1265,10 +1073,9 @@ function initApp(){
|
|||||||
document.getElementById("chk-auxiliary").addEventListener("change", function(){
|
document.getElementById("chk-auxiliary").addEventListener("change", function(){
|
||||||
cache = {};
|
cache = {};
|
||||||
render(filteredInstances());
|
render(filteredInstances());
|
||||||
if(graphRunningData){ graphDrawing = false; drawGraph("running-graph", graphRunningData, ""); }
|
if(graphRunningData){ drawGraph("running-graph", graphRunningData, ""); }
|
||||||
});
|
});
|
||||||
loadAllDetails();
|
loadAllDetails();
|
||||||
scheduleGraphWarmup();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("btn-login").addEventListener("click", doLogin);
|
document.getElementById("btn-login").addEventListener("click", doLogin);
|
||||||
@@ -1292,45 +1099,6 @@ if(getToken()) initApp();
|
|||||||
window.navigateToDep = navigateToDep;
|
window.navigateToDep = navigateToDep;
|
||||||
window.navBack = navBack;
|
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<allInstances.length;j++){
|
|
||||||
if(allInstances[j].instanceUid === rootUid){ inst = allInstances[j]; break; }
|
|
||||||
}
|
|
||||||
if(inst) filtered.push({
|
|
||||||
id: rootUid, label: inst.displayName || rootUid,
|
|
||||||
status: inst.explainedStatus || "unknown", svc: inst.svc || "",
|
|
||||||
lane: "regular", created: inst.instanceConfigDtCreated || inst.dtCreated || ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { nodes: filtered, edges: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user