Веб-UI: темы, навигация API, статистика и доработки API
- Шапка: логотип Kubernetes, ссылка на главную, выпадающее меню API (Swagger/ReDoc/Health), переключатель светлой/тёмной темы (localStorage). - Светлая тема в синей гамме; выравнивание кнопки темы в ряду с пилюлями. - Дашборд: единая карточка ошибки health/stats, подсказка Docker/Podman, поле container_cli в GET /stats, total_workers_from_meta всегда число (0 без meta). - Правки кластеров, job_store, compose, документация и частичные шаблоны.
This commit is contained in:
+406
-125
@@ -28,6 +28,20 @@
|
||||
var currentPollJobId = null;
|
||||
/** Имя кластера в открытой модалке «Состояние» (для скачивания kubeconfig). */
|
||||
var currentModalClusterName = null;
|
||||
/** Кэш блоков cluster_resources по имени кластера (последний успешный GET /stats). */
|
||||
var cachedClusterResourcesByName = {};
|
||||
/** Успешный снимок GET /health — для ряда карточек среды + «Каталогов». */
|
||||
var cachedHealthPayload = null;
|
||||
/** local_cluster_dirs_count из GET /stats; показывается справа от Kubectl. */
|
||||
var cachedLocalClusterDirsCount = null;
|
||||
|
||||
function isHomePage() {
|
||||
return (document.body.getAttribute("data-dashboard-mode") || "home") === "home";
|
||||
}
|
||||
|
||||
function isCreatePage() {
|
||||
return document.body.getAttribute("data-dashboard-mode") === "create";
|
||||
}
|
||||
|
||||
function formatApiError(data, fallback) {
|
||||
if (!data) return fallback;
|
||||
@@ -240,143 +254,363 @@
|
||||
}, 4500);
|
||||
}
|
||||
|
||||
function setStatusBannerClass(ok, degraded) {
|
||||
const el = document.getElementById("status-banner");
|
||||
/**
|
||||
* Карточки среды в блоке статистики (как плитки «Кластеров в kind»).
|
||||
* @param {string} label подпись снизу (CAPS в стиле stats-tile__label)
|
||||
* @param {string} valueText крупный текст сверху
|
||||
* @param {boolean} isOk
|
||||
* @param {string | null} [detail] доп. строка мелким шрифтом
|
||||
*/
|
||||
function buildStatsEnvCard(label, valueText, isOk, detail) {
|
||||
const tile = document.createElement("div");
|
||||
tile.className =
|
||||
"stats-tile stats-env-card " + (isOk ? "stats-env-card--ok" : "stats-env-card--bad");
|
||||
const valEl = document.createElement("span");
|
||||
valEl.className = "stats-tile__value";
|
||||
valEl.textContent = valueText;
|
||||
const labEl = document.createElement("span");
|
||||
labEl.className = "stats-tile__label";
|
||||
labEl.textContent = label;
|
||||
tile.appendChild(valEl);
|
||||
tile.appendChild(labEl);
|
||||
if (detail) {
|
||||
const dEl = document.createElement("span");
|
||||
dEl.className = "stats-env-card__detail muted";
|
||||
dEl.textContent = detail.slice(0, 400);
|
||||
tile.appendChild(dEl);
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
/** Нормализованное имя CLI для подписей (docker → Docker, podman → Podman). */
|
||||
function titleCaseContainerCli(cliRaw) {
|
||||
const s = String(cliRaw || "docker").trim() || "docker";
|
||||
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
/** Текст под заголовком «Статистика»: через какой движок идут узлы и метрики. */
|
||||
function updateStatsEngineHint(cliRaw) {
|
||||
const el = document.getElementById("stats-engine-hint");
|
||||
if (!el) return;
|
||||
var cls = "hero-panel-status muted ";
|
||||
if (ok) cls += "ok";
|
||||
else if (degraded) cls += "degraded";
|
||||
else cls += "err";
|
||||
el.className = cls;
|
||||
const name = titleCaseContainerCli(cliRaw);
|
||||
el.textContent =
|
||||
"Узлы kind (контейнеры control-plane и worker) и метрики ниже собираются через " + name + ".";
|
||||
el.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function hideStatsEngineHint() {
|
||||
const el = document.getElementById("stats-engine-hint");
|
||||
if (!el) return;
|
||||
el.classList.add("hidden");
|
||||
el.textContent = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Скрыть ряд среды и метрик, показать одну карточку ошибки на всю карточку «Статистика».
|
||||
* @param {"Health"|"Статистика"} sourceLabel подпись под «Ошибка»
|
||||
*/
|
||||
function coverStatsCardWithError(sourceLabel, message) {
|
||||
hideStatsEngineHint();
|
||||
const failureWrap = document.getElementById("stats-failure-wrap");
|
||||
const env = document.getElementById("stats-env");
|
||||
const metricsGrid = document.getElementById("stats-grid");
|
||||
if (failureWrap) {
|
||||
failureWrap.classList.remove("hidden");
|
||||
failureWrap.replaceChildren();
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "stats-tile stats-env-card stats-env-card--bad stats-failure-card";
|
||||
const valEl = document.createElement("span");
|
||||
valEl.className = "stats-tile__value";
|
||||
valEl.textContent = "Ошибка";
|
||||
const labEl = document.createElement("span");
|
||||
labEl.className = "stats-tile__label";
|
||||
labEl.textContent = sourceLabel;
|
||||
const dEl = document.createElement("span");
|
||||
dEl.className = "stats-env-card__detail muted";
|
||||
dEl.textContent = message || "";
|
||||
tile.appendChild(valEl);
|
||||
tile.appendChild(labEl);
|
||||
tile.appendChild(dEl);
|
||||
failureWrap.appendChild(tile);
|
||||
}
|
||||
if (env) env.classList.add("hidden");
|
||||
if (metricsGrid) metricsGrid.classList.add("hidden");
|
||||
}
|
||||
|
||||
/** Показать снова ряд среды и сетку метрик (после успешных запросов). */
|
||||
function revealStatsCardBody() {
|
||||
const failureWrap = document.getElementById("stats-failure-wrap");
|
||||
const env = document.getElementById("stats-env");
|
||||
const metricsGrid = document.getElementById("stats-grid");
|
||||
if (failureWrap) {
|
||||
failureWrap.classList.add("hidden");
|
||||
failureWrap.replaceChildren();
|
||||
}
|
||||
if (env) env.classList.remove("hidden");
|
||||
if (metricsGrid) metricsGrid.classList.remove("hidden");
|
||||
}
|
||||
|
||||
/** Карточка-метрика в ряду среды (без зелёного/красного статуса), напр. «Каталогов». */
|
||||
function buildStatsEnvMetricCard(label, valueText) {
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "stats-tile stats-env-card stats-env-card--metric";
|
||||
const valEl = document.createElement("span");
|
||||
valEl.className = "stats-tile__value";
|
||||
valEl.textContent = valueText;
|
||||
const labEl = document.createElement("span");
|
||||
labEl.className = "stats-tile__label";
|
||||
labEl.textContent = label;
|
||||
tile.appendChild(valEl);
|
||||
tile.appendChild(labEl);
|
||||
return tile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ряд: Docker API · Kind · Kubectl · Каталогов (число из /stats).
|
||||
*/
|
||||
function paintStatsEnvRow() {
|
||||
const el = document.getElementById("stats-env");
|
||||
if (!el) return;
|
||||
if (!cachedHealthPayload) {
|
||||
return;
|
||||
}
|
||||
el.className = "stats-env-cards";
|
||||
el.replaceChildren();
|
||||
const h = cachedHealthPayload;
|
||||
const cliRaw = String(h.container_cli || "docker").trim() || "docker";
|
||||
const cliTitle = titleCaseContainerCli(cliRaw);
|
||||
const apiOk = !!h.container_engine_ok;
|
||||
el.appendChild(
|
||||
buildStatsEnvCard(
|
||||
cliTitle + " API",
|
||||
apiOk ? "OK" : "Недоступно",
|
||||
apiOk,
|
||||
!apiOk && h.container_engine_detail ? String(h.container_engine_detail) : null,
|
||||
),
|
||||
);
|
||||
const kindOk = !!h.kind_in_path;
|
||||
el.appendChild(buildStatsEnvCard("Kind", kindOk ? "OK" : "нет в PATH", kindOk, null));
|
||||
const kubectlOk = !!h.kubectl_in_path;
|
||||
el.appendChild(
|
||||
buildStatsEnvCard("Kubectl", kubectlOk ? "OK" : "нет в PATH", kubectlOk, null),
|
||||
);
|
||||
const dirVal =
|
||||
cachedLocalClusterDirsCount != null ? String(cachedLocalClusterDirsCount) : "—";
|
||||
el.appendChild(buildStatsEnvMetricCard("Каталогов", dirVal));
|
||||
}
|
||||
|
||||
async function loadHealth() {
|
||||
const el = document.getElementById("status-banner");
|
||||
const el = document.getElementById("stats-env");
|
||||
if (!el) return;
|
||||
try {
|
||||
const h = await api("/health");
|
||||
const ok =
|
||||
h.status === "ok" && h.container_engine_ok && h.kind_in_path && h.kubectl_in_path;
|
||||
setStatusBannerClass(ok, !ok);
|
||||
var lines = "<strong>Среда:</strong> ";
|
||||
lines += escapeHtml(String(h.container_cli || "?"));
|
||||
lines += " → " + (h.container_engine_ok ? "API OK" : "API недоступен");
|
||||
lines += " · kind: " + (h.kind_in_path ? "да" : "нет");
|
||||
lines += " · kubectl: " + (h.kubectl_in_path ? "да" : "нет");
|
||||
if (!h.container_engine_ok && h.container_engine_detail) {
|
||||
lines +=
|
||||
"<br/><span class=\"muted\">" +
|
||||
escapeHtml(String(h.container_engine_detail).slice(0, 400)) +
|
||||
"</span>";
|
||||
}
|
||||
el.innerHTML = lines;
|
||||
cachedHealthPayload = h;
|
||||
revealStatsCardBody();
|
||||
paintStatsEnvRow();
|
||||
updateStatsEngineHint(h.container_cli);
|
||||
} catch (e) {
|
||||
setStatusBannerClass(false, false);
|
||||
el.textContent = "Не удалось запросить health: " + e.message;
|
||||
cachedHealthPayload = null;
|
||||
el.replaceChildren();
|
||||
coverStatsCardWithError("Health", e.message || String(e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Блок «Ресурсы узлов» из cluster_resources (CPU/RAM/I/O из docker stats).
|
||||
* SVG-донаты по полю aggregate_cluster_resources (главная страница).
|
||||
* @param {object | null | undefined} agg
|
||||
* @param {string | null | undefined} resourcesError
|
||||
* @param {string | undefined} containerCliRaw имя CLI из /stats (docker/podman) для подписи доната RAM
|
||||
*/
|
||||
function renderStatsResources(s) {
|
||||
const wrap = document.getElementById("stats-resources");
|
||||
if (!wrap) return;
|
||||
wrap.replaceChildren();
|
||||
if (s.cluster_resources_error) {
|
||||
function renderAggregateDonuts(agg, resourcesError, containerCliRaw) {
|
||||
const root = document.getElementById("aggregate-donuts");
|
||||
if (!root) return;
|
||||
|
||||
root.replaceChildren();
|
||||
if (resourcesError) {
|
||||
const p = document.createElement("p");
|
||||
p.className = "muted stats-resources-msg";
|
||||
p.textContent = "Ресурсы узлов: " + s.cluster_resources_error;
|
||||
wrap.appendChild(p);
|
||||
p.className = "muted aggregate-donuts-placeholder";
|
||||
p.textContent = "Метрики узлов недоступны: " + resourcesError;
|
||||
root.appendChild(p);
|
||||
return;
|
||||
}
|
||||
const list = s.cluster_resources || [];
|
||||
if (!list.length) {
|
||||
if (!agg || !agg.nodes_count) {
|
||||
const p2 = document.createElement("p");
|
||||
p2.className = "muted stats-resources-msg";
|
||||
p2.textContent = "Нет кластеров в kind — метрики узлов пусты.";
|
||||
wrap.appendChild(p2);
|
||||
p2.className = "muted aggregate-donuts-placeholder";
|
||||
p2.textContent = "Нет запущенных узлов — диаграммы появятся после старта кластера.";
|
||||
root.appendChild(p2);
|
||||
return;
|
||||
}
|
||||
const h3 = document.createElement("h3");
|
||||
h3.className = "stats-resources-heading";
|
||||
h3.textContent = "Ресурсы узлов (CPU, RAM, I/O)";
|
||||
wrap.appendChild(h3);
|
||||
list.forEach(function (cr) {
|
||||
const sec = document.createElement("section");
|
||||
sec.className = "stats-cluster-res";
|
||||
const h4 = document.createElement("h4");
|
||||
h4.className = "stats-cluster-res__title";
|
||||
h4.textContent = cr.cluster_name;
|
||||
sec.appendChild(h4);
|
||||
const nodes = cr.nodes || [];
|
||||
if (!nodes.length) {
|
||||
const pn = document.createElement("p");
|
||||
pn.className = "muted stats-cluster-res__note";
|
||||
pn.textContent = cr.note || "Нет запущенных контейнеров узлов.";
|
||||
sec.appendChild(pn);
|
||||
} else {
|
||||
nodes.forEach(function (n) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "stats-node-res";
|
||||
const code = document.createElement("code");
|
||||
code.textContent = n.container_name;
|
||||
row.appendChild(code);
|
||||
const ul = document.createElement("ul");
|
||||
ul.className = "stats-node-res__metrics";
|
||||
function addMetric(label, val) {
|
||||
if (val == null || val === "") return;
|
||||
const li = document.createElement("li");
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = label + ": ";
|
||||
li.appendChild(strong);
|
||||
li.appendChild(document.createTextNode(String(val)));
|
||||
ul.appendChild(li);
|
||||
}
|
||||
addMetric("CPU", n.cpu_percent);
|
||||
addMetric("Память", n.memory_usage);
|
||||
addMetric("% RAM", n.memory_percent);
|
||||
addMetric("Сеть", n.net_io);
|
||||
addMetric("Диск", n.block_io);
|
||||
addMetric("PID", n.pids != null ? String(n.pids) : null);
|
||||
row.appendChild(ul);
|
||||
sec.appendChild(row);
|
||||
});
|
||||
|
||||
function donutItem(title, ringPct, centerLabel, subLine) {
|
||||
const p = Math.max(0, Math.min(100, Number(ringPct) || 0));
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "donut-item";
|
||||
wrap.setAttribute("role", "group");
|
||||
wrap.setAttribute("aria-label", title);
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("viewBox", "0 0 110 110");
|
||||
svg.setAttribute("class", "donut-svg");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
const track = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||
track.setAttribute("class", "donut-track");
|
||||
track.setAttribute("cx", "55");
|
||||
track.setAttribute("cy", "55");
|
||||
track.setAttribute("r", "40");
|
||||
track.setAttribute("fill", "none");
|
||||
track.setAttribute("stroke-width", "9");
|
||||
track.setAttribute("pathLength", "100");
|
||||
const fill = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||
fill.setAttribute("class", "donut-fill");
|
||||
fill.setAttribute("cx", "55");
|
||||
fill.setAttribute("cy", "55");
|
||||
fill.setAttribute("r", "40");
|
||||
fill.setAttribute("fill", "none");
|
||||
fill.setAttribute("stroke-width", "9");
|
||||
fill.setAttribute("pathLength", "100");
|
||||
fill.setAttribute("stroke-dasharray", p + " " + (100 - p));
|
||||
fill.setAttribute("transform", "rotate(-90 55 55)");
|
||||
svg.appendChild(track);
|
||||
svg.appendChild(fill);
|
||||
wrap.appendChild(svg);
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "donut-meta";
|
||||
const tEl = document.createElement("span");
|
||||
tEl.className = "donut-meta__title";
|
||||
tEl.textContent = title;
|
||||
meta.appendChild(tEl);
|
||||
const vEl = document.createElement("span");
|
||||
vEl.className = "donut-meta__val";
|
||||
vEl.textContent = centerLabel || "—";
|
||||
meta.appendChild(vEl);
|
||||
if (subLine) {
|
||||
const sEl = document.createElement("span");
|
||||
sEl.className = "donut-meta__sub muted";
|
||||
sEl.textContent = subLine;
|
||||
meta.appendChild(sEl);
|
||||
}
|
||||
wrap.appendChild(sec);
|
||||
wrap.appendChild(meta);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
const ramEngine = titleCaseContainerCli(containerCliRaw);
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "donut-grid";
|
||||
grid.appendChild(
|
||||
donutItem("CPU (ср.)", agg.cpu_ring, String(agg.cpu_label || "—"), "узлов: " + agg.nodes_count),
|
||||
);
|
||||
grid.appendChild(
|
||||
donutItem("% RAM (" + ramEngine + ")", agg.memory_percent_ring, String(agg.memory_percent_label || "—"), ""),
|
||||
);
|
||||
grid.appendChild(
|
||||
donutItem("RAM used/limit", agg.memory_used_ratio_ring, String(agg.memory_used_ratio_label || "—"), ""),
|
||||
);
|
||||
grid.appendChild(
|
||||
donutItem("Сеть Σ", agg.network_ring, String(agg.network_label || "—"), "накопит. I/O"),
|
||||
);
|
||||
grid.appendChild(
|
||||
donutItem("Диск Σ", agg.disk_ring, String(agg.disk_label || "—"), "накопит. I/O"),
|
||||
);
|
||||
root.appendChild(grid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Раскрывающийся блок метрик узлов под строкой таблицы кластеров.
|
||||
* @param {string} clusterName
|
||||
*/
|
||||
function buildClusterResourcesDetails(clusterName) {
|
||||
const cr = cachedClusterResourcesByName[clusterName];
|
||||
const details = document.createElement("details");
|
||||
details.className = "cluster-nodes-resources";
|
||||
const summ = document.createElement("summary");
|
||||
summ.textContent = "Ресурсы узлов (CPU, RAM, сеть, диск)";
|
||||
details.appendChild(summ);
|
||||
if (!cr || !(cr.nodes && cr.nodes.length)) {
|
||||
const p = document.createElement("p");
|
||||
p.className = "muted cluster-nodes-resources__empty";
|
||||
p.textContent = cr && cr.note ? cr.note : "Нет данных: узлы остановлены или метрики недоступны.";
|
||||
details.appendChild(p);
|
||||
return details;
|
||||
}
|
||||
cr.nodes.forEach(function (n) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "stats-node-res cluster-row-node-res";
|
||||
const code = document.createElement("code");
|
||||
code.textContent = n.container_name;
|
||||
row.appendChild(code);
|
||||
const ul = document.createElement("ul");
|
||||
ul.className = "stats-node-res__metrics";
|
||||
function addMetric(label, val) {
|
||||
if (val == null || val === "") return;
|
||||
const li = document.createElement("li");
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = label + ": ";
|
||||
li.appendChild(strong);
|
||||
li.appendChild(document.createTextNode(String(val)));
|
||||
ul.appendChild(li);
|
||||
}
|
||||
addMetric("CPU", n.cpu_percent);
|
||||
addMetric("Память", n.memory_usage);
|
||||
addMetric("% RAM", n.memory_percent);
|
||||
addMetric("Сеть", n.net_io);
|
||||
addMetric("Диск", n.block_io);
|
||||
addMetric("PID", n.pids != null ? String(n.pids) : null);
|
||||
row.appendChild(ul);
|
||||
details.appendChild(row);
|
||||
});
|
||||
return details;
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
const dl = document.getElementById("stats-dl");
|
||||
const errEl = document.getElementById("stats-err");
|
||||
if (!dl) return;
|
||||
const grid = document.getElementById("stats-grid");
|
||||
if (!grid) return;
|
||||
try {
|
||||
const s = await api("/stats");
|
||||
errEl.classList.add("hidden");
|
||||
revealStatsCardBody();
|
||||
cachedLocalClusterDirsCount = s.local_cluster_dirs_count;
|
||||
paintStatsEnvRow();
|
||||
const cliForHint = s.container_cli || (cachedHealthPayload && cachedHealthPayload.container_cli) || "docker";
|
||||
updateStatsEngineHint(cliForHint);
|
||||
const rows = [
|
||||
["Кластеров в kind", s.kind_clusters_count],
|
||||
["Локальных каталогов", s.local_cluster_dirs_count],
|
||||
["Сумма workers (meta)", s.total_workers_from_meta != null ? s.total_workers_from_meta : "—"],
|
||||
["Сумма workers (meta)", s.total_workers_from_meta ?? 0],
|
||||
["Заданий в памяти", s.jobs_total],
|
||||
["Заданий с ошибкой", s.jobs_recent_failed],
|
||||
];
|
||||
const frag = document.createDocumentFragment();
|
||||
grid.replaceChildren();
|
||||
rows.forEach(function (kv) {
|
||||
const dt = document.createElement("dt");
|
||||
dt.textContent = kv[0];
|
||||
const dd = document.createElement("dd");
|
||||
dd.textContent = String(kv[1]);
|
||||
frag.appendChild(dt);
|
||||
frag.appendChild(dd);
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "stats-tile";
|
||||
const valEl = document.createElement("span");
|
||||
valEl.className = "stats-tile__value";
|
||||
valEl.textContent = String(kv[1]);
|
||||
const labEl = document.createElement("span");
|
||||
labEl.className = "stats-tile__label";
|
||||
labEl.textContent = kv[0];
|
||||
tile.appendChild(valEl);
|
||||
tile.appendChild(labEl);
|
||||
grid.appendChild(tile);
|
||||
});
|
||||
dl.replaceChildren(frag);
|
||||
renderStatsResources(s);
|
||||
cachedClusterResourcesByName = {};
|
||||
(s.cluster_resources || []).forEach(function (cr) {
|
||||
cachedClusterResourcesByName[cr.cluster_name] = cr;
|
||||
});
|
||||
renderAggregateDonuts(
|
||||
s.aggregate_cluster_resources,
|
||||
s.cluster_resources_error,
|
||||
s.container_cli || "docker",
|
||||
);
|
||||
} catch (e) {
|
||||
errEl.textContent = "Статистика: " + e.message;
|
||||
errEl.classList.remove("hidden");
|
||||
const wrap = document.getElementById("stats-resources");
|
||||
if (wrap) wrap.replaceChildren();
|
||||
coverStatsCardWithError("Статистика", e.message || String(e));
|
||||
grid.replaceChildren();
|
||||
cachedClusterResourcesByName = {};
|
||||
const donuts = document.getElementById("aggregate-donuts");
|
||||
if (donuts) {
|
||||
donuts.replaceChildren();
|
||||
const ph = document.createElement("p");
|
||||
ph.className = "muted aggregate-donuts-placeholder";
|
||||
ph.textContent = "Сводка по узлам недоступна (ошибка загрузки статистики).";
|
||||
donuts.appendChild(ph);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +664,19 @@
|
||||
const ver = (c.meta && (c.meta.kubernetes_version_tag || c.meta.node_image)) || "—";
|
||||
const wn = c.meta && c.meta.worker_nodes != null ? c.meta.worker_nodes : "—";
|
||||
const nameEsc = escapeHtml(c.name);
|
||||
var stateCell = "";
|
||||
if (c.registered_in_kind) {
|
||||
if (c.kind_nodes_running) {
|
||||
stateCell =
|
||||
"<span class=\"badge badge-ok\" title=\"Контейнеры узлов запущены\">Запущен</span>";
|
||||
} else {
|
||||
stateCell =
|
||||
"<span class=\"badge badge-stopped\" title=\"Зарегистрирован в kind, узлы остановлены\">Остановлен</span>";
|
||||
}
|
||||
} else {
|
||||
stateCell =
|
||||
"<span class=\"muted\" title=\"Нет в kind — только локальный каталог или кластер удалён из kind\">—</span>";
|
||||
}
|
||||
tr.innerHTML =
|
||||
"<td><code class=\"cluster-name\">" +
|
||||
nameEsc +
|
||||
@@ -446,6 +693,9 @@
|
||||
"<td>" +
|
||||
escapeHtml(String(wn)) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
stateCell +
|
||||
"</td>" +
|
||||
"<td class=\"actions\"><span class=\"actions-toolbar\"></span></td>";
|
||||
const td = tr.querySelector(".actions-toolbar");
|
||||
td.appendChild(
|
||||
@@ -459,18 +709,7 @@
|
||||
false,
|
||||
),
|
||||
);
|
||||
td.appendChild(
|
||||
iconActionButton(
|
||||
ICONS.play,
|
||||
"Старт: если кластер в kind — запуск контейнеров; иначе при наличии kind-config.yaml — kind create в фоне",
|
||||
"",
|
||||
function () {
|
||||
startCluster(c.name);
|
||||
},
|
||||
false,
|
||||
),
|
||||
);
|
||||
if (c.registered_in_kind) {
|
||||
if (c.registered_in_kind && c.kind_nodes_running) {
|
||||
td.appendChild(
|
||||
iconActionButton(
|
||||
ICONS.stop,
|
||||
@@ -482,6 +721,18 @@
|
||||
false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
td.appendChild(
|
||||
iconActionButton(
|
||||
ICONS.play,
|
||||
"Старт: если кластер в kind — запуск контейнеров; иначе при наличии kind-config.yaml — kind create в фоне",
|
||||
"",
|
||||
function () {
|
||||
startCluster(c.name);
|
||||
},
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
td.appendChild(
|
||||
iconActionButton(
|
||||
@@ -510,6 +761,16 @@
|
||||
),
|
||||
);
|
||||
frag.appendChild(tr);
|
||||
if (isHomePage()) {
|
||||
const trExp = document.createElement("tr");
|
||||
trExp.className = "cluster-resources-expand-row";
|
||||
const tdExp = document.createElement("td");
|
||||
tdExp.colSpan = 7;
|
||||
tdExp.className = "cluster-resources-expand-cell";
|
||||
tdExp.appendChild(buildClusterResourcesDetails(c.name));
|
||||
trExp.appendChild(tdExp);
|
||||
frag.appendChild(trExp);
|
||||
}
|
||||
});
|
||||
tbody.replaceChildren(frag);
|
||||
bindActionTooltipHosts(document.getElementById("tbl-clusters"));
|
||||
@@ -968,11 +1229,19 @@
|
||||
pollTimer = setInterval(tick, JOB_POLL_ACTIVE_MS);
|
||||
}
|
||||
|
||||
function refreshLists() {
|
||||
loadHealth();
|
||||
loadStats();
|
||||
loadClusters();
|
||||
loadJobs();
|
||||
async function refreshLists() {
|
||||
try {
|
||||
await loadHealth();
|
||||
if (isHomePage()) {
|
||||
await loadStats();
|
||||
await loadClusters();
|
||||
}
|
||||
if (isCreatePage()) {
|
||||
await loadJobs();
|
||||
}
|
||||
} catch (e) {
|
||||
/* loadHealth/loadStats/loadClusters сами сообщают об ошибках в UI */
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
@@ -1078,11 +1347,23 @@
|
||||
|
||||
autoTimer = setInterval(refreshLists, AUTO_REFRESH_MS);
|
||||
|
||||
loadHealth();
|
||||
loadStats();
|
||||
loadVersions();
|
||||
loadClusters();
|
||||
loadJobs();
|
||||
(async function bootstrap() {
|
||||
try {
|
||||
await loadHealth();
|
||||
if (isHomePage()) {
|
||||
await loadStats();
|
||||
await loadClusters();
|
||||
}
|
||||
if (isCreatePage()) {
|
||||
await loadJobs();
|
||||
}
|
||||
} catch (e) {
|
||||
/* см. сообщения в блоках */
|
||||
}
|
||||
})();
|
||||
if (isCreatePage()) {
|
||||
loadVersions();
|
||||
}
|
||||
bindActionTooltipHosts(document.getElementById("modal-overlay"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user