Журнал: пагинация 30 записей на страницу, API offset/total_pages
- job_journal: collect_recent_journal_entries_page_sync(limit, offset) - GET /api/v1/journal/recent: limit по умолчанию 30, offset, total, page, total_pages - journal.html/js: навигация Первая/Назад/номера/Вперёд/Последняя, стили - app/docs/api_routes.md: описание query и пример ответа - Прочие изменения UI/API (аддоны, helm, job_journal в кластерах) в том же коммите
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Страница Helm-аддонов: GET /api/v1/clusters, статус и POST/DELETE …/addons/*.
|
||||
*
|
||||
* Автор: Сергей Антропов
|
||||
* Сайт: https://devops.org.ru
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const body = document.body;
|
||||
if ((body.getAttribute("data-dashboard-mode") || "") !== "addons") return;
|
||||
|
||||
const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {RequestInit} [opts]
|
||||
*/
|
||||
async function api(path, opts) {
|
||||
const url = path.startsWith("http") ? path : API + path;
|
||||
const r = await fetch(url, opts);
|
||||
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 sel = document.getElementById("addons-cluster-select");
|
||||
const statusBar = document.getElementById("addons-status-bar");
|
||||
const logPre = document.getElementById("addons-log-pre");
|
||||
const overlay = document.getElementById("addons-busy-overlay");
|
||||
const busyLabel = document.getElementById("addons-busy-label");
|
||||
|
||||
function getClusterName() {
|
||||
if (!sel || !sel.value) return "";
|
||||
return String(sel.value).trim();
|
||||
}
|
||||
|
||||
function setBusy(show, label) {
|
||||
if (!overlay) return;
|
||||
overlay.classList.toggle("hidden", !show);
|
||||
overlay.setAttribute("aria-busy", show ? "true" : "false");
|
||||
overlay.setAttribute("aria-hidden", show ? "false" : "true");
|
||||
document.body.classList.toggle("cluster-addons-busy", !!show);
|
||||
if (busyLabel && label) busyLabel.textContent = label;
|
||||
}
|
||||
|
||||
/** Скрыть полноэкранный спиннер первой загрузки страницы (как на панели и странице кластера). */
|
||||
function hideAddonsPageLoadingOverlay() {
|
||||
const el = document.getElementById("addons-page-loading-overlay");
|
||||
if (!el) return;
|
||||
el.classList.add("hidden");
|
||||
el.setAttribute("aria-busy", "false");
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("cluster-addons-page-loading");
|
||||
}
|
||||
|
||||
function appendLog(line) {
|
||||
if (!logPre) return;
|
||||
logPre.textContent = (logPre.textContent ? logPre.textContent + "\n" : "") + line;
|
||||
logPre.scrollTop = logPre.scrollHeight;
|
||||
}
|
||||
|
||||
function formatStatus(st) {
|
||||
if (!st) return "";
|
||||
const parts = [
|
||||
"ingress-nginx: " + (st.ingress_nginx ? "да" : "нет"),
|
||||
"prometheus-stack: " + (st.kube_prometheus_stack ? "да" : "нет"),
|
||||
"metrics-server: " + (st.metrics_server ? "да" : "нет"),
|
||||
"Istio+Kiali: " + (st.istio_mesh_ready ? "да" : "нет"),
|
||||
];
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Заполнить выпадающий список версиями чарта; первая опция — «последняя».
|
||||
* @param {HTMLSelectElement | null} selectEl
|
||||
* @param {string[] | undefined} versions
|
||||
*/
|
||||
function fillVersionSelect(selectEl, versions) {
|
||||
if (!selectEl) return;
|
||||
const cur = selectEl.value;
|
||||
selectEl.innerHTML = "";
|
||||
const o0 = document.createElement("option");
|
||||
o0.value = "";
|
||||
o0.textContent = "Последняя (из репозитория)";
|
||||
selectEl.appendChild(o0);
|
||||
(versions || []).forEach(function (v) {
|
||||
if (!v) return;
|
||||
const o = document.createElement("option");
|
||||
o.value = v;
|
||||
o.textContent = v;
|
||||
selectEl.appendChild(o);
|
||||
});
|
||||
if (cur) {
|
||||
for (let i = 0; i < selectEl.options.length; i++) {
|
||||
if (selectEl.options[i].value === cur) {
|
||||
selectEl.value = cur;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChartVersions() {
|
||||
try {
|
||||
const d = await api("/helm/chart-versions");
|
||||
fillVersionSelect(document.getElementById("addons-ing-version-select"), d.ingress_nginx);
|
||||
fillVersionSelect(document.getElementById("addons-prom-version-select"), d.kube_prometheus_stack);
|
||||
fillVersionSelect(document.getElementById("addons-ms-version-select"), d.metrics_server);
|
||||
fillVersionSelect(document.getElementById("addons-istio-version-select"), d.istio);
|
||||
fillVersionSelect(document.getElementById("addons-kiali-version-select"), d.kiali_server);
|
||||
} catch (e) {
|
||||
appendLog("Список версий чартов недоступен: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadClusters() {
|
||||
if (!sel) return;
|
||||
try {
|
||||
const rows = await api("/clusters");
|
||||
sel.innerHTML = "";
|
||||
const withKube = rows.filter(function (c) { return c.has_local_kubeconfig; });
|
||||
if (!withKube.length) {
|
||||
const o = document.createElement("option");
|
||||
o.value = "";
|
||||
o.textContent = "Нет кластеров с kubeconfig на диске";
|
||||
sel.appendChild(o);
|
||||
return;
|
||||
}
|
||||
const o0 = document.createElement("option");
|
||||
o0.value = "";
|
||||
o0.textContent = "— выберите кластер —";
|
||||
sel.appendChild(o0);
|
||||
withKube.forEach(function (c) {
|
||||
const o = document.createElement("option");
|
||||
o.value = c.name;
|
||||
o.textContent = c.name + (c.kind_nodes_running ? "" : " (узлы остановлены)");
|
||||
sel.appendChild(o);
|
||||
});
|
||||
} catch (e) {
|
||||
sel.innerHTML = "";
|
||||
const o = document.createElement("option");
|
||||
o.value = "";
|
||||
o.textContent = "Ошибка загрузки списка";
|
||||
sel.appendChild(o);
|
||||
appendLog("Ошибка GET /clusters: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
const n = getClusterName();
|
||||
if (!statusBar) return;
|
||||
if (!n) {
|
||||
statusBar.textContent = "Выберите кластер для просмотра статуса аддонов.";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const st = await api("/clusters/" + encodeURIComponent(n) + "/addons/status");
|
||||
statusBar.textContent = formatStatus(st);
|
||||
} catch (e) {
|
||||
statusBar.textContent = "Статус недоступен: " + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function runInstall(endpoint, payload) {
|
||||
const n = getClusterName();
|
||||
if (!n) {
|
||||
alert("Выберите кластер.");
|
||||
return;
|
||||
}
|
||||
setBusy(true, "Установка (Helm), подождите…");
|
||||
if (logPre) logPre.textContent = "";
|
||||
appendLog("POST …/addons/" + endpoint + " для «" + n + "»");
|
||||
try {
|
||||
const r = await api("/clusters/" + encodeURIComponent(n) + "/addons/" + endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: payload !== undefined ? JSON.stringify(payload) : "{}",
|
||||
});
|
||||
appendLog(r.message || "OK");
|
||||
if (r.log) appendLog(r.log);
|
||||
await refreshStatus();
|
||||
} catch (e) {
|
||||
appendLog("Ошибка: " + e.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function runDelete(endpoint) {
|
||||
const n = getClusterName();
|
||||
if (!n) {
|
||||
alert("Выберите кластер.");
|
||||
return;
|
||||
}
|
||||
if (!window.confirm("Удалить аддон «" + endpoint + "» в кластере «" + n + "»?")) return;
|
||||
setBusy(true, "Удаление релиза, подождите…");
|
||||
if (logPre) logPre.textContent = "";
|
||||
appendLog("DELETE …/addons/" + endpoint + " для «" + n + "»");
|
||||
try {
|
||||
const r = await api("/clusters/" + encodeURIComponent(n) + "/addons/" + endpoint, {
|
||||
method: "DELETE",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
appendLog(r.message || "OK");
|
||||
if (r.log) appendLog(r.log);
|
||||
await refreshStatus();
|
||||
} catch (e) {
|
||||
appendLog("Ошибка: " + e.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll(".btn-addon-install").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
const ep = btn.getAttribute("data-endpoint") || "";
|
||||
if (ep === "ingress-nginx") {
|
||||
const sel = document.getElementById("addons-ing-version-select");
|
||||
const v = sel && sel.value.trim();
|
||||
runInstall(ep, v ? { chart_version: v } : {});
|
||||
return;
|
||||
}
|
||||
if (ep === "kube-prometheus-stack") {
|
||||
const u = document.getElementById("addons-grafana-user");
|
||||
const p = document.getElementById("addons-grafana-pass");
|
||||
const pv = document.getElementById("addons-prom-version-select");
|
||||
const user = u ? u.value.trim() : "admin";
|
||||
const pw = p ? p.value : "";
|
||||
if (pw.length < 8) {
|
||||
alert("Задайте пароль Grafana не короче 8 символов.");
|
||||
return;
|
||||
}
|
||||
const payload = { grafana_admin_user: user, grafana_admin_password: pw };
|
||||
if (pv && pv.value.trim()) payload.chart_version = pv.value.trim();
|
||||
runInstall(ep, payload);
|
||||
return;
|
||||
}
|
||||
if (ep === "metrics-server") {
|
||||
const ms = document.getElementById("addons-ms-version-select");
|
||||
const payload = {};
|
||||
if (ms && ms.value.trim()) payload.chart_version = ms.value.trim();
|
||||
runInstall(ep, Object.keys(payload).length ? payload : {});
|
||||
return;
|
||||
}
|
||||
if (ep === "istio-kiali") {
|
||||
const u = document.getElementById("addons-kiali-user");
|
||||
const p = document.getElementById("addons-kiali-pass");
|
||||
const isel = document.getElementById("addons-istio-version-select");
|
||||
const ksel = document.getElementById("addons-kiali-version-select");
|
||||
const user = u ? u.value.trim() : "kiali-admin";
|
||||
const pw = p ? p.value : "";
|
||||
if (pw.length < 8) {
|
||||
alert("Задайте пароль Kiali не короче 8 символов.");
|
||||
return;
|
||||
}
|
||||
const payload = { kiali_username: user, kiali_password: pw };
|
||||
if (isel && isel.value.trim()) payload.istio_chart_version = isel.value.trim();
|
||||
if (ksel && ksel.value.trim()) payload.kiali_chart_version = ksel.value.trim();
|
||||
runInstall(ep, payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".btn-addon-remove").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
const ep = btn.getAttribute("data-endpoint") || "";
|
||||
if (ep) runDelete(ep);
|
||||
});
|
||||
});
|
||||
|
||||
const refreshBtn = document.getElementById("addons-refresh-status");
|
||||
if (refreshBtn) refreshBtn.addEventListener("click", function () { refreshStatus(); });
|
||||
|
||||
if (sel) {
|
||||
sel.addEventListener("change", function () { refreshStatus(); });
|
||||
}
|
||||
|
||||
loadClusters()
|
||||
.then(function () {
|
||||
return refreshStatus();
|
||||
})
|
||||
.then(function () {
|
||||
return loadChartVersions();
|
||||
})
|
||||
.finally(function () {
|
||||
hideAddonsPageLoadingOverlay();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user