v9.12: remove all edges from graph (frontend + backend)
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.11
|
image: naeel/cloud-dashboard:v9.12
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user