diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 88f0fb3..db0017c 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -15,7 +15,7 @@ spec: spec: containers: - name: cloud-dashboard - image: naeel/cloud-dashboard:v9.43 + image: naeel/cloud-dashboard:v9.44 imagePullPolicy: Always ports: - containerPort: 8080 diff --git a/main.py b/main.py index 7d0a075..08b4bee 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,6 @@ from fastapi.responses import StreamingResponse from fastapi.staticfiles import StaticFiles import httpx from typing import Annotated -import asyncio import json import time from datetime import datetime, timezone @@ -16,8 +15,6 @@ DECK_APIS = { "test": "https://deck-api-test.ngcloud.ru/api/v1", } DEFAULT_DECK_ENV = "test" -CACHE_TTL_SECONDS = 180 -GRAPH_CACHE: dict[str, tuple[float, dict]] = {} # Centralized backend policy for table-related derived fields. DELETE_POLICY = { @@ -55,38 +52,6 @@ def _as_text(value) -> str: return "" -def extract_platform_name(inst: dict | None) -> str: - if not isinstance(inst, dict): - return "" - - sources = [ - inst.get("parameters"), - inst.get("params"), - inst.get("instanceParameters"), - inst.get("inputParameters"), - ] - needles = ("platform", "kuber", "k8s", "cluster", "shturval", "штурвал") - - for src in sources: - if isinstance(src, dict): - for key, value in src.items(): - key_s = str(key).strip().lower() - if any(n in key_s for n in needles): - txt = _as_text(value) - if txt: - return txt - elif isinstance(src, list): - for item in src: - if not isinstance(item, dict): - continue - key_s = str(item.get("name") or item.get("key") or item.get("code") or "").strip().lower() - if any(n in key_s for n in needles): - txt = _as_text(item.get("value") or item.get("displayValue") or item.get("text")) - if txt: - return txt - return "" - - def _parse_ts(value) -> float | None: if not value: return None @@ -204,146 +169,6 @@ async def list_instances( return {"instances": all_results, "total": len(all_results)} -@app.get("/api/graph") -async def get_graph( - request: Request, - x_deck_token: Annotated[str | None, Header()] = None, - x_deck_env: Annotated[str | None, Header()] = None, -): - if not x_deck_token: - raise HTTPException(status_code=401, detail="Token required") - env, deck_api = resolve_deck_api(x_deck_env) - - status_list = request.query_params.getlist("status") - root_uid = request.query_params.get("root_uid") - depth_raw = request.query_params.get("depth", "2") - try: - depth = max(1, min(5, int(depth_raw))) - except ValueError: - depth = 2 - - status_key = ",".join(sorted(status_list)) if status_list else "__all__" - cache_key = f"{x_deck_token[:24]}::{env}::{status_key}" - now = time.time() - - cached = GRAPH_CACHE.get(cache_key) - if cached and (now - cached[0] < CACHE_TTL_SECONDS): - base_graph = cached[1] - else: - all_instances = await fetch_all_instances(x_deck_token, deck_api) - if status_list: - all_instances = [i for i in all_instances if i.get("explainedStatus") in status_list] - - uid_to_inst = { - i.get("instanceUid"): i - for i in all_instances - if i.get("instanceUid") - } - allowed_uids = set(uid_to_inst.keys()) - - detail_map: dict[str, dict] = {} - async with httpx.AsyncClient(timeout=25) as shared_client: - for uid in allowed_uids: - try: - r = await shared_client.get( - f"{deck_api}/index.cfm/instances/{uid}", - headers=auth_headers(x_deck_token), - ) - if r.status_code == 401: - raise HTTPException(status_code=401, detail="Invalid token") - if r.status_code >= 400: - continue - inst = r.json().get("instance") - if inst: - detail_map[uid] = inst - except httpx.TimeoutException: - continue - - # Categorise singletons by service name - _PLATFORM_PATTERNS = ("виртуальный датацентр", "vdc", "virtual data center", "cloud director", "kubernetes", "k8s") - _SINGLETON_PATTERNS = ("object storage", "s3", "edge", "шлюз периметра", "dns", "container registry", "реестр контейнеров") - - def _is_platform_svc(svc_name: str) -> bool: - s = svc_name.strip().lower() - return any(p in s for p in _PLATFORM_PATTERNS) - - def _is_singleton_svc(svc_name: str) -> bool: - s = svc_name.strip().lower() - return any(p in s for p in _SINGLETON_PATTERNS) - - # Build realm→platform_uid map: instances whose resourceRealmCnt > 0 ARE platforms - realm_to_platform: dict[str, str] = {} - for uid_d, inst_d in detail_map.items(): - cnt = inst_d.get("resourceRealmCnt") or 0 - realm_name = inst_d.get("resourceRealm") or "" - if cnt and realm_name: - realm_to_platform[realm_name] = uid_d - - nodes = [] - for uid_i in allowed_uids: - i = uid_to_inst.get(uid_i, {}) - detail = detail_map.get(uid_i, {}) - svc = i.get("svc") or "" - realm_cnt = detail.get("resourceRealmCnt") or 0 - is_aux = bool(detail.get("isAuxiliary")) - # Determine lane by service name - if _is_platform_svc(svc): - lane = "platform" - elif _is_singleton_svc(svc): - lane = "singleton" - else: - lane = "regular" - nodes.append( - { - "id": uid_i, - "label": i.get("displayName") or uid_i, - "status": i.get("explainedStatus") or "unknown", - "svc": svc, - "realm": i.get("resourceRealm") or "", - "lane": lane, - "isAuxiliary": is_aux, - "created": i.get("instanceConfigDtCreated") or i.get("dtCreated") or "", - } - ) - - # Debug: log lane distribution - import sys - _lane_counts = {} - for _n in nodes: - _l = _n.get("lane", "unknown") - _lane_counts[_l] = _lane_counts.get(_l, 0) + 1 - print(f"[GRAPH] Lanes: {_lane_counts}, nodes: {len(nodes)}", flush=True) - for _n in nodes: - if _n["lane"] in ("platform", "singleton"): - print(f" {_n['lane'].upper()}: {_n['label']} | svc={_n['svc']}", flush=True) - - base_graph = {"nodes": nodes, "edges": []} - GRAPH_CACHE[cache_key] = (now, base_graph) - - # Return full graph fast - if not root_uid: - return { - "nodes": base_graph["nodes"], - "edges": [], - "total_nodes": len(base_graph["nodes"]), - "total_edges": 0, - "cached": bool(cached), - } - - node_ids = {n["id"] for n in base_graph["nodes"]} - if root_uid not in node_ids: - return {"nodes": [], "edges": [], "total_nodes": 0, "total_edges": 0, "cached": bool(cached)} - - sub_nodes = [n for n in base_graph["nodes"] if n["id"] == root_uid] - return { - "nodes": sub_nodes, - "edges": [], - "total_nodes": len(sub_nodes), - "total_edges": 0, - "cached": bool(cached), - } - - @app.get("/api/instances/stream") async def stream_instances( request: Request, diff --git a/static/index.html b/static/index.html index 215231e..36160b5 100644 --- a/static/index.html +++ b/static/index.html @@ -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 @@ - @@ -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 = '
Нет данных
'; - 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 = '
Собираю граф...
'; - ensureAllGraphLoaded() - .then(function(data){ drawGraph("running-graph", data || {nodes:[], edges:[]}, ""); }) - .catch(function(e){ - var el2 = document.getElementById("running-graph"); - if(el2) el2.innerHTML = '
' + escHtml(e.message) + '
'; - }); -} - -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 = '
Строю граф...
'; - ensureFullGraphLoaded().then(function(){ - var g = buildDepSubgraphFromDetail(uid); - if((g.nodes || []).length > 2){ - showGraphModal("Граф зависимостей: " + escHtml(instanceName(uid)), g, uid); - } else { - drawGraph(holderId, g, uid); - } - }).catch(function(e){ - var el = document.getElementById(holderId); - if(el) el.innerHTML = '
' + escHtml(e.message) + '
'; - }); -} - - function getToken(){ return localStorage.getItem("deck_token") || ""; } @@ -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");