Веб-интерфейс: страница /clusters, навигация и крошки для кластеров
- Выделена страница списка кластеров, панель упрощена; nav_active и крошки ведут в раздел Кластеры; theme.js синхронизирует активную пилюлю по URL. - Доработки дашборда, аддонов, журнала, стилей и API-документации. - Поддержка Podman: docker-compose.podman.yml, скрипты сокета; Makefile и env.
This commit is contained in:
+494
-46
@@ -12,6 +12,115 @@
|
||||
|
||||
const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
|
||||
|
||||
/** data-endpoint кнопки → поле ответа GET …/addons/status */
|
||||
var ENDPOINT_TO_STATUS_KEY = {
|
||||
"ingress-nginx": "ingress_nginx",
|
||||
"kube-prometheus-stack": "kube_prometheus_stack",
|
||||
"metrics-server": "metrics_server",
|
||||
"istio-kiali": "istio_mesh_ready",
|
||||
};
|
||||
|
||||
var ADDON_HUMAN_LABEL = {
|
||||
"ingress-nginx": "ingress-nginx",
|
||||
"kube-prometheus-stack": "kube-prometheus-stack",
|
||||
"metrics-server": "metrics-server",
|
||||
"istio-kiali": "Istio + Kiali",
|
||||
};
|
||||
|
||||
/** Последний успешный снимок статуса (для подписи «Установить» / «Обновить»). */
|
||||
var lastAddonsStatus = null;
|
||||
|
||||
/** @type {((value: boolean) => void) | null} */
|
||||
var confirmModalResolver = null;
|
||||
var confirmModalIsAlert = false;
|
||||
|
||||
function isConfirmModalOpen() {
|
||||
const el = document.getElementById("confirm-modal-overlay");
|
||||
return !!(el && !el.classList.contains("hidden"));
|
||||
}
|
||||
|
||||
function closeConfirmModal(confirmed) {
|
||||
const ov = document.getElementById("confirm-modal-overlay");
|
||||
const cancelBtn = document.getElementById("confirm-modal-cancel");
|
||||
if (ov) ov.classList.add("hidden");
|
||||
document.body.classList.remove("modal-open");
|
||||
if (cancelBtn) cancelBtn.classList.remove("hidden");
|
||||
if (confirmModalResolver) {
|
||||
var fn = confirmModalResolver;
|
||||
confirmModalResolver = null;
|
||||
var out = confirmModalIsAlert ? true : !!confirmed;
|
||||
confirmModalIsAlert = false;
|
||||
fn(out);
|
||||
} else {
|
||||
confirmModalIsAlert = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Модальное подтверждение (разметка из partials/dashboard_modals.html).
|
||||
* @param {{ title?: string, message: string, confirmLabel?: string, danger?: boolean, alert?: boolean }} opts
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
function openConfirmModal(opts) {
|
||||
return new Promise(function (resolve) {
|
||||
const ov = document.getElementById("confirm-modal-overlay");
|
||||
const titleEl = document.getElementById("confirm-modal-title");
|
||||
const msgEl = document.getElementById("confirm-modal-message");
|
||||
const okBtn = document.getElementById("confirm-modal-ok");
|
||||
const cancelBtn = document.getElementById("confirm-modal-cancel");
|
||||
if (!ov || !titleEl || !msgEl || !okBtn) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
if (confirmModalResolver) {
|
||||
const prevFn = confirmModalResolver;
|
||||
confirmModalResolver = null;
|
||||
confirmModalIsAlert = false;
|
||||
prevFn(false);
|
||||
ov.classList.add("hidden");
|
||||
document.body.classList.remove("modal-open");
|
||||
if (cancelBtn) cancelBtn.classList.remove("hidden");
|
||||
}
|
||||
confirmModalResolver = resolve;
|
||||
confirmModalIsAlert = !!opts.alert;
|
||||
titleEl.textContent = opts.title || "Подтвердите действие";
|
||||
msgEl.textContent = opts.message || "";
|
||||
okBtn.textContent = opts.confirmLabel || (confirmModalIsAlert ? "Понятно" : "Подтвердить");
|
||||
okBtn.className = opts.danger ? "btn-danger" : "";
|
||||
if (cancelBtn) {
|
||||
cancelBtn.classList.toggle("hidden", confirmModalIsAlert);
|
||||
}
|
||||
ov.classList.remove("hidden");
|
||||
document.body.classList.add("modal-open");
|
||||
okBtn.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function bindConfirmModalControls() {
|
||||
const confirmOv = document.getElementById("confirm-modal-overlay");
|
||||
const confirmCancel = document.getElementById("confirm-modal-cancel");
|
||||
const confirmOk = document.getElementById("confirm-modal-ok");
|
||||
if (confirmCancel) {
|
||||
confirmCancel.addEventListener("click", function () {
|
||||
closeConfirmModal(false);
|
||||
});
|
||||
}
|
||||
if (confirmOk) {
|
||||
confirmOk.addEventListener("click", function () {
|
||||
closeConfirmModal(true);
|
||||
});
|
||||
}
|
||||
if (confirmOv) {
|
||||
confirmOv.addEventListener("click", function (ev) {
|
||||
if (ev.target === confirmOv) closeConfirmModal(confirmModalIsAlert ? true : false);
|
||||
});
|
||||
}
|
||||
document.addEventListener("keydown", function (ev) {
|
||||
if (ev.key !== "Escape" || !isConfirmModalOpen()) return;
|
||||
closeConfirmModal(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {RequestInit} [opts]
|
||||
@@ -42,22 +151,80 @@
|
||||
|
||||
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");
|
||||
const addonsCardsWrap = document.getElementById("addons-cards-wrap");
|
||||
const helmWrap = document.getElementById("addons-helm-progress-wrap");
|
||||
const helmHint = document.getElementById("addons-helm-progress-hint");
|
||||
const helmBar = document.getElementById("addons-helm-progress-bar");
|
||||
const helmTrack = document.getElementById("addons-helm-progress-track");
|
||||
const helmLabel = document.getElementById("addons-helm-progress-label");
|
||||
const helmLogPanel = document.getElementById("addons-helm-log-panel");
|
||||
const bootstrapMsg = document.getElementById("addons-bootstrap-msg");
|
||||
|
||||
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 updateAddonsCardsVisibility() {
|
||||
if (!addonsCardsWrap) return;
|
||||
var n = getClusterName();
|
||||
addonsCardsWrap.classList.toggle("hidden", !n);
|
||||
addonsCardsWrap.setAttribute("aria-hidden", n ? "false" : "true");
|
||||
}
|
||||
|
||||
/**
|
||||
* Предупреждения загрузки списка кластеров / версий чартов (не путать с журналом Helm).
|
||||
* @param {string} text пустая строка — скрыть блок
|
||||
*/
|
||||
function setBootstrapMsg(text) {
|
||||
if (!bootstrapMsg) return;
|
||||
var s = (text || "").trim();
|
||||
if (!s) {
|
||||
bootstrapMsg.classList.add("hidden");
|
||||
bootstrapMsg.textContent = "";
|
||||
return;
|
||||
}
|
||||
bootstrapMsg.classList.remove("hidden");
|
||||
bootstrapMsg.textContent = s;
|
||||
}
|
||||
|
||||
/** Показать карточку «Ход операции Helm» (как create-progress-wrap на странице создания кластера). */
|
||||
function revealHelmProgressSection() {
|
||||
if (!helmWrap) return;
|
||||
helmWrap.classList.remove("hidden");
|
||||
helmWrap.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
|
||||
function updateHelmProgressBar(pct, stage) {
|
||||
var p = Math.max(0, Math.min(100, Number(pct) || 0));
|
||||
if (helmBar) helmBar.style.width = p + "%";
|
||||
if (helmTrack) helmTrack.setAttribute("aria-valuenow", String(p));
|
||||
if (helmLabel) helmLabel.textContent = stage || "—";
|
||||
}
|
||||
|
||||
/** Полный текст журнала в панели с теми же классами, что «Журнал задания» при создании кластера. */
|
||||
function setHelmLogText(text) {
|
||||
if (!helmLogPanel) return;
|
||||
helmLogPanel.textContent = text || "";
|
||||
helmLogPanel.scrollTop = helmLogPanel.scrollHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Блокировка форм на время синхронного запроса Helm (без полноэкранного спиннера).
|
||||
* @param {boolean} disabled
|
||||
*/
|
||||
function setAddonFormsDisabled(disabled) {
|
||||
document
|
||||
.querySelectorAll(
|
||||
"#addons-cards-wrap .cluster-addon-card button, #addons-cards-wrap .cluster-addon-card input, #addons-cards-wrap .cluster-addon-card select, #addons-cards-wrap .cluster-addon-card textarea",
|
||||
)
|
||||
.forEach(function (el) {
|
||||
el.disabled = !!disabled;
|
||||
});
|
||||
if (sel) sel.disabled = !!disabled;
|
||||
var refBtn = document.getElementById("addons-refresh-status");
|
||||
if (refBtn) refBtn.disabled = !!disabled;
|
||||
}
|
||||
|
||||
/** Скрыть полноэкранный спиннер первой загрузки страницы (как на панели и странице кластера). */
|
||||
@@ -70,21 +237,177 @@
|
||||
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;
|
||||
/**
|
||||
* Текстовое сообщение в полосе статуса (нет кластера / ошибка).
|
||||
* @param {string} text
|
||||
*/
|
||||
function renderAddonStatusMessage(text) {
|
||||
if (!statusBar) return;
|
||||
statusBar.className = "cluster-addons-status-bar cluster-addons-status-bar--message muted";
|
||||
statusBar.replaceChildren();
|
||||
const p = document.createElement("p");
|
||||
p.className = "cluster-addons-status-msg";
|
||||
p.textContent = text;
|
||||
statusBar.appendChild(p);
|
||||
}
|
||||
|
||||
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(" · ");
|
||||
/** Максимальная длина поля values_yaml на сервере (см. Pydantic max_length). */
|
||||
var VALUES_YAML_MAX_LEN = 131072;
|
||||
|
||||
/**
|
||||
* Полоса под селектором кластера: только сообщения (нет кластера / ошибка), без сетки статусов.
|
||||
*/
|
||||
function clearAddonToolbarStatusBar() {
|
||||
if (!statusBar) return;
|
||||
statusBar.className = "cluster-addons-status-bar";
|
||||
statusBar.replaceChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* Бейджи «Установлен» / «Не установлен» в правом верхнем углу карточки аддона.
|
||||
* @param {object | null | undefined} st ответ GET …/addons/status или null при сбросе
|
||||
*/
|
||||
function updateAddonCardBadges(st) {
|
||||
document.querySelectorAll(".cluster-addon-card[data-addon]").forEach(function (card) {
|
||||
var da = card.getAttribute("data-addon") || "";
|
||||
var key = ENDPOINT_TO_STATUS_KEY[da];
|
||||
var on = !!(st && key && st[key]);
|
||||
var badge = card.querySelector(".cluster-addon-card__badge");
|
||||
if (!badge) return;
|
||||
badge.textContent = on ? "Установлен" : "Не установлен";
|
||||
badge.className =
|
||||
"cluster-addon-card__badge" + (on ? " cluster-addon-card__badge--on" : " cluster-addon-card__badge--off");
|
||||
var hum = ADDON_HUMAN_LABEL[da] || da;
|
||||
badge.setAttribute("aria-label", hum + ": " + (on ? "установлен" : "не установлен"));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Прикрепить к телу POST YAML из textarea (полные values чарта). Для istio-kiali — три поля.
|
||||
* Пустой textarea → поле не отправляется (сервер соберёт defaults или пропустит -f).
|
||||
* @param {string} ep data-endpoint
|
||||
* @param {Record<string, unknown>} payload
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function attachAddonYamlToPayload(ep, payload) {
|
||||
if (ep === "istio-kiali") {
|
||||
var triple = [
|
||||
{ id: "addons-values-yaml-istio-kiali", key: "values_yaml" },
|
||||
{ id: "addons-values-yaml-istio-base", key: "istio_base_values_yaml" },
|
||||
{ id: "addons-values-yaml-istio-istiod", key: "istio_istiod_values_yaml" },
|
||||
];
|
||||
for (var i = 0; i < triple.length; i++) {
|
||||
var ta = document.getElementById(triple[i].id);
|
||||
if (!ta) continue;
|
||||
var s = String(ta.value || "").trim();
|
||||
if (!s) continue;
|
||||
if (s.length > VALUES_YAML_MAX_LEN) {
|
||||
alert("Текст YAML слишком длинный (максимум " + VALUES_YAML_MAX_LEN + " символов).");
|
||||
return false;
|
||||
}
|
||||
payload[triple[i].key] = s;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
var id = "addons-values-yaml-" + ep;
|
||||
var ta2 = document.getElementById(id);
|
||||
if (!ta2) return true;
|
||||
var s2 = String(ta2.value || "").trim();
|
||||
if (!s2) return true;
|
||||
if (s2.length > VALUES_YAML_MAX_LEN) {
|
||||
alert("Текст YAML слишком длинный (максимум " + VALUES_YAML_MAX_LEN + " символов).");
|
||||
return false;
|
||||
}
|
||||
payload.values_yaml = s2;
|
||||
return true;
|
||||
}
|
||||
|
||||
var COMPOSE_DEBOUNCE_MS = 600;
|
||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||||
var composeDebounceTimer = null;
|
||||
|
||||
/**
|
||||
* Заполнить все textarea: один POST **compose-values-batch** (один helm repo update на сервере).
|
||||
*/
|
||||
async function refreshAllComposeValuesFromServer() {
|
||||
setBootstrapMsg("");
|
||||
var si = document.getElementById("addons-ing-version-select");
|
||||
var pv = document.getElementById("addons-prom-version-select");
|
||||
var ms = document.getElementById("addons-ms-version-select");
|
||||
var isel = document.getElementById("addons-istio-version-select");
|
||||
var ksel = document.getElementById("addons-kiali-version-select");
|
||||
var gu = document.getElementById("addons-grafana-user");
|
||||
var gp = document.getElementById("addons-grafana-pass");
|
||||
var batchBody = {
|
||||
ingress_chart_version: si && si.value.trim() ? si.value.trim() : null,
|
||||
kube_prometheus_chart_version: pv && pv.value.trim() ? pv.value.trim() : null,
|
||||
metrics_server_chart_version: ms && ms.value.trim() ? ms.value.trim() : null,
|
||||
istio_chart_version: isel && isel.value.trim() ? isel.value.trim() : null,
|
||||
kiali_chart_version: ksel && ksel.value.trim() ? ksel.value.trim() : null,
|
||||
grafana_admin_user: gu ? gu.value.trim() || null : null,
|
||||
grafana_admin_password: gp ? gp.value || null : null,
|
||||
};
|
||||
try {
|
||||
var r = await api("/helm/addons/compose-values-batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify(batchBody),
|
||||
});
|
||||
var ti = document.getElementById("addons-values-yaml-ingress-nginx");
|
||||
var tp = document.getElementById("addons-values-yaml-kube-prometheus-stack");
|
||||
var tm = document.getElementById("addons-values-yaml-metrics-server");
|
||||
var tk = document.getElementById("addons-values-yaml-istio-kiali");
|
||||
var tb = document.getElementById("addons-values-yaml-istio-base");
|
||||
var tiod = document.getElementById("addons-values-yaml-istio-istiod");
|
||||
if (ti) ti.value = r.ingress_nginx_values_yaml || "";
|
||||
if (tp) tp.value = r.kube_prometheus_values_yaml || "";
|
||||
if (tm) tm.value = r.metrics_server_values_yaml || "";
|
||||
if (tk) tk.value = r.kiali_values_yaml || "";
|
||||
if (tb) tb.value = r.istio_base_values_yaml || "";
|
||||
if (tiod) tiod.value = r.istio_istiod_values_yaml || "";
|
||||
} catch (e) {
|
||||
setBootstrapMsg("Не удалось подставить values из чартов (helm show values): " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleComposeValuesRefresh() {
|
||||
if (composeDebounceTimer) clearTimeout(composeDebounceTimer);
|
||||
composeDebounceTimer = setTimeout(function () {
|
||||
composeDebounceTimer = null;
|
||||
refreshAllComposeValuesFromServer();
|
||||
}, COMPOSE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
function addonEndpointInstalled(endpoint) {
|
||||
var key = ENDPOINT_TO_STATUS_KEY[endpoint];
|
||||
return !!(lastAddonsStatus && key && lastAddonsStatus[key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Текст «Установить»/«Обновить», видимость «Удалить» только при установленном аддоне; равная ширина — в CSS.
|
||||
*/
|
||||
function updateAddonActionButtons() {
|
||||
document.querySelectorAll(".cluster-addon-card__actions").forEach(function (row) {
|
||||
var installBtn = row.querySelector(".btn-addon-install");
|
||||
var removeBtn = row.querySelector(".btn-addon-remove");
|
||||
if (!installBtn) return;
|
||||
var ep = installBtn.getAttribute("data-endpoint") || "";
|
||||
var on = addonEndpointInstalled(ep);
|
||||
installBtn.textContent = on ? "Обновить" : "Установить";
|
||||
var hum = ADDON_HUMAN_LABEL[ep] || ep;
|
||||
installBtn.setAttribute("aria-label", (on ? "Обновить " : "Установить ") + hum);
|
||||
if (removeBtn) {
|
||||
if (on) {
|
||||
removeBtn.classList.remove("hidden");
|
||||
removeBtn.removeAttribute("hidden");
|
||||
removeBtn.setAttribute("aria-label", "Удалить " + hum);
|
||||
} else {
|
||||
removeBtn.classList.add("hidden");
|
||||
removeBtn.setAttribute("hidden", "hidden");
|
||||
removeBtn.removeAttribute("aria-label");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,13 +443,15 @@
|
||||
async function loadChartVersions() {
|
||||
try {
|
||||
const d = await api("/helm/chart-versions");
|
||||
setBootstrapMsg("");
|
||||
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);
|
||||
await refreshAllComposeValuesFromServer();
|
||||
} catch (e) {
|
||||
appendLog("Список версий чартов недоступен: " + e.message);
|
||||
setBootstrapMsg("Список версий чартов недоступен: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +459,7 @@
|
||||
if (!sel) return;
|
||||
try {
|
||||
const rows = await api("/clusters");
|
||||
setBootstrapMsg("");
|
||||
sel.innerHTML = "";
|
||||
const withKube = rows.filter(function (c) { return c.has_local_kubeconfig; });
|
||||
if (!withKube.length) {
|
||||
@@ -159,23 +485,37 @@
|
||||
o.value = "";
|
||||
o.textContent = "Ошибка загрузки списка";
|
||||
sel.appendChild(o);
|
||||
appendLog("Ошибка GET /clusters: " + e.message);
|
||||
setBootstrapMsg("Ошибка GET /clusters: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
const n = getClusterName();
|
||||
if (!statusBar) return;
|
||||
if (!statusBar) {
|
||||
updateAddonsCardsVisibility();
|
||||
return;
|
||||
}
|
||||
if (!n) {
|
||||
statusBar.textContent = "Выберите кластер для просмотра статуса аддонов.";
|
||||
lastAddonsStatus = null;
|
||||
clearAddonToolbarStatusBar();
|
||||
updateAddonCardBadges(null);
|
||||
updateAddonActionButtons();
|
||||
updateAddonsCardsVisibility();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const st = await api("/clusters/" + encodeURIComponent(n) + "/addons/status");
|
||||
statusBar.textContent = formatStatus(st);
|
||||
lastAddonsStatus = st;
|
||||
clearAddonToolbarStatusBar();
|
||||
updateAddonCardBadges(st);
|
||||
updateAddonActionButtons();
|
||||
} catch (e) {
|
||||
statusBar.textContent = "Статус недоступен: " + e.message;
|
||||
lastAddonsStatus = null;
|
||||
renderAddonStatusMessage("Статус недоступен: " + e.message);
|
||||
updateAddonCardBadges(null);
|
||||
updateAddonActionButtons();
|
||||
}
|
||||
updateAddonsCardsVisibility();
|
||||
}
|
||||
|
||||
async function runInstall(endpoint, payload) {
|
||||
@@ -184,22 +524,63 @@
|
||||
alert("Выберите кластер.");
|
||||
return;
|
||||
}
|
||||
setBusy(true, "Установка (Helm), подождите…");
|
||||
if (logPre) logPre.textContent = "";
|
||||
appendLog("POST …/addons/" + endpoint + " для «" + n + "»");
|
||||
const isUpdate = addonEndpointInstalled(endpoint);
|
||||
const hum = ADDON_HUMAN_LABEL[endpoint] || endpoint;
|
||||
revealHelmProgressSection();
|
||||
setAddonFormsDisabled(true);
|
||||
if (helmHint) {
|
||||
helmHint.textContent = isUpdate
|
||||
? "Обновление релиза «" + hum + "» через helm в контейнере; ниже — полный вывод по завершении."
|
||||
: "Установка релиза «" + hum + "» через helm в контейнере; ниже — полный вывод по завершении.";
|
||||
}
|
||||
updateHelmProgressBar(8, isUpdate ? "Обновление Helm…" : "Установка Helm…");
|
||||
setHelmLogText(
|
||||
"Кластер: «" +
|
||||
n +
|
||||
"»\n" +
|
||||
(isUpdate ? "POST (обновление)" : "POST (установка)") +
|
||||
" /api/v1/clusters/…/addons/" +
|
||||
endpoint +
|
||||
"\n\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);
|
||||
var okLines = [
|
||||
"Кластер: «" + n + "»",
|
||||
(isUpdate ? "POST (обновление)" : "POST (установка)") + " /addons/" + endpoint,
|
||||
"",
|
||||
r.message || "OK",
|
||||
];
|
||||
if (r.log) {
|
||||
okLines.push("");
|
||||
okLines.push(String(r.log));
|
||||
}
|
||||
setHelmLogText(okLines.join("\n"));
|
||||
updateHelmProgressBar(100, "Готово");
|
||||
if (helmHint) {
|
||||
helmHint.textContent =
|
||||
"Операция завершена. Полный журнал добавлен в файл helm_addon_log.json в каталоге кластера (история записей).";
|
||||
}
|
||||
await refreshStatus();
|
||||
} catch (e) {
|
||||
appendLog("Ошибка: " + e.message);
|
||||
var failLines = [
|
||||
"Кластер: «" + n + "»",
|
||||
(isUpdate ? "POST (обновление)" : "POST (установка)") + " /addons/" + endpoint,
|
||||
"",
|
||||
"Ошибка: " + e.message,
|
||||
];
|
||||
setHelmLogText(failLines.join("\n"));
|
||||
updateHelmProgressBar(100, "Ошибка");
|
||||
if (helmHint) {
|
||||
helmHint.textContent =
|
||||
"Запрос не удался. При ответе сервера об ошибке Helm в журнале кластера могла появиться запись со статусом failed.";
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
setAddonFormsDisabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,22 +590,66 @@
|
||||
alert("Выберите кластер.");
|
||||
return;
|
||||
}
|
||||
if (!window.confirm("Удалить аддон «" + endpoint + "» в кластере «" + n + "»?")) return;
|
||||
setBusy(true, "Удаление релиза, подождите…");
|
||||
if (logPre) logPre.textContent = "";
|
||||
appendLog("DELETE …/addons/" + endpoint + " для «" + n + "»");
|
||||
const hum = ADDON_HUMAN_LABEL[endpoint] || endpoint;
|
||||
const ok = await openConfirmModal({
|
||||
title: "Удалить аддон?",
|
||||
message:
|
||||
"Кластер «" +
|
||||
n +
|
||||
"»: снять релиз «" +
|
||||
hum +
|
||||
"» с кластера (helm uninstall). Действие необратимо для установленных компонентов.",
|
||||
confirmLabel: "Удалить",
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
revealHelmProgressSection();
|
||||
setAddonFormsDisabled(true);
|
||||
if (helmHint) {
|
||||
helmHint.textContent =
|
||||
"Удаление релиза «" + hum + "» (helm uninstall); ниже — полный вывод по завершении.";
|
||||
}
|
||||
updateHelmProgressBar(8, "Удаление Helm…");
|
||||
setHelmLogText(
|
||||
"Кластер: «" + n + "»\nDELETE /api/v1/clusters/…/addons/" + endpoint + "\n\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);
|
||||
var okDel = [
|
||||
"Кластер: «" + n + "»",
|
||||
"DELETE /addons/" + endpoint,
|
||||
"",
|
||||
r.message || "OK",
|
||||
];
|
||||
if (r.log) {
|
||||
okDel.push("");
|
||||
okDel.push(String(r.log));
|
||||
}
|
||||
setHelmLogText(okDel.join("\n"));
|
||||
updateHelmProgressBar(100, "Готово");
|
||||
if (helmHint) {
|
||||
helmHint.textContent =
|
||||
"Операция завершена. Запись добавлена в helm_addon_log.json в каталоге кластера (история).";
|
||||
}
|
||||
await refreshStatus();
|
||||
} catch (e) {
|
||||
appendLog("Ошибка: " + e.message);
|
||||
var failDel = [
|
||||
"Кластер: «" + n + "»",
|
||||
"DELETE /addons/" + endpoint,
|
||||
"",
|
||||
"Ошибка: " + e.message,
|
||||
];
|
||||
setHelmLogText(failDel.join("\n"));
|
||||
updateHelmProgressBar(100, "Ошибка");
|
||||
if (helmHint) {
|
||||
helmHint.textContent =
|
||||
"Запрос не удался. При ошибке Helm на сервере в журнале кластера могла появиться запись со статусом failed.";
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
setAddonFormsDisabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +659,10 @@
|
||||
if (ep === "ingress-nginx") {
|
||||
const sel = document.getElementById("addons-ing-version-select");
|
||||
const v = sel && sel.value.trim();
|
||||
runInstall(ep, v ? { chart_version: v } : {});
|
||||
const payload = {};
|
||||
if (v) payload.chart_version = v;
|
||||
if (!attachAddonYamlToPayload(ep, payload)) return;
|
||||
runInstall(ep, payload);
|
||||
return;
|
||||
}
|
||||
if (ep === "kube-prometheus-stack") {
|
||||
@@ -249,6 +677,7 @@
|
||||
}
|
||||
const payload = { grafana_admin_user: user, grafana_admin_password: pw };
|
||||
if (pv && pv.value.trim()) payload.chart_version = pv.value.trim();
|
||||
if (!attachAddonYamlToPayload(ep, payload)) return;
|
||||
runInstall(ep, payload);
|
||||
return;
|
||||
}
|
||||
@@ -256,7 +685,8 @@
|
||||
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 : {});
|
||||
if (!attachAddonYamlToPayload(ep, payload)) return;
|
||||
runInstall(ep, payload);
|
||||
return;
|
||||
}
|
||||
if (ep === "istio-kiali") {
|
||||
@@ -273,6 +703,7 @@
|
||||
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();
|
||||
if (!attachAddonYamlToPayload(ep, payload)) return;
|
||||
runInstall(ep, payload);
|
||||
}
|
||||
});
|
||||
@@ -292,6 +723,23 @@
|
||||
sel.addEventListener("change", function () { refreshStatus(); });
|
||||
}
|
||||
|
||||
bindConfirmModalControls();
|
||||
|
||||
[
|
||||
"addons-ing-version-select",
|
||||
"addons-prom-version-select",
|
||||
"addons-ms-version-select",
|
||||
"addons-istio-version-select",
|
||||
"addons-kiali-version-select",
|
||||
].forEach(function (id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.addEventListener("change", function () { scheduleComposeValuesRefresh(); });
|
||||
});
|
||||
var grafanaUserEl = document.getElementById("addons-grafana-user");
|
||||
var grafanaPassEl = document.getElementById("addons-grafana-pass");
|
||||
if (grafanaUserEl) grafanaUserEl.addEventListener("input", function () { scheduleComposeValuesRefresh(); });
|
||||
if (grafanaPassEl) grafanaPassEl.addEventListener("input", function () { scheduleComposeValuesRefresh(); });
|
||||
|
||||
loadClusters()
|
||||
.then(function () {
|
||||
return refreshStatus();
|
||||
|
||||
Reference in New Issue
Block a user