/** * Страница «Журнал»: режимы 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 = "
" + esc(cluster) + "" + esc(kind) + "