sync: save local state before v9.7 work
This commit is contained in:
@@ -85,3 +85,17 @@ ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=
|
|||||||
|
|
||||||
Если есть сомнение, где выполнять команду:
|
Если есть сомнение, где выполнять команду:
|
||||||
- ОСТАНОВИСЬ и спроси оператора.
|
- ОСТАНОВИСЬ и спроси оператора.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🔁 VERSION & SYNC DISCIPLINE (ОБЯЗАТЕЛЬНО)
|
||||||
|
|
||||||
|
После КАЖДОГО изменения кода (любой файл с логикой/UI/API):
|
||||||
|
- ОБЯЗАТЕЛЬНО повысить версию для build/push (минимум patch).
|
||||||
|
- Перед build/push проверить, что новая версия действительно изменилась относительно предыдущей.
|
||||||
|
- Перед деплоем и после деплоя проверить, что используется именно новая версия, а не старая.
|
||||||
|
|
||||||
|
Проверка синхронизации локальной папки и ВМ:
|
||||||
|
- Считать, что `/home/naeel/remote_dev/dashboard` должен быть синхронизирован с `~/terra/dashboard` на ВМ.
|
||||||
|
- Перед build/push обязательно делать проверку синхронизации (например, сверка `sha256sum` ключевых файлов/артефактов между локальным путём и путём на ВМ).
|
||||||
|
- Если есть расхождение — ОСТАНОВИТЬСЯ, сначала устранить рассинхрон, только потом продолжать.
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: cloud-dashboard
|
- name: cloud-dashboard
|
||||||
image: naeel/cloud-dashboard:v1
|
image: naeel/cloud-dashboard:v9.7
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
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()
|
||||||
@@ -28,6 +30,53 @@ def resolve_deck_api(env_raw: str | None) -> tuple[str, str]:
|
|||||||
return env, DECK_APIS[env]
|
return env, DECK_APIS[env]
|
||||||
|
|
||||||
|
|
||||||
|
def _as_text(value) -> str:
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.strip()
|
||||||
|
if isinstance(value, (int, float, bool)):
|
||||||
|
return str(value).strip()
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for key in ("value", "displayValue", "name", "label", "id", "text"):
|
||||||
|
nested = _as_text(value.get(key))
|
||||||
|
if nested:
|
||||||
|
return nested
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
|
||||||
async def fetch_all_instances(token: str, deck_api: str):
|
async def fetch_all_instances(token: str, deck_api: str):
|
||||||
params: dict = {"page": 1, "size": 200}
|
params: dict = {"page": 1, "size": 200}
|
||||||
all_results = []
|
all_results = []
|
||||||
@@ -103,11 +152,11 @@ async def get_graph(
|
|||||||
}
|
}
|
||||||
allowed_uids = set(uid_to_inst.keys())
|
allowed_uids = set(uid_to_inst.keys())
|
||||||
|
|
||||||
sem = asyncio.Semaphore(16)
|
sem = asyncio.Semaphore(20)
|
||||||
|
|
||||||
async def fetch_detail(uid: str):
|
async def fetch_detail(uid: str, client: httpx.AsyncClient):
|
||||||
async with sem:
|
async with sem:
|
||||||
async with httpx.AsyncClient(timeout=20) as client:
|
try:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
f"{deck_api}/index.cfm/instances/{uid}",
|
f"{deck_api}/index.cfm/instances/{uid}",
|
||||||
headers=auth_headers(x_deck_token),
|
headers=auth_headers(x_deck_token),
|
||||||
@@ -117,38 +166,86 @@ async def get_graph(
|
|||||||
if r.status_code >= 400:
|
if r.status_code >= 400:
|
||||||
return uid, None
|
return uid, None
|
||||||
return uid, r.json().get("instance")
|
return uid, r.json().get("instance")
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
return uid, None
|
||||||
|
|
||||||
details = await asyncio.gather(*[fetch_detail(uid) for uid in allowed_uids])
|
async with httpx.AsyncClient(timeout=25) as shared_client:
|
||||||
|
details = await asyncio.gather(*[fetch_detail(uid, shared_client) for uid in allowed_uids])
|
||||||
detail_map = {uid: inst for uid, inst in details if inst}
|
detail_map = {uid: inst for uid, inst in details if inst}
|
||||||
|
|
||||||
edge_set = set()
|
# 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 dep_uid(dep_obj: dict):
|
def _is_platform_svc(svc_name: str) -> bool:
|
||||||
return dep_obj.get("uid") or dep_obj.get("instanceUid")
|
s = svc_name.strip().lower()
|
||||||
|
return any(p in s for p in _PLATFORM_PATTERNS)
|
||||||
|
|
||||||
for uid, inst in detail_map.items():
|
def _is_singleton_svc(svc_name: str) -> bool:
|
||||||
for dep in inst.get("dependencies", []) or []:
|
s = svc_name.strip().lower()
|
||||||
src = dep_uid(dep)
|
return any(p in s for p in _SINGLETON_PATTERNS)
|
||||||
if src and src in allowed_uids:
|
|
||||||
edge_set.add((src, uid))
|
# Build realm→platform_uid map: instances whose resourceRealmCnt > 0 ARE platforms
|
||||||
for dep_on in inst.get("dependentInstances", []) or []:
|
realm_to_platform: dict[str, str] = {}
|
||||||
dst = dep_uid(dep_on)
|
for uid_d, inst_d in detail_map.items():
|
||||||
if dst and dst in allowed_uids:
|
cnt = inst_d.get("resourceRealmCnt") or 0
|
||||||
edge_set.add((uid, dst))
|
realm_name = inst_d.get("resourceRealm") or ""
|
||||||
|
if cnt and realm_name:
|
||||||
|
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 in allowed_uids:
|
for uid_i in allowed_uids:
|
||||||
i = uid_to_inst.get(uid, {})
|
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(
|
nodes.append(
|
||||||
{
|
{
|
||||||
"id": uid,
|
"id": uid_i,
|
||||||
"label": i.get("displayName") or uid,
|
"label": i.get("displayName") or uid_i,
|
||||||
"status": i.get("explainedStatus") or "unknown",
|
"status": i.get("explainedStatus") or "unknown",
|
||||||
"svc": i.get("svc") or "",
|
"svc": svc,
|
||||||
"realm": i.get("resourceRealm") or "",
|
"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)}, edges: {len(edge_set)}", flush=True)
|
||||||
|
for _n in nodes:
|
||||||
|
if _n["lane"] in ("platform", "singleton"):
|
||||||
|
print(f" {_n['lane'].upper()}: {_n['label']} | svc={_n['svc']}", flush=True)
|
||||||
|
|
||||||
edges = [{"source": s, "target": t} for s, t in sorted(edge_set)]
|
edges = [{"source": s, "target": t} for s, t in sorted(edge_set)]
|
||||||
base_graph = {"nodes": nodes, "edges": edges}
|
base_graph = {"nodes": nodes, "edges": edges}
|
||||||
GRAPH_CACHE[cache_key] = (now, base_graph)
|
GRAPH_CACHE[cache_key] = (now, base_graph)
|
||||||
@@ -197,6 +294,57 @@ async def get_graph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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,
|
||||||
|
|||||||
+307
-75
@@ -33,6 +33,12 @@
|
|||||||
.filters { display: flex; gap: 10px; flex-wrap: wrap; }
|
.filters { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
.filter-label { display: flex; align-items: center; gap: 5px; cursor: pointer; font-size: 12px; }
|
.filter-label { display: flex; align-items: center; gap: 5px; cursor: pointer; font-size: 12px; }
|
||||||
.filter-label input { cursor: pointer; width: 18px; height: 18px; accent-color: #58a6ff; }
|
.filter-label input { cursor: pointer; width: 18px; height: 18px; accent-color: #58a6ff; }
|
||||||
|
.data-ts { font-size: 11px; color: #8b949e; white-space: nowrap; margin-right: 8px; }
|
||||||
|
.nav-back { padding: 6px 16px; background: #161b22; color: #8b949e; font-size: 12px; border-bottom: 1px solid #30363d; }
|
||||||
|
.nav-back a { color: #58a6ff; text-decoration: none; }
|
||||||
|
.nav-back a:hover { text-decoration: underline; }
|
||||||
|
.dep-item { cursor: pointer; border-radius: 4px; transition: background .15s; }
|
||||||
|
.dep-item:hover { background: #21262d; }
|
||||||
.btn-refresh { padding: 5px 12px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; cursor: pointer; font-size: 12px; }
|
.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-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 { padding: 5px 12px; background: #1f6feb; border: 1px solid #3b82f6; border-radius: 6px; color: #fff; cursor: pointer; font-size: 12px; }
|
||||||
@@ -179,10 +185,12 @@
|
|||||||
<label class="filter-label"><input type="checkbox" value="deleted"> <span class="status-badge s-deleted">deleted</span></label>
|
<label class="filter-label"><input type="checkbox" value="deleted"> <span class="status-badge s-deleted">deleted</span></label>
|
||||||
<label class="filter-label"><input type="checkbox" value="pending"> <span class="status-badge s-pending">pending</span></label>
|
<label class="filter-label"><input type="checkbox" value="pending"> <span class="status-badge s-pending">pending</span></label>
|
||||||
<label class="filter-label"><input type="checkbox" value="not created"> <span class="status-badge s-default">not created</span></label>
|
<label class="filter-label"><input type="checkbox" value="not created"> <span class="status-badge s-default">not created</span></label>
|
||||||
|
<label class="filter-label" style="margin-left:12px;border-left:1px solid #555;padding-left:12px"><input type="checkbox" id="chk-auxiliary"> <span style="font-size:12px;color:#8b949e">Служебные</span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
<span id="data-ts" style="font-size:11px;color:#8b949e;"></span>
|
||||||
<button class="btn-refresh" id="btn-refresh">↻ Обновить</button>
|
<button class="btn-refresh" id="btn-refresh">↻ Обновить</button>
|
||||||
<button class="btn-graph" id="btn-running-graph">Граф running</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>
|
||||||
@@ -191,6 +199,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="stats" id="stats"></div>
|
<div class="stats" id="stats"></div>
|
||||||
|
<div class="nav-back" id="nav-back" style="display:none"></div>
|
||||||
<div class="table-wrap" id="table-wrap">
|
<div class="table-wrap" id="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -227,6 +236,7 @@
|
|||||||
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@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";
|
||||||
@@ -238,8 +248,11 @@ var expandedUid = null;
|
|||||||
var cache = {};
|
var cache = {};
|
||||||
var graphAllData = null;
|
var graphAllData = null;
|
||||||
var graphRunningData = null;
|
var graphRunningData = null;
|
||||||
|
var graphFullData = null;
|
||||||
var graphAllKey = "";
|
var graphAllKey = "";
|
||||||
var graphWarmupScheduled = false;
|
var graphWarmupScheduled = false;
|
||||||
|
var graphDrawing = false;
|
||||||
|
var graphFetchInFlight = false;
|
||||||
|
|
||||||
function escHtml(v){
|
function escHtml(v){
|
||||||
return String(v == null ? "" : v)
|
return String(v == null ? "" : v)
|
||||||
@@ -264,6 +277,8 @@ function statusColor(s){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fetchGraph(params){
|
function fetchGraph(params){
|
||||||
|
if(graphFetchInFlight) return Promise.resolve(null);
|
||||||
|
graphFetchInFlight = true;
|
||||||
var p = new URLSearchParams();
|
var p = new URLSearchParams();
|
||||||
Object.keys(params || {}).forEach(function(k){
|
Object.keys(params || {}).forEach(function(k){
|
||||||
var v = params[k];
|
var v = params[k];
|
||||||
@@ -272,113 +287,303 @@ function fetchGraph(params){
|
|||||||
});
|
});
|
||||||
return fetch(API + "/graph?" + p.toString(), { headers: apiHeaders() })
|
return fetch(API + "/graph?" + p.toString(), { headers: apiHeaders() })
|
||||||
.then(function(r){
|
.then(function(r){
|
||||||
|
graphFetchInFlight = false;
|
||||||
if(r.status === 401){ doLogout(); return null; }
|
if(r.status === 401){ doLogout(); return null; }
|
||||||
if(!r.ok) throw new Error("HTTP " + r.status);
|
if(!r.ok) throw new Error("HTTP " + r.status);
|
||||||
return r.json();
|
return r.json();
|
||||||
|
})
|
||||||
|
.catch(function(e){
|
||||||
|
graphFetchInFlight = false;
|
||||||
|
throw e;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphElements(data, focusUid){
|
function normGraphText(v){
|
||||||
|
return String(v || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUserSingletonNode(n){
|
||||||
|
var t = [n.label, n.svc, n.id].map(normGraphText).join(" ");
|
||||||
|
return /cloud director|cloud_director|(^|\W)s3(\W|$)|(^|\W)edge(\W|$)/.test(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlatformNode(n, platformNames){
|
||||||
|
var label = normGraphText(n.label);
|
||||||
|
var id = normGraphText(n.id);
|
||||||
|
var svc = normGraphText(n.svc);
|
||||||
|
if(platformNames.size > 0){
|
||||||
|
if(platformNames.has(label) || platformNames.has(id)) return true;
|
||||||
|
var matched = false;
|
||||||
|
platformNames.forEach(function(p){
|
||||||
|
if(matched || !p) return;
|
||||||
|
if(label.indexOf(p) >= 0 || id.indexOf(p) >= 0) matched = true;
|
||||||
|
});
|
||||||
|
if(matched) return true;
|
||||||
|
}
|
||||||
|
return /platform|kuber|k8s|cluster|shturval|штурвал/.test(svc + " " + label);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRowLayout(data, containerWidth, forceShowAux){
|
||||||
|
var MAX_PER_ROW = 5;
|
||||||
|
var nodes = data.nodes || [];
|
||||||
|
var showAux = document.getElementById("chk-auxiliary");
|
||||||
|
var hideAux = forceShowAux ? false : (showAux ? !showAux.checked : true);
|
||||||
|
|
||||||
|
var filtered = hideAux ? nodes.filter(function(n){ return !n.isAuxiliary; }) : nodes;
|
||||||
|
if(filtered.length === 0) filtered = nodes;
|
||||||
|
|
||||||
|
var platforms = [];
|
||||||
|
var singletons = [];
|
||||||
|
var rest = [];
|
||||||
|
|
||||||
|
filtered.forEach(function(n){
|
||||||
|
var lane = n.lane || "regular";
|
||||||
|
if(lane === "platform"){ platforms.push(n); return; }
|
||||||
|
if(lane === "singleton"){ singletons.push(n); return; }
|
||||||
|
rest.push(n);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper: split array into chunks of max N
|
||||||
|
function chunkArray(arr, n){
|
||||||
|
var res = [];
|
||||||
|
for(var i=0; i<arr.length; i+=n) res.push(arr.slice(i, i+n));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build visual rows: each row is an array of nodes, max MAX_PER_ROW per row
|
||||||
|
// Category label rows are tracked for separators
|
||||||
|
var allRows = []; // each entry: {nodes:[], category:string}
|
||||||
|
var platRows = chunkArray(platforms, MAX_PER_ROW);
|
||||||
|
for(var p=0; p<platRows.length; p++) allRows.push({nodes: platRows[p], category: "platform"});
|
||||||
|
|
||||||
|
var singleRows = chunkArray(singletons, MAX_PER_ROW);
|
||||||
|
for(var s=0; s<singleRows.length; s++) allRows.push({nodes: singleRows[s], category: "singleton"});
|
||||||
|
|
||||||
|
var restRows = chunkArray(rest, MAX_PER_ROW);
|
||||||
|
for(var rr=0; rr<restRows.length; rr++) allRows.push({nodes: restRows[rr], category: "regular"});
|
||||||
|
|
||||||
|
if(allRows.length === 0) allRows = [{nodes: nodes.slice(0, MAX_PER_ROW), category: "regular"}];
|
||||||
|
|
||||||
|
var usableWidth = Math.max(860, containerWidth || 0);
|
||||||
|
var positions = {};
|
||||||
|
var laneById = {};
|
||||||
|
var nodeW = 200;
|
||||||
|
var gapX = 50;
|
||||||
|
var gapY = 160;
|
||||||
|
var categoryGap = 80; // extra gap between categories
|
||||||
|
var currentY = 60;
|
||||||
|
var prevCategory = "";
|
||||||
|
|
||||||
|
for(var r=0; r<allRows.length; r++){
|
||||||
|
var row = allRows[r];
|
||||||
|
// Add extra gap when switching category
|
||||||
|
if(prevCategory && row.category !== prevCategory) currentY += categoryGap;
|
||||||
|
prevCategory = row.category;
|
||||||
|
|
||||||
|
var items = row.nodes;
|
||||||
|
var totalW = items.length * nodeW + (items.length - 1) * gapX;
|
||||||
|
var xStart = Math.max(40, (usableWidth - totalW) / 2);
|
||||||
|
for(var i=0; i<items.length; i++){
|
||||||
|
positions[items[i].id] = { x: xStart + i * (nodeW + gapX), y: currentY };
|
||||||
|
laneById[items[i].id] = r;
|
||||||
|
}
|
||||||
|
currentY += gapY;
|
||||||
|
}
|
||||||
|
|
||||||
|
data._filteredNodes = filtered;
|
||||||
|
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
|
||||||
|
var SVC_COLORS = {
|
||||||
|
"cloud director": "#1e3a5f",
|
||||||
|
"kubernetes": "#2d4a22",
|
||||||
|
"object storage": "#4a3728",
|
||||||
|
"s3": "#4a3728",
|
||||||
|
"dns": "#3b2d50",
|
||||||
|
"container registry": "#2a3f4f",
|
||||||
|
"apache nifi": "#4a3020",
|
||||||
|
"postgresql": "#1a3550",
|
||||||
|
"rabbitmq": "#4a3040",
|
||||||
|
"grafana": "#3a2a10",
|
||||||
|
"keycloak": "#2a3a3a",
|
||||||
|
"terraform": "#2a2050",
|
||||||
|
"fission": "#3a2040",
|
||||||
|
"edge gateway": "#1a4040"
|
||||||
|
};
|
||||||
|
function svcBgColor(svc){
|
||||||
|
var s = (svc || "").trim().toLowerCase();
|
||||||
|
for(var k in SVC_COLORS){ if(s.indexOf(k) !== -1) return SVC_COLORS[k]; }
|
||||||
|
// Hash fallback
|
||||||
|
var h = 0;
|
||||||
|
for(var i=0; i<s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
|
||||||
|
var hue = ((h % 360) + 360) % 360;
|
||||||
|
return "hsl(" + hue + ",30%,18%)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute human-readable age from ISO date string
|
||||||
|
function humanAge(dtStr){
|
||||||
|
if(!dtStr) return "";
|
||||||
|
var d;
|
||||||
|
try { d = new Date(dtStr); } catch(e){ return ""; }
|
||||||
|
if(isNaN(d.getTime())) return "";
|
||||||
|
var diff = Date.now() - d.getTime();
|
||||||
|
if(diff < 0) return "";
|
||||||
|
var mins = Math.floor(diff / 60000);
|
||||||
|
var hrs = Math.floor(mins / 60);
|
||||||
|
var days = Math.floor(hrs / 24);
|
||||||
|
var months = Math.floor(days / 30);
|
||||||
|
var years = Math.floor(days / 365);
|
||||||
|
if(years >= 1) return years + "г " + (months % 12) + "м";
|
||||||
|
if(months >= 1) return months + "м " + (days % 30) + "д";
|
||||||
|
if(days >= 1) return days + "д";
|
||||||
|
if(hrs >= 1) return hrs + "ч";
|
||||||
|
return mins + "мин";
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphElements(data, focusUid, laneById){
|
||||||
var elems = [];
|
var elems = [];
|
||||||
(data.nodes || []).forEach(function(n){
|
(data.nodes || []).forEach(function(n){
|
||||||
var label = (n.label || n.id || "");
|
var name = (n.label || n.id || "");
|
||||||
var labelLen = label.length;
|
var svc = n.svc || "";
|
||||||
var textMaxWidth = Math.max(170, Math.min(320, labelLen * 8));
|
var age = humanAge(n.created);
|
||||||
var width = Math.max(190, Math.min(360, textMaxWidth + 26));
|
// Build label: name + svc + (2 blank lines) + age (small font)
|
||||||
var lineChars = Math.max(10, Math.floor(textMaxWidth / 8));
|
var label = name;
|
||||||
var lines = Math.max(1, Math.min(4, Math.ceil(labelLen / lineChars)));
|
if(svc) label += "\n" + svc;
|
||||||
var height = Math.max(64, Math.min(150, 36 + (lines * 20)));
|
if(age) label += "\n\n\u23f1 " + age;
|
||||||
var fontSize = labelLen > 48 ? 14 : labelLen > 32 ? 16 : 18;
|
// Fixed node width; text wraps inside
|
||||||
|
var nodeFixedW = 200;
|
||||||
|
var textMaxWidth = nodeFixedW - 20;
|
||||||
|
// Estimate lines needed for name wrapping
|
||||||
|
var charsPerLine = Math.floor(textMaxWidth / 8);
|
||||||
|
var nameLines = Math.max(1, Math.ceil(name.length / charsPerLine));
|
||||||
|
var totalLines = nameLines + (svc ? 1 : 0) + (age ? 3 : 0);
|
||||||
|
var height = Math.max(50, 16 + totalLines * 20);
|
||||||
|
var fontSize = age ? (name.length > 36 ? 10 : name.length > 24 ? 11 : 12) : (name.length > 36 ? 11 : name.length > 24 ? 12 : 13);
|
||||||
|
var bgColor = svcBgColor(svc);
|
||||||
elems.push({
|
elems.push({
|
||||||
data: {
|
data: {
|
||||||
id: n.id,
|
id: n.id,
|
||||||
label: label,
|
label: n.label || n.id,
|
||||||
fullLabel: n.label || n.id,
|
fullLabel: n.label || n.id,
|
||||||
status: n.status || "unknown",
|
status: n.status || "unknown",
|
||||||
color: statusColor(n.status || ""),
|
color: statusColor(n.status || ""),
|
||||||
width: width,
|
bgColor: bgColor,
|
||||||
|
width: nodeFixedW,
|
||||||
height: height,
|
height: height,
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
textMaxWidth: textMaxWidth
|
textMaxWidth: textMaxWidth,
|
||||||
|
svc: svc,
|
||||||
|
age: age
|
||||||
},
|
},
|
||||||
classes: (focusUid && n.id === focusUid) ? "focus" : ""
|
classes: (focusUid && n.id === focusUid) ? "focus" : ""
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
(data.edges || []).forEach(function(e, i){
|
(data.edges || []).forEach(function(e, i){
|
||||||
elems.push({ data: { id: "e" + i + "-" + e.source + "-" + e.target, source: e.source, target: e.target } });
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawGraph(containerId, data, focusUid){
|
function drawGraph(containerId, data, focusUid, forceShowAux){
|
||||||
|
if(graphDrawing) return;
|
||||||
|
graphDrawing = true;
|
||||||
var el = document.getElementById(containerId);
|
var el = document.getElementById(containerId);
|
||||||
if(!el || !window.cytoscape) return;
|
if(!el || !window.cytoscape) { 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;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var nCount = (data.nodes || []).length;
|
var layoutMeta = buildRowLayout(data, el.clientWidth || 1200, forceShowAux);
|
||||||
var layoutCfg;
|
var posMap = layoutMeta.positions;
|
||||||
if(nCount <= 2){
|
// Use filtered nodes if available
|
||||||
layoutCfg = {
|
var visibleNodes = data._filteredNodes || data.nodes || [];
|
||||||
name: "grid",
|
var visibleIds = new Set(visibleNodes.map(function(n){ return n.id; }));
|
||||||
fit: true,
|
var filteredData = {
|
||||||
padding: 12,
|
nodes: visibleNodes,
|
||||||
avoidOverlap: true,
|
edges: (data.edges || []).filter(function(e){ return visibleIds.has(e.source) && visibleIds.has(e.target); })
|
||||||
rows: 1,
|
|
||||||
cols: nCount
|
|
||||||
};
|
};
|
||||||
} else {
|
var layoutCfg = {
|
||||||
var cols = nCount <= 8 ? 3 : nCount <= 18 ? 4 : 5;
|
name: "preset",
|
||||||
layoutCfg = {
|
|
||||||
name: "grid",
|
|
||||||
fit: true,
|
fit: true,
|
||||||
padding: 12,
|
padding: 24,
|
||||||
avoidOverlap: true,
|
positions: function(node){ return posMap[node.id()] || {x: 0, y: 0}; },
|
||||||
avoidOverlapPadding: 18,
|
animate: false
|
||||||
nodeDimensionsIncludeLabels: true,
|
|
||||||
animate: false,
|
|
||||||
condense: true,
|
|
||||||
cols: cols
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
var cy = cytoscape({
|
var cy = cytoscape({
|
||||||
container: el,
|
container: el,
|
||||||
elements: graphElements(data, focusUid),
|
elements: graphElements(filteredData, focusUid, layoutMeta.laneById),
|
||||||
|
autoungrabify: true,
|
||||||
style: [
|
style: [
|
||||||
{ selector: "node", style: {
|
{ selector: "node", style: {
|
||||||
"shape": "round-rectangle",
|
"shape": "round-rectangle",
|
||||||
"background-color": "data(color)",
|
"background-color": "data(bgColor)",
|
||||||
"label": "data(label)",
|
"label": "",
|
||||||
"font-size": "data(fontSize)",
|
|
||||||
"font-weight": 600,
|
|
||||||
"color": "#e6edf3",
|
|
||||||
"text-wrap": "wrap",
|
|
||||||
"text-max-width": "data(textMaxWidth)",
|
|
||||||
"text-valign": "center",
|
|
||||||
"text-halign": "center",
|
|
||||||
"width": "data(width)",
|
"width": "data(width)",
|
||||||
"height": "data(height)",
|
"height": "data(height)",
|
||||||
"padding": "10px",
|
"padding": "10px",
|
||||||
"border-color": "#0d1117",
|
"border-color": "data(color)",
|
||||||
"border-width": 2
|
"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: {
|
{ selector: "edge", style: {
|
||||||
"line-color": "#9fc2ff",
|
"line-color": "data(color)",
|
||||||
"target-arrow-color": "#9fc2ff",
|
"target-arrow-color": "data(color)",
|
||||||
"target-arrow-shape": "triangle",
|
"target-arrow-shape": "triangle",
|
||||||
"curve-style": "bezier",
|
"curve-style": "taxi",
|
||||||
"width": 3.2,
|
"taxi-direction": "auto",
|
||||||
"arrow-scale": 1.8,
|
"taxi-turn": "50px",
|
||||||
"opacity": 1
|
"taxi-turn-min-distance": "10px",
|
||||||
|
"width": 2.4,
|
||||||
|
"arrow-scale": 1.3,
|
||||||
|
"opacity": 0.9
|
||||||
}}
|
}}
|
||||||
],
|
],
|
||||||
layout: layoutCfg
|
layout: layoutCfg
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// HTML labels: render name+svc normally, age as small italic
|
||||||
|
if(cy.nodeHtmlLabel){
|
||||||
|
cy.nodeHtmlLabel([{
|
||||||
|
query: 'node',
|
||||||
|
halign: 'center',
|
||||||
|
valign: 'center',
|
||||||
|
halignBox: 'center',
|
||||||
|
valignBox: 'center',
|
||||||
|
tpl: function(d){
|
||||||
|
var fs = d.fontSize || 13;
|
||||||
|
var html = '<div style="text-align:center;color:#dbeafe;font-family:system-ui,sans-serif;padding:4px 6px;">';
|
||||||
|
html += '<div style="font-size:'+fs+'px;font-weight:600;word-break:break-word;white-space:normal;">' + (d.label||'') + '</div>';
|
||||||
|
if(d.svc) html += '<div style="font-size:'+(fs-1)+'px;margin-top:2px;">' + d.svc + '</div>';
|
||||||
|
if(d.age) html += '<div style="font-size:9px;font-style:italic;color:#9ca3af;margin-top:14px;">⏱ ' + d.age + '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure proper viewport after tab activation/layout changes.
|
// Ensure proper viewport after tab activation/layout changes.
|
||||||
var refit = function(){
|
var refit = function(){
|
||||||
try { cy.resize(); cy.fit(undefined, 24); } catch(e) { /* noop */ }
|
try { cy.resize(); cy.fit(undefined, 24); } catch(e) { /* noop */ }
|
||||||
@@ -386,6 +591,8 @@ function drawGraph(containerId, data, focusUid){
|
|||||||
setTimeout(refit, 0);
|
setTimeout(refit, 0);
|
||||||
setTimeout(refit, 120);
|
setTimeout(refit, 120);
|
||||||
cy.on("layoutstop", refit);
|
cy.on("layoutstop", refit);
|
||||||
|
// Release draw guard after layout settles.
|
||||||
|
setTimeout(function(){ graphDrawing = false; }, 300);
|
||||||
|
|
||||||
cy.on("tap", "node", function(evt){
|
cy.on("tap", "node", function(evt){
|
||||||
var n = evt.target.data();
|
var n = evt.target.data();
|
||||||
@@ -410,23 +617,30 @@ function ensureAllGraphLoaded(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureFullGraphLoaded(){
|
||||||
|
if(graphFullData) return Promise.resolve(graphFullData);
|
||||||
|
return fetchGraph({}).then(function(data){
|
||||||
|
graphFullData = data || {nodes:[], edges:[]};
|
||||||
|
return graphFullData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderInstanceGraph(uid){
|
function renderInstanceGraph(uid){
|
||||||
var holderId = "g-" + uid;
|
var holderId = "g-" + uid;
|
||||||
var holder = document.getElementById(holderId);
|
var holder = document.getElementById(holderId);
|
||||||
if(holder) holder.innerHTML = '<div class="graph-loading">Строю граф зависимостей...</div>';
|
if(holder) holder.innerHTML = '<div class="graph-loading">Строю граф зависимостей...</div>';
|
||||||
// Wait one frame so the graph panel becomes visible and gets real dimensions.
|
|
||||||
var run = function(){
|
var run = function(){
|
||||||
ensureAllGraphLoaded().then(function(){
|
ensureFullGraphLoaded().then(function(){
|
||||||
var g = buildClientSubgraph(graphAllData || {nodes:[], edges:[]}, uid, 3);
|
var g = buildClientSubgraph(graphFullData || {nodes:[], edges:[]}, uid, 3);
|
||||||
if((g.nodes || []).length <= 1 && graphAllData){
|
if((g.nodes || []).length <= 1 && graphFullData){
|
||||||
var near = buildClientSubgraph(graphAllData, uid, 1);
|
var near = buildClientSubgraph(graphFullData, uid, 1);
|
||||||
if((near.nodes || []).length > (g.nodes || []).length) g = near;
|
if((near.nodes || []).length > (g.nodes || []).length) g = near;
|
||||||
}
|
}
|
||||||
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);
|
showGraphModal("Граф зависимостей: " + escHtml(instanceName(uid)), g, uid, true);
|
||||||
} else {
|
} else {
|
||||||
drawGraph(holderId, g, uid);
|
drawGraph(holderId, g, uid, true);
|
||||||
}
|
}
|
||||||
}).catch(function(e){
|
}).catch(function(e){
|
||||||
var el = document.getElementById(holderId);
|
var el = document.getElementById(holderId);
|
||||||
@@ -487,13 +701,12 @@ function openRunningGraph(){
|
|||||||
var modal = document.getElementById("graph-modal");
|
var modal = document.getElementById("graph-modal");
|
||||||
modal.classList.add("open");
|
modal.classList.add("open");
|
||||||
var titleEl = document.getElementById("graph-modal-title");
|
var titleEl = document.getElementById("graph-modal-title");
|
||||||
if(titleEl) titleEl.textContent = "Общий граф зависимостей: running";
|
var statuses = getChecked();
|
||||||
|
if(titleEl) titleEl.textContent = "Общий граф зависимостей" + (statuses.length ? ": " + statuses.join(", ") : ": все");
|
||||||
var target = document.getElementById("running-graph");
|
var target = document.getElementById("running-graph");
|
||||||
if(target) target.innerHTML = '<div class="graph-loading">Собираю общий граф running...</div>';
|
if(target) target.innerHTML = '<div class="graph-loading">Собираю граф...</div>';
|
||||||
var run = function(data){ drawGraph("running-graph", data || {nodes:[], edges:[]}, ""); };
|
ensureAllGraphLoaded()
|
||||||
if(graphRunningData) { run(graphRunningData); return; }
|
.then(function(data){ drawGraph("running-graph", data || {nodes:[], edges:[]}, ""); })
|
||||||
fetchGraph({ status: ["running"] })
|
|
||||||
.then(function(data){ graphRunningData = data || {nodes:[], edges:[]}; run(graphRunningData); })
|
|
||||||
.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>';
|
||||||
@@ -504,12 +717,12 @@ function closeRunningGraph(){
|
|||||||
document.getElementById("graph-modal").classList.remove("open");
|
document.getElementById("graph-modal").classList.remove("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
function showGraphModal(title, data, focusUid){
|
function showGraphModal(title, data, focusUid, forceShowAux){
|
||||||
var modal = document.getElementById("graph-modal");
|
var modal = document.getElementById("graph-modal");
|
||||||
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");
|
||||||
drawGraph("running-graph", data || {nodes:[], edges:[]}, focusUid || "");
|
drawGraph("running-graph", data || {nodes:[], edges:[]}, focusUid || "", forceShowAux);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleGraphWarmup(){
|
function scheduleGraphWarmup(){
|
||||||
@@ -522,9 +735,9 @@ function scheduleGraphWarmup(){
|
|||||||
}).catch(function(){ /* ignore */ });
|
}).catch(function(){ /* ignore */ });
|
||||||
};
|
};
|
||||||
if(window.requestIdleCallback){
|
if(window.requestIdleCallback){
|
||||||
requestIdleCallback(warm, { timeout: 12000 });
|
requestIdleCallback(warm, { timeout: 3000 });
|
||||||
} else {
|
} else {
|
||||||
setTimeout(warm, 12000);
|
setTimeout(warm, 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,6 +1012,8 @@ function loadInstances(){
|
|||||||
allInstances = data.instances || [];
|
allInstances = data.instances || [];
|
||||||
renderStats(allInstances);
|
renderStats(allInstances);
|
||||||
render(allInstances);
|
render(allInstances);
|
||||||
|
var ts = document.getElementById("data-ts");
|
||||||
|
if(ts) ts.textContent = "Данные на: " + new Date().toLocaleTimeString("ru-RU", {hour:"2-digit",minute:"2-digit",second:"2-digit"});
|
||||||
})
|
})
|
||||||
.catch(function(e){
|
.catch(function(e){
|
||||||
document.getElementById("tbody").innerHTML = "<tr><td colspan=\"7\" class=\"err-msg\">" + e.message + "</td></tr>";
|
document.getElementById("tbody").innerHTML = "<tr><td colspan=\"7\" class=\"err-msg\">" + e.message + "</td></tr>";
|
||||||
@@ -872,11 +1087,28 @@ function initApp(preloaded){
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("btn-login").addEventListener("click", doLogin);
|
document.getElementById("btn-login").addEventListener("click", doLogin);
|
||||||
document.getElementById("btn-refresh").addEventListener("click", loadInstances);
|
document.getElementById("btn-refresh").addEventListener("click", function(){
|
||||||
|
// Reset all caches so data is fetched fresh
|
||||||
|
allInstances = [];
|
||||||
|
cache = {};
|
||||||
|
graphAllData = null;
|
||||||
|
graphAllKey = "";
|
||||||
|
graphRunningData = null;
|
||||||
|
graphFullData = null;
|
||||||
|
graphWarmupScheduled = false;
|
||||||
|
loadInstances();
|
||||||
|
});
|
||||||
document.getElementById("btn-running-graph").addEventListener("click", openRunningGraph);
|
document.getElementById("btn-running-graph").addEventListener("click", openRunningGraph);
|
||||||
document.getElementById("btn-theme").addEventListener("click", toggleTheme);
|
document.getElementById("btn-theme").addEventListener("click", toggleTheme);
|
||||||
document.getElementById("btn-close-graph").addEventListener("click", closeRunningGraph);
|
document.getElementById("btn-close-graph").addEventListener("click", closeRunningGraph);
|
||||||
document.getElementById("btn-exit").addEventListener("click", doLogout);
|
document.getElementById("btn-exit").addEventListener("click", doLogout);
|
||||||
|
document.getElementById("chk-auxiliary").addEventListener("change", function(){
|
||||||
|
// Redraw current graph with updated auxiliary filter
|
||||||
|
if(graphRunningData){
|
||||||
|
graphDrawing = false;
|
||||||
|
drawGraph("running-graph", graphRunningData, "");
|
||||||
|
}
|
||||||
|
});
|
||||||
document.getElementById("graph-modal").addEventListener("click", function(e){
|
document.getElementById("graph-modal").addEventListener("click", function(e){
|
||||||
if(e.target && e.target.id === "graph-modal") closeRunningGraph();
|
if(e.target && e.target.id === "graph-modal") closeRunningGraph();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
test
|
||||||
|
https://deck-api-test.ngcloud.ru/api/v1
|
||||||
|
api_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWNjMjY4LTZjNmEtNzgxZS04NjEzLTRiZWQ0ZWM3Y2QyMCIsImV4cCI6MTc4OTAxNTg2NywiaWF0IjoxNzczNDYzODY3LCJqdGkiOiJmYjRkMGFiNy1lYTI1LTQ5YmItYmQ3OC0zYjBiNmRiZTdhNzkiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJjb21wYW55X2lkIjoiIiwiZ3JvdXBzIjpudWxsLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiIiLCJnaXZlbl9uYW1lIjoiIiwiZmFtaWx5X25hbWUiOiIiLCJlbWFpbCI6InRhemV0ZGlub3ZuQGdtYWlsLmNvbSJ9.xPxWUWA8e_GEUQ8bgpuHOZvoY7Xu2udRtGPrpHZrzP3Z5uhX-NqDvVDDZQRSpuqoAeXjmekTxX0kHFvwEmv-Kd1hnQZJA_nJf3DvSVPYErtu4ePgy4U2N-4uwlMvjtRysxv17SMEPaDP4XAKj6SxDZ8eEtRCnrTJpgxwh-2cz1NRUsH99pIBY0doj7yNkPOQnnrOCQpnzWdX-d1I1ERCwGg10qOa5VpxP3yXoc2PuDMF6gFU_1VxlhCEdL94TTlFQDPdaBmaDk6hDwUq6T7RZk4cRQ8eao10tUWLHucfaMBGtKEutgrbqR8kOmMB6wY3x86TXdZfBS0zr7RO137d-g"
|
||||||
|
|
||||||
|
|
||||||
|
prod
|
||||||
|
https://deck-api.ngcloud.ru/api/v1
|
||||||
|
api_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOTllMzI1LTFjZGYtN2NkYS05MzE5LWU1MzAyYTg1ZTI5MSIsImV4cCI6MTc4NjkzMjI2MCwiaWF0IjoxNzcxMzgwMjYwLCJqdGkiOiIzOTQ3ZTgyMy0yNjljLTQ0MTAtYmU0My1iNGVkNTc1Njg0ZTQiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJncm91cHMiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6IiIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUifQ.hzpIIqNWkKIoYUXDaLY7DLyGKH70rz0ZTqanv19qxF10i3N1t1g_KknA4Qsw1MduTyLzIz7y5SRSr4PSQ1gzR0vB_C0GudSFUhyBNNKkS4ClhRDWW9eN_IIEljbiJMLQi2L07XJ7Y5DQ0sIHRAPLkreCDFMKQ0yTCrKoScCJIDuUqzaTcOaX-hfjaxW8iV0SZMDxl0C5O3tke0btxkaLBaAcWH0V-1yu2r2m29fyU33FqikF0xAcDXiuZphfsrShKQYArZjKAphYCP_Vpmr-1sdjinkn8sPSk1qZny0rka8G6WVZUGaZSOnW8SYNLVUwdqtuQmK-Y18o7U0Suzrsjg"
|
||||||
Reference in New Issue
Block a user