v9.16: replace cytoscape with SVG rectangles for instant graph rendering
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.15
|
image: naeel/cloud-dashboard:v9.16
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
|
|||||||
+145
-73
@@ -205,7 +205,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button class="btn-refresh" id="btn-refresh">↻ Обновить</button>
|
<button class="btn-refresh" id="btn-refresh">↻ Обновить</button>
|
||||||
<button class="btn-graph" id="btn-running-graph">Граф зависимостей</button>
|
|
||||||
<button class="btn-theme" id="btn-theme" title="Сменить тему">🌙</button>
|
<button class="btn-theme" id="btn-theme" title="Сменить тему">🌙</button>
|
||||||
<div class="header-user">
|
<div class="header-user">
|
||||||
<span class="user-email" id="user-email"></span>
|
<span class="user-email" id="user-email"></span>
|
||||||
@@ -250,8 +250,7 @@
|
|||||||
<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>
|
||||||
<script src="https://unpkg.com/cytoscape@3.29.2/dist/cytoscape.min.js"></script>
|
|
||||||
<script src="https://unpkg.com/cytoscape-node-html-label@1.2.2/dist/cytoscape-node-html-label.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
"use strict";
|
"use strict";
|
||||||
@@ -461,6 +460,31 @@ function humanAge(dtStr){
|
|||||||
return mins + " мин";
|
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){
|
function graphElements(data, focusUid, laneById){
|
||||||
var elems = [];
|
var elems = [];
|
||||||
(data.nodes || []).forEach(function(n){
|
(data.nodes || []).forEach(function(n){
|
||||||
@@ -509,86 +533,132 @@ function drawGraph(containerId, data, focusUid, forceShowAux){
|
|||||||
if(graphDrawing) return;
|
if(graphDrawing) return;
|
||||||
graphDrawing = true;
|
graphDrawing = true;
|
||||||
var el = document.getElementById(containerId);
|
var el = document.getElementById(containerId);
|
||||||
if(!el || !window.cytoscape) { graphDrawing = false; return; }
|
if(!el) { graphDrawing = false; return; }
|
||||||
el.innerHTML = "";
|
el.innerHTML = "";
|
||||||
if((data.nodes || []).length === 0){
|
if((data.nodes || []).length === 0){
|
||||||
el.innerHTML = '<div class="empty-msg" style="padding:14px">Загрузка графа... Если долго — обновите страницу.</div>';
|
el.innerHTML = '<div class="empty-msg" style="padding:14px">Нет узлов для отображения</div>';
|
||||||
graphDrawing = false;
|
graphDrawing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var layoutMeta = buildRowLayout(data, el.clientWidth || 1200, forceShowAux);
|
var layoutMeta = buildRowLayout(data, el.clientWidth || 1200, forceShowAux);
|
||||||
var posMap = layoutMeta.positions;
|
var posMap = layoutMeta.positions;
|
||||||
// Use filtered nodes if available
|
var visibleNodes = data.nodes || [];
|
||||||
var visibleNodes = data._filteredNodes || data.nodes || [];
|
|
||||||
var visibleIds = new Set(visibleNodes.map(function(n){ return n.id; }));
|
|
||||||
var filteredData = {
|
|
||||||
nodes: visibleNodes,
|
|
||||||
edges: []
|
|
||||||
};
|
|
||||||
var layoutCfg = {
|
|
||||||
name: "preset",
|
|
||||||
fit: true,
|
|
||||||
padding: 24,
|
|
||||||
positions: function(node){ return posMap[node.id()] || {x: 0, y: 0}; },
|
|
||||||
animate: false
|
|
||||||
};
|
|
||||||
|
|
||||||
var cy = cytoscape({
|
// Calculate container bounds
|
||||||
container: el,
|
var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
||||||
elements: graphElements(filteredData, focusUid, layoutMeta.laneById),
|
visibleNodes.forEach(function(n){
|
||||||
autoungrabify: true,
|
var pos = posMap[n.id];
|
||||||
style: [
|
if(!pos) return;
|
||||||
{ selector: "node", style: {
|
var w = n.nodeWidth || 200;
|
||||||
"shape": "round-rectangle",
|
var h = n.nodeHeight || 80;
|
||||||
"background-color": "data(bgColor)",
|
minX = Math.min(minX, pos.x - w/2);
|
||||||
"label": "",
|
maxX = Math.max(maxX, pos.x + w/2);
|
||||||
"width": "data(width)",
|
minY = Math.min(minY, pos.y - h/2);
|
||||||
"height": "data(height)",
|
maxY = Math.max(maxY, pos.y + h/2);
|
||||||
"padding": "10px",
|
});
|
||||||
"border-color": "data(color)",
|
|
||||||
"border-width": 2.4
|
var padding = 40;
|
||||||
}},
|
var containerW = maxX - minX + 2*padding;
|
||||||
{ selector: "node.focus", style: { "border-color": "#facc15", "border-width": 3 }}
|
var containerH = maxY - minY + 2*padding;
|
||||||
],
|
var offsetX = -minX + padding;
|
||||||
layout: layoutCfg
|
var offsetY = -minY + padding;
|
||||||
|
|
||||||
|
// Create SVG container for scrolling
|
||||||
|
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.width = "100%";
|
||||||
|
svg.style.height = "100%";
|
||||||
|
svg.style.backgroundColor = "#0d1117";
|
||||||
|
svg.style.cursor = "default";
|
||||||
|
|
||||||
|
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||||
|
|
||||||
|
// Draw nodes
|
||||||
|
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: name (bold, underline), svc (italic), age (small)
|
||||||
|
var nameFs = Math.round((n.fontSize || 13) * 1.25);
|
||||||
|
var txtX = x;
|
||||||
|
var txtY = y - h/4;
|
||||||
|
|
||||||
|
// Name
|
||||||
|
var nameText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||||
|
nameText.setAttribute("x", txtX);
|
||||||
|
nameText.setAttribute("y", txtY);
|
||||||
|
nameText.setAttribute("text-anchor", "middle");
|
||||||
|
nameText.setAttribute("font-size", nameFs);
|
||||||
|
nameText.setAttribute("font-weight", "bold");
|
||||||
|
nameText.setAttribute("text-decoration", "underline");
|
||||||
|
nameText.setAttribute("fill", "#dbeafe");
|
||||||
|
nameText.setAttribute("font-family", "system-ui, sans-serif");
|
||||||
|
nameText.textContent = n.label || n.id || "";
|
||||||
|
nameText.style.pointerEvents = "none";
|
||||||
|
g.appendChild(nameText);
|
||||||
|
|
||||||
|
// Service
|
||||||
|
if(n.svc){
|
||||||
|
var svcText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||||
|
svcText.setAttribute("x", txtX);
|
||||||
|
svcText.setAttribute("y", txtY + nameFs * 0.8 + 3);
|
||||||
|
svcText.setAttribute("text-anchor", "middle");
|
||||||
|
svcText.setAttribute("font-size", (nameFs - 2));
|
||||||
|
svcText.setAttribute("font-style", "italic");
|
||||||
|
svcText.setAttribute("fill", "#93c5fd");
|
||||||
|
svcText.setAttribute("font-family", "system-ui, sans-serif");
|
||||||
|
svcText.textContent = n.svc;
|
||||||
|
svcText.style.pointerEvents = "none";
|
||||||
|
g.appendChild(svcText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Age
|
||||||
|
if(n.age){
|
||||||
|
var ageText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||||
|
ageText.setAttribute("x", txtX);
|
||||||
|
ageText.setAttribute("y", y + h/4 + 4);
|
||||||
|
ageText.setAttribute("text-anchor", "middle");
|
||||||
|
ageText.setAttribute("font-size", 9);
|
||||||
|
ageText.setAttribute("font-style", "italic");
|
||||||
|
ageText.setAttribute("fill", "#9ca3af");
|
||||||
|
ageText.setAttribute("font-family", "system-ui, sans-serif");
|
||||||
|
ageText.textContent = "⏱ " + n.age;
|
||||||
|
ageText.style.pointerEvents = "none";
|
||||||
|
g.appendChild(ageText);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// HTML labels: render name+svc normally, age as small italic
|
svg.appendChild(g);
|
||||||
if(cy.nodeHtmlLabel){
|
el.appendChild(svg);
|
||||||
cy.nodeHtmlLabel([{
|
|
||||||
query: 'node',
|
|
||||||
halign: 'center',
|
|
||||||
valign: 'center',
|
|
||||||
halignBox: 'center',
|
|
||||||
valignBox: 'center',
|
|
||||||
tpl: function(d){
|
|
||||||
var fs = d.fontSize || 13;
|
|
||||||
var nameFs = Math.round(fs * 1.25);
|
|
||||||
var html = '<div style="text-align:center;color:#dbeafe;font-family:system-ui,sans-serif;padding:4px 6px;">';
|
|
||||||
html += '<div style="font-size:'+nameFs+'px;font-weight:700;text-decoration:underline;word-break:break-word;white-space:normal;">' + (d.label||'') + '</div>';
|
|
||||||
if(d.svc) html += '<div style="font-size:'+(fs-1)+'px;font-style:italic;margin-top:3px;color:#93c5fd;">' + d.svc + '</div>';
|
|
||||||
if(d.age) html += '<div style="font-size:9px;font-style:italic;color:#9ca3af;margin-top:10px;">⏱ ' + d.age + '</div>';
|
|
||||||
html += '</div>';
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure proper viewport after tab activation/layout changes.
|
// Auto-fit on container size change
|
||||||
var refit = function(){
|
setTimeout(function(){ graphDrawing = false; }, 50);
|
||||||
try { cy.resize(); cy.fit(undefined, 24); } catch(e) { /* noop */ }
|
|
||||||
};
|
|
||||||
setTimeout(refit, 0);
|
|
||||||
setTimeout(refit, 120);
|
|
||||||
cy.on("layoutstop", refit);
|
|
||||||
// Release draw guard after layout settles.
|
|
||||||
setTimeout(function(){ graphDrawing = false; }, 300);
|
|
||||||
|
|
||||||
cy.on("tap", "node", function(evt){
|
|
||||||
var n = evt.target.data();
|
|
||||||
var tip = n.fullLabel + "\n" + (n.status || "unknown");
|
|
||||||
if(n.fullLabel) el.title = tip;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureAllGraphLoaded(){
|
function ensureAllGraphLoaded(){
|
||||||
@@ -622,6 +692,7 @@ function renderInstanceGraph(uid){
|
|||||||
var run = function(){
|
var run = function(){
|
||||||
ensureFullGraphLoaded().then(function(){
|
ensureFullGraphLoaded().then(function(){
|
||||||
var g = buildDepSubgraphFromDetail(uid);
|
var g = buildDepSubgraphFromDetail(uid);
|
||||||
|
enrichNodesWithMetadata(g);
|
||||||
if((g.nodes || []).length > 2){
|
if((g.nodes || []).length > 2){
|
||||||
if(holder) holder.innerHTML = '<div class="graph-inline-note">Граф открыт почти на весь экран для удобства чтения.</div>';
|
if(holder) holder.innerHTML = '<div class="graph-inline-note">Граф открыт почти на весь экран для удобства чтения.</div>';
|
||||||
showGraphModal("Граф зависимостей: " + escHtml(instanceName(uid)), g, uid, true);
|
showGraphModal("Граф зависимостей: " + escHtml(instanceName(uid)), g, uid, true);
|
||||||
@@ -703,7 +774,7 @@ function openRunningGraph(){
|
|||||||
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){ drawGraph("running-graph", data || {nodes:[], edges:[]}, ""); })
|
.then(function(data){ enrichNodesWithMetadata(data || {nodes:[], edges:[]}, ""); drawGraph("running-graph", data || {nodes:[], edges:[]}, ""); })
|
||||||
.catch(function(e){
|
.catch(function(e){
|
||||||
var el = document.getElementById("running-graph");
|
var el = document.getElementById("running-graph");
|
||||||
if(el) el.innerHTML = '<div class="err-msg">' + escHtml(e.message) + '</div>';
|
if(el) el.innerHTML = '<div class="err-msg">' + escHtml(e.message) + '</div>';
|
||||||
@@ -719,6 +790,7 @@ function showGraphModal(title, data, focusUid, forceShowAux){
|
|||||||
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 || "", forceShowAux);
|
drawGraph("running-graph", data || {nodes:[], edges:[]}, focusUid || "", forceShowAux);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user