Files
KindClustersDashboard/app/static/js/journal.js
T
Sergey Antropoff 4b703801e1 Kiali anonymous, журнал Helm, kubeconfig для контейнеров, UI аддонов
- Kiali: убран login, anonymous по умолчанию; удалены поля логина/пароля из UI и API
- Журнал Helm: install/upgrade/delete, message и колонка в journal.js
- Аддоны: values свёрнуты при подгрузке для установленных
- GET …/kubeconfig/docker: host.docker.internal:порт + tls-server-name; кнопка в UI
- apply_apiserver_endpoint_to_kubeconfig_file; KIND_K8S_APISERVER_GATEWAY_HOST в compose/env.example
- README и api_routes.md обновлены
2026-04-04 18:54:10 +03:00

497 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Страница «Журнал»: режимы per_cluster / provision / helm_addons, пагинация по 30.
*
* Автор: Сергей Антропов
* Сайт: https://devops.org.ru
*/
(function () {
"use strict";
const body = document.body;
if ((body.getAttribute("data-dashboard-mode") || "") !== "journal") return;
const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
var PAGE_SIZE = 30;
/** @type {'per_cluster' | 'provision' | 'helm_addons'} */
var viewMode = "per_cluster";
var currentPage = 1;
/** Выбранный кластер в режиме per_cluster */
var selectedCluster = "";
async function apiGet(path) {
const url = path.startsWith("http") ? path : API + path;
const r = await fetch(url, { headers: { Accept: "application/json" } });
const text = await r.text();
var data;
try {
data = text ? JSON.parse(text) : null;
} catch (e) {
data = { raw: text };
}
if (!r.ok) {
const msg =
typeof data.detail === "string"
? data.detail
: Array.isArray(data.detail)
? data.detail.map(function (x) { return x.msg || JSON.stringify(x); }).join("; ")
: text || r.statusText;
const err = new Error(msg);
err.status = r.status;
throw err;
}
return data;
}
const tbody = document.getElementById("journal-tbody");
const statusEl = document.getElementById("journal-status");
const emptyEl = document.getElementById("journal-empty");
const tableWrap = document.getElementById("journal-table-wrap");
const refreshBtn = document.getElementById("journal-refresh-btn");
const pagNav = document.getElementById("journal-pagination");
const clusterSelect = document.getElementById("journal-cluster-select");
const clusterPicker = document.getElementById("journal-cluster-picker");
const thKind = document.getElementById("journal-th-kind");
const sectionTitle = document.getElementById("journal-table-section-title");
function hideJournalPageLoadingOverlay() {
const el = document.getElementById("journal-page-loading-overlay");
if (!el) return;
el.classList.add("hidden");
el.setAttribute("aria-busy", "false");
el.setAttribute("aria-hidden", "true");
document.body.classList.remove("journal-page-loading");
}
function esc(s) {
const d = document.createElement("div");
d.textContent = s == null ? "" : String(s);
return d.innerHTML;
}
function formatUtc(iso) {
if (!iso) return "—";
try {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return d.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, " Z");
} catch (e) {
return iso;
}
}
function statusClass(st) {
const s = (st || "").toLowerCase();
if (s === "success") return "journal-status-pill journal-status-pill--ok";
if (s === "failed") return "journal-status-pill journal-status-pill--err";
if (s === "cancelled") return "journal-status-pill journal-status-pill--warn";
return "journal-status-pill";
}
/** Суффикс job_kind (после helm_addon_install_|upgrade_|delete_) → id аддона в API. */
function helmJobSlugToAddonId(slug) {
const m = {
ingress_nginx: "ingress-nginx",
kube_prometheus_stack: "kube-prometheus-stack",
metrics_server: "metrics-server",
istio_kiali: "istio-kiali",
};
return m[slug] || String(slug || "").replace(/_/g, "-") || "—";
}
function helmAddonTitle(id) {
const m = {
"ingress-nginx": "ingress-nginx",
"kube-prometheus-stack": "kube-prometheus-stack",
"metrics-server": "metrics-server",
"istio-kiali": "Istio + Kiali",
};
return m[id] || id || "—";
}
function helmOperationTitle(op) {
const o = (op || "").toLowerCase();
if (o === "install") return "Установка";
if (o === "upgrade") return "Обновление";
if (o === "delete") return "Удаление";
return op || "—";
}
/**
* Колонка «Вид операции» для режима Helm: установка / обновление / удаление и имя аддона.
* Старые записи без result.action разбираются по префиксу kind.
*/
function formatHelmJournalKind(e) {
const r = e.result;
if (r && typeof r === "object") {
const verb = helmOperationTitle(r.action);
const ad = r.addon != null ? helmAddonTitle(String(r.addon)) : "—";
if (r.action && r.addon != null && String(r.addon).length) {
return verb + " · " + ad;
}
}
const k = e.kind || "";
if (k.indexOf("helm_addon_delete_") === 0) {
return "Удаление · " + helmAddonTitle(helmJobSlugToAddonId(k.replace(/^helm_addon_delete_/, "")));
}
if (k.indexOf("helm_addon_upgrade_") === 0) {
return "Обновление · " + helmAddonTitle(helmJobSlugToAddonId(k.replace(/^helm_addon_upgrade_/, "")));
}
if (k.indexOf("helm_addon_install_") === 0) {
return "Установка · " + helmAddonTitle(helmJobSlugToAddonId(k.replace(/^helm_addon_install_/, "")));
}
return k || "—";
}
function visiblePageItems(current, total) {
const delta = 2;
const range = [];
let i;
for (i = 1; i <= total; i++) {
if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) {
range.push(i);
}
}
const out = [];
let l = 0;
for (i = 0; i < range.length; i++) {
const p = range[i];
if (l) {
if (p - l === 2) out.push(l + 1);
else if (p - l > 2) out.push("…");
}
out.push(p);
l = p;
}
return out;
}
function renderPagination(total, limit, offset, page, totalPages, itemCount, onSelectPage) {
if (!pagNav) return;
pagNav.innerHTML = "";
if (total <= 0 || totalPages <= 0) {
pagNav.classList.add("hidden");
return;
}
pagNav.classList.remove("hidden");
const wrap = document.createElement("div");
wrap.className = "journal-pagination-inner";
function mkBtn(label, disabled, ariaLabel, onClick) {
const b = document.createElement("button");
b.type = "button";
b.className = "journal-pag-btn";
b.textContent = label;
b.setAttribute("aria-label", ariaLabel);
b.disabled = !!disabled;
if (!disabled && onClick) b.addEventListener("click", onClick);
return b;
}
wrap.appendChild(
mkBtn("« Первая", page <= 1, "Первая страница", function () { onSelectPage(1); }),
);
wrap.appendChild(
mkBtn(" Назад", page <= 1, "Предыдущая страница", function () { onSelectPage(page - 1); }),
);
const pagesEl = document.createElement("div");
pagesEl.className = "journal-pag-pages";
pagesEl.setAttribute("role", "group");
pagesEl.setAttribute("aria-label", "Номера страниц");
visiblePageItems(page, totalPages).forEach(function (item) {
if (item === "…") {
const span = document.createElement("span");
span.className = "journal-pag-ellipsis";
span.setAttribute("aria-hidden", "true");
span.textContent = "…";
pagesEl.appendChild(span);
return;
}
const pNum = item;
const b = document.createElement("button");
b.type = "button";
b.className = "journal-pag-num" + (pNum === page ? " journal-pag-num--active" : "");
b.textContent = String(pNum);
b.setAttribute("aria-label", "Страница " + pNum);
if (pNum === page) b.setAttribute("aria-current", "page");
else b.removeAttribute("aria-current");
if (pNum !== page) {
b.addEventListener("click", function () { onSelectPage(pNum); });
}
pagesEl.appendChild(b);
});
wrap.appendChild(pagesEl);
wrap.appendChild(
mkBtn("Вперёд ", page >= totalPages, "Следующая страница", function () { onSelectPage(page + 1); }),
);
wrap.appendChild(
mkBtn("Последняя »", page >= totalPages, "Последняя страница", function () { onSelectPage(totalPages); }),
);
pagNav.appendChild(wrap);
}
function goToPage(p) {
if (p < 1) p = 1;
currentPage = p;
loadJournal(false);
}
/**
* Построить строки таблицы: jobs (log_lines) или dir-лог (lines).
*/
function renderTableRows(entries, mode) {
if (!tbody) return;
tbody.innerHTML = "";
entries.forEach(function (e) {
const tr = document.createElement("tr");
tr.className = "journal-data-row";
const cluster = e.source_cluster || e.cluster_name || "—";
const finished = formatUtc(
e.finished_at_utc || e.created_at_utc,
);
const kind = mode === "helm_addons" ? formatHelmJournalKind(e) : (e.kind || "");
const msg = (e.message || "").trim();
let lines = [];
if (mode === "per_cluster") {
lines = Array.isArray(e.log_lines) ? e.log_lines : [];
} else {
lines = Array.isArray(e.lines) ? e.lines : [];
}
const logText = lines.join("\n");
const detailsId = "journal-log-" + Math.random().toString(36).slice(2, 11);
tr.innerHTML =
"<td class=\"journal-cell-time\">" + esc(finished) + "</td>" +
"<td><code class=\"journal-code\">" + esc(cluster) + "</code></td>" +
"<td><code class=\"journal-code journal-cell-kind\">" + esc(kind) + "</code></td>" +
"<td><span class=\"" + esc(statusClass(e.status)) + "\">" + esc(e.status || "") + "</span></td>" +
"<td class=\"journal-cell-msg\">" + esc(msg || "—") + "</td>" +
"<td class=\"journal-cell-actions\">" +
"<button type=\"button\" class=\"btn-secondary btn-journal-toggle\" aria-expanded=\"false\" aria-controls=\"" +
detailsId + "\">Показать лог</button>" +
"</td>";
tbody.appendChild(tr);
if (logText) {
const trDetails = document.createElement("tr");
trDetails.className = "journal-details-row hidden";
trDetails.innerHTML =
"<td colspan=\"6\" class=\"journal-details-cell\">" +
"<pre id=\"" + detailsId + "\" class=\"mono journal-log-pre\" hidden></pre>" +
"</td>";
tbody.appendChild(trDetails);
const pre = trDetails.querySelector("pre");
if (pre) pre.textContent = logText;
const btn = tr.querySelector(".btn-journal-toggle");
if (btn && trDetails && pre) {
btn.addEventListener("click", function () {
const open = trDetails.classList.contains("hidden");
trDetails.classList.toggle("hidden", !open);
pre.hidden = !open;
btn.setAttribute("aria-expanded", open ? "true" : "false");
btn.textContent = open ? "Скрыть лог" : "Показать лог";
});
}
} else {
const btn = tr.querySelector(".btn-journal-toggle");
if (btn) {
btn.disabled = true;
btn.textContent = "Нет строк";
}
}
});
}
function applyViewLabels() {
if (thKind) {
if (viewMode === "per_cluster") thKind.textContent = "Тип задания";
else thKind.textContent = "Вид операции";
}
if (sectionTitle) {
if (viewMode === "per_cluster") sectionTitle.textContent = "Задания выбранного кластера";
else if (viewMode === "provision") sectionTitle.textContent = "Развёртывание по кластерам";
else sectionTitle.textContent = "Helm-аддоны по кластерам";
}
if (clusterPicker) {
clusterPicker.classList.toggle("hidden", viewMode !== "per_cluster");
}
}
function buildListUrl() {
const off = (currentPage - 1) * PAGE_SIZE;
const q =
"?limit=" + encodeURIComponent(String(PAGE_SIZE)) +
"&offset=" + encodeURIComponent(String(off));
if (viewMode === "per_cluster") {
return "/journal/recent" + q + "&cluster=" + encodeURIComponent(selectedCluster);
}
if (viewMode === "provision") return "/journal/provision" + q;
return "/journal/helm-addons" + q;
}
async function loadJournal(resetPage) {
if (!tbody || !statusEl) {
hideJournalPageLoadingOverlay();
return;
}
applyViewLabels();
if (resetPage) currentPage = 1;
if (viewMode === "per_cluster" && !selectedCluster) {
statusEl.textContent = "Выберите кластер.";
tbody.innerHTML = "";
if (emptyEl) {
emptyEl.classList.remove("hidden");
emptyEl.textContent = "Укажите кластер в списке выше.";
}
if (tableWrap) tableWrap.classList.add("hidden");
if (pagNav) pagNav.classList.add("hidden");
hideJournalPageLoadingOverlay();
return;
}
statusEl.textContent = "Загрузка…";
tbody.innerHTML = "";
if (emptyEl) emptyEl.classList.add("hidden");
if (tableWrap) tableWrap.classList.remove("hidden");
if (pagNav) pagNav.classList.add("hidden");
try {
const data = await apiGet(buildListUrl());
const entries = data.entries || [];
const total = typeof data.total === "number" ? data.total : entries.length;
const limit = typeof data.limit === "number" ? data.limit : PAGE_SIZE;
const respOffset = typeof data.offset === "number" ? data.offset : (currentPage - 1) * PAGE_SIZE;
const totalPages = typeof data.total_pages === "number" ? data.total_pages : Math.ceil(total / limit) || 0;
let page = typeof data.page === "number" ? data.page : currentPage;
if (total > 0 && currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
await loadJournal(false);
return;
}
if (total > 0 && entries.length === 0 && respOffset >= total && totalPages > 0) {
currentPage = totalPages;
await loadJournal(false);
return;
}
page = currentPage;
if (!entries.length) {
if (emptyEl) {
emptyEl.classList.remove("hidden");
emptyEl.textContent =
viewMode === "per_cluster"
? "Нет записей в journal/jobs_history.json для этого кластера."
: viewMode === "provision"
? "Нет файлов provision_log.json (или каталогов кластеров)."
: "Нет файлов helm_addon_log.json — установите аддон со страницы «Аддоны».";
}
if (tableWrap) tableWrap.classList.add("hidden");
statusEl.textContent =
total === 0
? "Записей нет."
: "Страница пуста (всего: " + total + ").";
renderPagination(total, limit, respOffset, page, totalPages, 0, goToPage);
return;
}
if (emptyEl) emptyEl.classList.add("hidden");
if (tableWrap) tableWrap.classList.remove("hidden");
const fromN = respOffset + 1;
const toN = respOffset + entries.length;
statusEl.textContent =
"Записи " + fromN + "" + toN + " из " + total + " · страница " + page + " из " + (totalPages || 1) + ".";
renderTableRows(entries, viewMode);
renderPagination(total, limit, respOffset, page, totalPages, entries.length, goToPage);
} catch (err) {
statusEl.textContent = "Ошибка: " + err.message;
if (emptyEl) {
emptyEl.classList.remove("hidden");
emptyEl.textContent = "Не удалось загрузить данные.";
}
if (tableWrap) tableWrap.classList.add("hidden");
if (pagNav) pagNav.classList.add("hidden");
} finally {
hideJournalPageLoadingOverlay();
}
}
async function loadClusterList() {
if (!clusterSelect) return;
try {
const rows = await apiGet("/clusters");
clusterSelect.innerHTML = "";
const names = rows
.map(function (c) { return c.name; })
.filter(Boolean)
.sort();
if (!names.length) {
const o = document.createElement("option");
o.value = "";
o.textContent = "Нет каталогов кластеров";
clusterSelect.appendChild(o);
selectedCluster = "";
return;
}
names.forEach(function (name) {
const o = document.createElement("option");
o.value = name;
o.textContent = name;
clusterSelect.appendChild(o);
});
if (!selectedCluster || names.indexOf(selectedCluster) < 0) {
selectedCluster = names[0];
}
clusterSelect.value = selectedCluster;
} catch (e) {
clusterSelect.innerHTML = "";
const o = document.createElement("option");
o.value = "";
o.textContent = "Ошибка загрузки списка";
clusterSelect.appendChild(o);
selectedCluster = "";
}
}
function bindViewRadios() {
document.querySelectorAll('input[name="journal_view"]').forEach(function (radio) {
radio.addEventListener("change", function () {
if (!radio.checked) return;
viewMode = radio.value;
currentPage = 1;
loadJournal(false);
});
});
}
if (clusterSelect) {
clusterSelect.addEventListener("change", function () {
selectedCluster = String(clusterSelect.value || "").trim();
currentPage = 1;
if (viewMode === "per_cluster") loadJournal(false);
});
}
if (refreshBtn) {
refreshBtn.addEventListener("click", function () { loadJournal(true); });
}
bindViewRadios();
(async function init() {
await loadClusterList();
await loadJournal(false);
})();
})();