v9.44: remove all graph code (backend endpoint, frontend SVG/panzoom/CSS, ~536 lines dead code)
This commit is contained in:
+2
-360
@@ -36,12 +36,8 @@
|
||||
.filter-label input { cursor: pointer; width: 18px; height: 18px; accent-color: #58a6ff; }
|
||||
.btn-refresh { padding: 5px 12px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; cursor: pointer; font-size: 12px; }
|
||||
.btn-refresh:hover { background: #30363d; }
|
||||
.btn-graph { padding: 5px 12px; background: #1f6feb; border: 1px solid #3b82f6; border-radius: 6px; color: #fff; cursor: pointer; font-size: 12px; }
|
||||
.btn-graph:hover { background: #2563eb; }
|
||||
.btn-theme { padding: 5px 10px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; cursor: pointer; font-size: 14px; line-height: 1; }
|
||||
.btn-theme:hover { background: #30363d; }
|
||||
.btn-graph { padding: 5px 12px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; cursor: pointer; font-size: 12px; }
|
||||
.btn-graph:hover { background: #30363d; }
|
||||
.header-user { display: flex; align-items: center; gap: 10px; }
|
||||
.user-email { font-size: 12px; color: #8b949e; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.btn-exit { padding: 5px 12px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #f85149; cursor: pointer; font-size: 12px; }
|
||||
@@ -100,20 +96,6 @@
|
||||
body.light .panel-history { background: #dcfce7; border-top: 2px solid #4ade80; }
|
||||
body.light .panel-deps { background: #fef3c7; border-top: 2px solid #f59e0b; }
|
||||
.panel.active { display: block; }
|
||||
.inst-graph { height: 420px; border: 1px solid #30363d; border-radius: 8px; background: #0b0f14; }
|
||||
.graph-hint { color: #8b949e; font-size: 12px; margin-bottom: 8px; }
|
||||
.graph-loading { display: flex; align-items: center; justify-content: center; height: 100%; color: #9fb3c8; font-size: 13px; }
|
||||
.graph-loading::before { content: ""; width: 14px; height: 14px; border: 2px solid #3b82f6; border-top-color: transparent; border-radius: 50%; display: inline-block; margin-right: 8px; animation: spin .9s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.graph-modal { position: fixed; inset: 0; background: rgba(2, 8, 20, .75); display: none; align-items: center; justify-content: center; z-index: 1200; }
|
||||
.graph-modal.open { display: flex; }
|
||||
.graph-modal-box { width: min(98vw, 1700px); height: min(95vh, 1100px); background: #0d1117; border: 1px solid #30363d; border-radius: 12px; display: flex; flex-direction: column; }
|
||||
.graph-modal-head { padding: 10px 14px; border-bottom: 1px solid #30363d; display: flex; align-items: center; gap: 10px; }
|
||||
.graph-modal-title { font-size: 14px; font-weight: 600; }
|
||||
.graph-modal-close { margin-left: auto; padding: 4px 10px; background: #21262d; border: 1px solid #30363d; color: #e1e4e8; border-radius: 6px; cursor: pointer; }
|
||||
.graph-modal-body { flex: 1; min-height: 0; }
|
||||
.running-graph { width: 100%; height: 100%; }
|
||||
.graph-inline-note { color: #8b949e; font-size: 12px; margin-top: 8px; }
|
||||
.params-grid { display: grid; grid-template-columns: max-content max-content; gap: 14px 68px; max-width: none; margin: 0; justify-content: start; align-content: start; }
|
||||
.params-section { min-width: 0; }
|
||||
.params-section h4 { font-size: 11px; font-weight: 600; color: #8b949e; text-transform: uppercase; margin-bottom: 8px; }
|
||||
@@ -169,8 +151,8 @@
|
||||
body.light .header-logo .logo-text { color: #0f172a; }
|
||||
body.light .header-logo .logo-sub, body.light .user-email { color: #475569; }
|
||||
body.light .data-ts { color: #64748b; }
|
||||
body.light .btn-refresh, body.light .btn-theme, body.light .btn-exit, body.light .btn-graph { background: #f8fafc; border-color: #cbd5e1; color: #0f172a; }
|
||||
body.light .btn-refresh:hover, body.light .btn-theme:hover, body.light .btn-exit:hover, body.light .btn-graph:hover { background: #e2e8f0; }
|
||||
body.light .btn-refresh, body.light .btn-theme, body.light .btn-exit { background: #f8fafc; border-color: #cbd5e1; color: #0f172a; }
|
||||
body.light .btn-refresh:hover, body.light .btn-theme:hover, body.light .btn-exit:hover { background: #e2e8f0; }
|
||||
body.light .main { background: #f3f6fb; }
|
||||
body.light .stat-card { background: #ffffff; border-color: #cbd5e1; }
|
||||
body.light .stat-card .lbl { color: #64748b; }
|
||||
@@ -199,10 +181,6 @@
|
||||
body.light .tabs { border-bottom-color: #b6c6e8; background: #dfe9fa; }
|
||||
body.light .tab { color: #475569; }
|
||||
body.light .tab.active { color: #1d4ed8; border-bottom-color: #1d4ed8; }
|
||||
body.light .graph-modal { background: rgba(15, 23, 42, .35); }
|
||||
body.light .graph-modal-box { background: #ffffff; border-color: #cbd5e1; }
|
||||
body.light .graph-modal-head { border-bottom-color: #cbd5e1; }
|
||||
body.light .graph-modal-close { background: #f8fafc; border-color: #cbd5e1; color: #0f172a; }
|
||||
body.light .p-item { border-bottom-color: #e2e8f0; color: #0f172a; }
|
||||
body.light .p-item:nth-child(odd) { background: rgba(148, 163, 184, .12); }
|
||||
body.light .p-item:nth-child(even) { background: rgba(148, 163, 184, .18); }
|
||||
@@ -304,7 +282,6 @@
|
||||
</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>
|
||||
@@ -318,11 +295,6 @@ var sortCol = "displayName";
|
||||
var sortDir = 1;
|
||||
var expandedUid = null;
|
||||
var cache = {};
|
||||
var graphAllData = null;
|
||||
var graphRunningData = null;
|
||||
var graphFullData = null;
|
||||
var graphAllKey = "";
|
||||
var graphFetchInFlight = false;
|
||||
var rawDetail = {};
|
||||
var navBackUid = null;
|
||||
var renderTimer = null;
|
||||
@@ -435,332 +407,6 @@ function visibleUidSet(){
|
||||
}
|
||||
|
||||
|
||||
// ─── 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;
|
||||
var p = new URLSearchParams();
|
||||
Object.keys(params || {}).forEach(function(k){
|
||||
var v = params[k];
|
||||
if(Array.isArray(v)) v.forEach(function(x){ p.append(k, x); });
|
||||
else if(v !== undefined && v !== null && v !== "") p.append(k, String(v));
|
||||
});
|
||||
return fetch(API + "/graph?" + p.toString(), { headers: apiHeaders() })
|
||||
.then(function(r){
|
||||
graphFetchInFlight = false;
|
||||
if(r.status === 401){ doLogout(); return null; }
|
||||
if(!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.json();
|
||||
})
|
||||
.catch(function(e){
|
||||
graphFetchInFlight = false;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
function ensureAllGraphLoaded(){
|
||||
var statuses = getChecked().slice().sort();
|
||||
var key = statuses.join(",");
|
||||
if(key === graphAllKey && graphAllData) return Promise.resolve(graphAllData);
|
||||
if(statuses.length === 0){
|
||||
graphAllKey = key;
|
||||
graphAllData = {nodes:[], edges:[]};
|
||||
return Promise.resolve(graphAllData);
|
||||
}
|
||||
return fetchGraph({ status: statuses }).then(function(data){
|
||||
graphAllKey = key;
|
||||
graphAllData = data || {nodes:[], edges:[]};
|
||||
return graphAllData;
|
||||
});
|
||||
}
|
||||
|
||||
function ensureFullGraphLoaded(){
|
||||
if(graphFullData) return Promise.resolve(graphFullData);
|
||||
return fetchGraph({}).then(function(data){
|
||||
graphFullData = data || {nodes:[], edges:[]};
|
||||
return graphFullData;
|
||||
});
|
||||
}
|
||||
|
||||
// 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 < 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); });
|
||||
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(", ") : "все");
|
||||
var target = document.getElementById("running-graph");
|
||||
if(target) target.innerHTML = '<div class="graph-loading">Собираю граф...</div>';
|
||||
ensureAllGraphLoaded()
|
||||
.then(function(data){ drawGraph("running-graph", data || {nodes:[], edges:[]}, ""); })
|
||||
.catch(function(e){
|
||||
var el2 = document.getElementById("running-graph");
|
||||
if(el2) el2.innerHTML = '<div class="err-msg">' + escHtml(e.message) + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function closeRunningGraph(){
|
||||
document.getElementById("graph-modal").classList.remove("open");
|
||||
}
|
||||
|
||||
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");
|
||||
drawGraph("running-graph", data || {nodes:[], edges:[]}, focusUid || "");
|
||||
}
|
||||
|
||||
// ── 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") || "";
|
||||
}
|
||||
@@ -1236,10 +882,6 @@ function scheduleRender(){
|
||||
allInstances = [];
|
||||
cache = {};
|
||||
rawDetail = {};
|
||||
graphAllData = null;
|
||||
graphRunningData = null;
|
||||
graphFullData = null;
|
||||
graphAllKey = "";
|
||||
expandedUid = null;
|
||||
navBackUid = null;
|
||||
var nb = document.getElementById("nav-back");
|
||||
|
||||
Reference in New Issue
Block a user