v9.12: remove all edges from graph (frontend + backend)

This commit is contained in:
Naeel
2026-04-16 15:39:01 +03:00
parent 2ec13f85f2
commit 2813d52d3b
3 changed files with 16 additions and 140 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ spec:
spec: spec:
containers: containers:
- name: cloud-dashboard - name: cloud-dashboard
image: naeel/cloud-dashboard:v9.11 image: naeel/cloud-dashboard:v9.12
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8080 - containerPort: 8080
+7 -96
View File
@@ -1,10 +1,8 @@
from fastapi import FastAPI, Header, HTTPException, Request from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import StreamingResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import httpx import httpx
from typing import Annotated from typing import Annotated
import asyncio import asyncio
import json
import time import time
app = FastAPI() app = FastAPI()
@@ -193,21 +191,6 @@ async def get_graph(
if cnt and realm_name: if cnt and realm_name:
realm_to_platform[realm_name] = uid_d realm_to_platform[realm_name] = uid_d
# Build directed edges from dependencies/dependentInstances
# Arrow points TO the dependency (what you depend on)
edge_set: set[tuple[str, str]] = set()
for uid_d, inst_d in detail_map.items():
if uid_d not in allowed_uids:
continue
for dep in inst_d.get("dependencies", []) or []:
src = dep.get("uid") or dep.get("instanceUid") or ""
if src and src in allowed_uids and src != uid_d:
edge_set.add((uid_d, src)) # uid_d depends on src, arrow uid_d→src
for dep_on in inst_d.get("dependentInstances", []) or []:
dst = dep_on.get("uid") or dep_on.get("instanceUid") or ""
if dst and dst in allowed_uids and dst != uid_d:
edge_set.add((dst, uid_d)) # dst depends on uid_d, arrow dst→uid_d
nodes = [] nodes = []
for uid_i in allowed_uids: for uid_i in allowed_uids:
i = uid_to_inst.get(uid_i, {}) i = uid_to_inst.get(uid_i, {})
@@ -241,22 +224,21 @@ async def get_graph(
for _n in nodes: for _n in nodes:
_l = _n.get("lane", "unknown") _l = _n.get("lane", "unknown")
_lane_counts[_l] = _lane_counts.get(_l, 0) + 1 _lane_counts[_l] = _lane_counts.get(_l, 0) + 1
print(f"[GRAPH] Lanes: {_lane_counts}, nodes: {len(nodes)}, edges: {len(edge_set)}", flush=True) print(f"[GRAPH] Lanes: {_lane_counts}, nodes: {len(nodes)}", flush=True)
for _n in nodes: for _n in nodes:
if _n["lane"] in ("platform", "singleton"): if _n["lane"] in ("platform", "singleton"):
print(f" {_n['lane'].upper()}: {_n['label']} | svc={_n['svc']}", flush=True) print(f" {_n['lane'].upper()}: {_n['label']} | svc={_n['svc']}", flush=True)
edges = [{"source": s, "target": t} for s, t in sorted(edge_set)] base_graph = {"nodes": nodes, "edges": []}
base_graph = {"nodes": nodes, "edges": edges}
GRAPH_CACHE[cache_key] = (now, base_graph) GRAPH_CACHE[cache_key] = (now, base_graph)
# Return full graph fast # Return full graph fast
if not root_uid: if not root_uid:
return { return {
"nodes": base_graph["nodes"], "nodes": base_graph["nodes"],
"edges": base_graph["edges"], "edges": [],
"total_nodes": len(base_graph["nodes"]), "total_nodes": len(base_graph["nodes"]),
"total_edges": len(base_graph["edges"]), "total_edges": 0,
"cached": bool(cached), "cached": bool(cached),
} }
@@ -264,87 +246,16 @@ async def get_graph(
if root_uid not in node_ids: if root_uid not in node_ids:
return {"nodes": [], "edges": [], "total_nodes": 0, "total_edges": 0, "cached": bool(cached)} return {"nodes": [], "edges": [], "total_nodes": 0, "total_edges": 0, "cached": bool(cached)}
adj: dict[str, set[str]] = {uid: set() for uid in node_ids} sub_nodes = [n for n in base_graph["nodes"] if n["id"] == root_uid]
for e in base_graph["edges"]:
s, t = e["source"], e["target"]
if s in adj and t in adj:
adj[s].add(t)
adj[t].add(s)
frontier = {root_uid}
seen = {root_uid}
for _ in range(depth):
nxt = set()
for node in frontier:
nxt |= adj.get(node, set())
nxt -= seen
if not nxt:
break
seen |= nxt
frontier = nxt
sub_nodes = [n for n in base_graph["nodes"] if n["id"] in seen]
sub_edges = [e for e in base_graph["edges"] if e["source"] in seen and e["target"] in seen]
return { return {
"nodes": sub_nodes, "nodes": sub_nodes,
"edges": sub_edges, "edges": [],
"total_nodes": len(sub_nodes), "total_nodes": len(sub_nodes),
"total_edges": len(sub_edges), "total_edges": 0,
"cached": bool(cached), "cached": bool(cached),
} }
@app.get("/api/instances/stream")
async def stream_instances(
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")
_, deck_api = resolve_deck_api(x_deck_env)
all_instances = await fetch_all_instances(x_deck_token, deck_api)
uid_to_inst = {
i.get("instanceUid"): i
for i in all_instances
if i.get("instanceUid")
}
async def generate():
sem = asyncio.Semaphore(20)
queue: asyncio.Queue = asyncio.Queue()
total = len(uid_to_inst)
async def fetch_one(uid: str, client: httpx.AsyncClient):
async with sem:
try:
r = await client.get(
f"{deck_api}/index.cfm/instances/{uid}",
headers=auth_headers(x_deck_token),
)
detail = None if r.status_code >= 400 else r.json().get("instance")
except Exception:
detail = None
await queue.put((uid, detail))
async with httpx.AsyncClient(timeout=25) as shared_client:
tasks = [
asyncio.create_task(fetch_one(uid, shared_client))
for uid in uid_to_inst
]
for _ in range(total):
uid, detail = await queue.get()
row = dict(uid_to_inst.get(uid, {}))
if detail:
row["_detail"] = detail
yield json.dumps(row, ensure_ascii=False) + "\n"
yield json.dumps({"_done": True, "total": total}, ensure_ascii=False) + "\n"
return StreamingResponse(generate(), media_type="application/x-ndjson")
@app.get("/api/instances/{uid}") @app.get("/api/instances/{uid}")
async def get_instance( async def get_instance(
request: Request, request: Request,
+8 -43
View File
@@ -81,12 +81,12 @@
.tab { padding: 7px 14px; font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; color: #8b949e; } .tab { padding: 7px 14px; font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; color: #8b949e; }
.tab.active { color: #58a6ff; border-bottom-color: #58a6ff; } .tab.active { color: #58a6ff; border-bottom-color: #58a6ff; }
.panel { display: none; } .panel { display: none; }
.panel-params { background: #0d1117; } .panel-params { background: #111827; border-top: 2px solid #1e3a5f; }
.panel-history { background: #0b0f1a; } .panel-history { background: #0f1a0f; border-top: 2px solid #1a4a2e; }
.panel-deps { background: #0f1107; } .panel-deps { background: #1a1209; border-top: 2px solid #3d2a00; }
body.light .panel-params { background: #f8fafc; } body.light .panel-params { background: #eff6ff; border-top: 2px solid #bfdbfe; }
body.light .panel-history { background: #f0f4ff; } body.light .panel-history { background: #f0fdf4; border-top: 2px solid #bbf7d0; }
body.light .panel-deps { background: #f0fff4; } body.light .panel-deps { background: #fffbeb; border-top: 2px solid #fde68a; }
.panel.active { display: block; } .panel.active { display: block; }
.inst-graph { height: 420px; border: 1px solid #30363d; border-radius: 8px; background: #0b0f14; } .inst-graph { height: 420px; border: 1px solid #30363d; border-radius: 8px; background: #0b0f14; }
.graph-hint { color: #8b949e; font-size: 12px; margin-bottom: 8px; } .graph-hint { color: #8b949e; font-size: 12px; margin-bottom: 8px; }
@@ -412,14 +412,6 @@ function buildRowLayout(data, containerWidth, forceShowAux){
return { positions: positions, laneById: laneById }; return { positions: positions, laneById: laneById };
} }
function edgeColorForRows(sourceRow, targetRow, edgeIndex){
var palette = ["#e11d48", "#0ea5e9", "#22c55e", "#f59e0b", "#a855f7", "#14b8a6", "#f97316"];
var s = Number.isFinite(sourceRow) ? sourceRow : 0;
var t = Number.isFinite(targetRow) ? targetRow : 0;
var idx = Math.abs((s * 11) + (t * 17) + edgeIndex) % palette.length;
return palette[idx];
}
// Service-based background colors for graph nodes // Service-based background colors for graph nodes
var SVC_COLORS = { var SVC_COLORS = {
"cloud director": "#1e3a5f", "cloud director": "#1e3a5f",
@@ -503,20 +495,6 @@ function graphElements(data, focusUid, laneById){
classes: (focusUid && n.id === focusUid) ? "focus" : "" classes: (focusUid && n.id === focusUid) ? "focus" : ""
}); });
}); });
(data.edges || []).forEach(function(e, i){
var sourceRow = Number.isFinite(laneById[e.source]) ? laneById[e.source] : 0;
var targetRow = Number.isFinite(laneById[e.target]) ? laneById[e.target] : 0;
elems.push({
data: {
id: "e" + i + "-" + e.source + "-" + e.target,
source: e.source,
target: e.target,
sourceRow: sourceRow,
targetRow: targetRow,
color: edgeColorForRows(sourceRow, targetRow, i)
}
});
});
return elems; return elems;
} }
@@ -538,7 +516,7 @@ function drawGraph(containerId, data, focusUid, forceShowAux){
var visibleIds = new Set(visibleNodes.map(function(n){ return n.id; })); var visibleIds = new Set(visibleNodes.map(function(n){ return n.id; }));
var filteredData = { var filteredData = {
nodes: visibleNodes, nodes: visibleNodes,
edges: (data.edges || []).filter(function(e){ return visibleIds.has(e.source) && visibleIds.has(e.target); }) edges: []
}; };
var layoutCfg = { var layoutCfg = {
name: "preset", name: "preset",
@@ -563,20 +541,7 @@ function drawGraph(containerId, data, focusUid, forceShowAux){
"border-color": "data(color)", "border-color": "data(color)",
"border-width": 2.4 "border-width": 2.4
}}, }},
{ selector: "node.focus", style: { "border-color": "#facc15", "border-width": 3 }}, { selector: "node.focus", style: { "border-color": "#facc15", "border-width": 3 }}
{ selector: "edge", style: {
"line-color": "data(color)",
"target-arrow-shape": "none",
"source-arrow-shape": "none",
"mid-target-arrow-shape": "none",
"mid-source-arrow-shape": "none",
"curve-style": "taxi",
"taxi-direction": "auto",
"taxi-turn": "50px",
"taxi-turn-min-distance": "10px",
"width": 2,
"opacity": 0.7
}}
], ],
layout: layoutCfg layout: layoutCfg
}); });