v9.19: rewrite graph with pure SVG + panzoom (pan/zoom), remove cytoscape/heavy code
This commit is contained in:
+248
-480
@@ -249,6 +249,7 @@
|
||||
</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>
|
||||
<script src="https://cdn.jsdelivr.net/npm/panzoom@9.4.3/dist/panzoom.min.js"></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;}};
|
||||
</script>
|
||||
@@ -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<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(){
|
||||
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 = '<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);
|
||||
}
|
||||
});
|
||||
// Build subgraph from rawDetail dependencies (BFS, no edges needed)
|
||||
function buildDepSubgraphFromDetail(rootUid){
|
||||
var seen = 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();
|
||||
frontier.forEach(function(n){
|
||||
(adj[n] || new Set()).forEach(function(x){ if(!seen.has(x)) nxt.add(x); });
|
||||
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;
|
||||
}
|
||||
return {
|
||||
nodes: nodes.filter(function(n){ return seen.has(n.id); }),
|
||||
edges: edges.filter(function(e){ return seen.has(e.source) && seen.has(e.target); })
|
||||
};
|
||||
var allNodes = (graphFullData && graphFullData.nodes) || [];
|
||||
var filtered = allNodes.filter(function(n){ return seen.has(n.id); });
|
||||
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(){
|
||||
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 = '<div class="graph-loading">Собираю граф...</div>';
|
||||
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 = '<div class="err-msg">' + escHtml(e.message) + '</div>';
|
||||
var el2 = document.getElementById("running-graph");
|
||||
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");
|
||||
}
|
||||
|
||||
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 = '<div class="graph-loading">Строю граф...</div>';
|
||||
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 = '<div class="err-msg">' + escHtml(e.message) + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
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<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>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user