UI: карточка установленных аддонов на странице кластера; API аддонов и правки стилей
- Отдельная section.card после сводки ресурсов узлов, выравнивание бейджа Helm с заголовком - Расширение эндпоинтов аддонов, схемы, helm_addons, cluster_addons.js/html - Обновление api_routes.md
This commit is contained in:
+430
-76
@@ -8,7 +8,9 @@
|
||||
"use strict";
|
||||
|
||||
const body = document.body;
|
||||
if ((body.getAttribute("data-dashboard-mode") || "") !== "addons") return;
|
||||
/* trim: лишний пробел/перенос в шаблоне ломал бы проверку — скрипт выходил бы до bootstrap и оверлей не снимался */
|
||||
var dashMode = (body.getAttribute("data-dashboard-mode") || body.dataset.dashboardMode || "").trim();
|
||||
if (dashMode !== "addons") return;
|
||||
|
||||
const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
|
||||
|
||||
@@ -30,6 +32,12 @@
|
||||
/** Последний успешный снимок статуса (для подписи «Установить» / «Обновить»). */
|
||||
var lastAddonsStatus = null;
|
||||
|
||||
/** Список версий из GET /helm/chart-versions уже загружен — можно мержить версии из кластера. */
|
||||
var chartVersionsReady = false;
|
||||
|
||||
/** Суффикс подписи для версии, совпадающей с установленной в кластере. */
|
||||
var INSTALLED_VER_SUFFIX = " (текущая установленная версия)";
|
||||
|
||||
/** @type {((value: boolean) => void) | null} */
|
||||
var confirmModalResolver = null;
|
||||
var confirmModalIsAlert = false;
|
||||
@@ -149,6 +157,37 @@
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обёртка над Promise: если запрос «висит» (сеть, прокси), await иначе не завершится и finally не выполнится.
|
||||
* @param {Promise<unknown>} promise
|
||||
* @param {number} ms
|
||||
* @param {string} timeoutMessage
|
||||
*/
|
||||
function promiseWithTimeout(promise, ms, timeoutMessage) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var finished = false;
|
||||
var to = setTimeout(function () {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
reject(new Error(timeoutMessage));
|
||||
}, ms);
|
||||
Promise.resolve(promise).then(
|
||||
function (v) {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
clearTimeout(to);
|
||||
resolve(v);
|
||||
},
|
||||
function (err) {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
clearTimeout(to);
|
||||
reject(err);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const sel = document.getElementById("addons-cluster-select");
|
||||
const statusBar = document.getElementById("addons-status-bar");
|
||||
const addonsCardsWrap = document.getElementById("addons-cards-wrap");
|
||||
@@ -189,11 +228,54 @@
|
||||
bootstrapMsg.textContent = s;
|
||||
}
|
||||
|
||||
/** Показать карточку «Ход операции Helm» (как create-progress-wrap на странице создания кластера). */
|
||||
function revealHelmProgressSection() {
|
||||
/** Известные data-addon карточек (только они — безопасный перенос DOM без произвольного селектора). */
|
||||
var ADDON_CARD_MOUNT_KEYS = {
|
||||
"ingress-nginx": true,
|
||||
"kube-prometheus-stack": true,
|
||||
"metrics-server": true,
|
||||
"istio-kiali": true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Переносит блок «Ход операции Helm» в конец карточки выбранного аддона (под кнопки).
|
||||
* Если карточку не нашли — оставляем/возвращаем в #addons-helm-progress-dock.
|
||||
* @param {string} endpoint то же, что data-endpoint / data-addon
|
||||
*/
|
||||
function mountHelmProgressUnderAddon(endpoint) {
|
||||
if (!helmWrap) return;
|
||||
var dock = document.getElementById("addons-helm-progress-dock");
|
||||
var ep = typeof endpoint === "string" ? endpoint.trim() : "";
|
||||
var card = null;
|
||||
if (ep && ADDON_CARD_MOUNT_KEYS[ep]) {
|
||||
card = document.querySelector('.cluster-addon-card[data-addon="' + ep + '"]');
|
||||
}
|
||||
var parent = card || dock;
|
||||
if (!parent) return;
|
||||
parent.appendChild(helmWrap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать блок «Ход операции Helm»; при переданном endpoint — сразу под соответствующей карточкой.
|
||||
* @param {string} [endpoint]
|
||||
*/
|
||||
function revealHelmProgressSection(endpoint) {
|
||||
if (endpoint) {
|
||||
mountHelmProgressUnderAddon(endpoint);
|
||||
}
|
||||
if (!helmWrap) return;
|
||||
helmWrap.classList.remove("hidden");
|
||||
helmWrap.setAttribute("aria-hidden", "false");
|
||||
requestAnimationFrame(function () {
|
||||
try {
|
||||
helmWrap.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
} catch (e1) {
|
||||
try {
|
||||
helmWrap.scrollIntoView(true);
|
||||
} catch (e2) {
|
||||
/* старые браузеры */
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateHelmProgressBar(pct, stage) {
|
||||
@@ -227,8 +309,15 @@
|
||||
if (refBtn) refBtn.disabled = !!disabled;
|
||||
}
|
||||
|
||||
/** Таймер «последней надежды», если async bootstrap не дошёл до hide (ошибка до finally, зависший fetch). */
|
||||
var addonsOverlayFailsafeTimer = null;
|
||||
|
||||
/** Скрыть полноэкранный спиннер первой загрузки страницы (как на панели и странице кластера). */
|
||||
function hideAddonsPageLoadingOverlay() {
|
||||
if (addonsOverlayFailsafeTimer !== null) {
|
||||
clearTimeout(addonsOverlayFailsafeTimer);
|
||||
addonsOverlayFailsafeTimer = null;
|
||||
}
|
||||
const el = document.getElementById("addons-page-loading-overlay");
|
||||
if (!el) return;
|
||||
el.classList.add("hidden");
|
||||
@@ -237,6 +326,24 @@
|
||||
document.body.classList.remove("cluster-addons-page-loading");
|
||||
}
|
||||
|
||||
/** Страховка на случай зависшего fetch без ответа/reject. */
|
||||
function armAddonsOverlayFailsafe(ms) {
|
||||
if (addonsOverlayFailsafeTimer !== null) {
|
||||
clearTimeout(addonsOverlayFailsafeTimer);
|
||||
addonsOverlayFailsafeTimer = null;
|
||||
}
|
||||
var delay = typeof ms === "number" && ms > 0 ? ms : 40000;
|
||||
addonsOverlayFailsafeTimer = setTimeout(function () {
|
||||
addonsOverlayFailsafeTimer = null;
|
||||
var el = document.getElementById("addons-page-loading-overlay");
|
||||
if (!el || el.classList.contains("hidden")) return;
|
||||
hideAddonsPageLoadingOverlay();
|
||||
setBootstrapMsg(
|
||||
"Превышено время ожидания ответа сервера. Проверьте сеть, прокси и доступность /api/v1, затем обновите страницу.",
|
||||
);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Текстовое сообщение в полосе статуса (нет кластера / ошибка).
|
||||
* @param {string} text
|
||||
@@ -322,60 +429,118 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
var COMPOSE_DEBOUNCE_MS = 600;
|
||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||||
var composeDebounceTimer = null;
|
||||
/** id панели с YAML (скрыта до первой загрузки по кнопке). */
|
||||
var ADDON_VALUES_PANEL_ID = {
|
||||
"ingress-nginx": "addons-values-panel-ingress-nginx",
|
||||
"kube-prometheus-stack": "addons-values-panel-kube-prometheus-stack",
|
||||
"metrics-server": "addons-values-panel-metrics-server",
|
||||
"istio-kiali": "addons-values-panel-istio-kiali",
|
||||
};
|
||||
|
||||
/**
|
||||
* Заполнить все textarea: один POST **compose-values-batch** (один helm repo update на сервере).
|
||||
* Скрыть блок Values и очистить textarea для одного аддона (смена версии / сброс).
|
||||
* @param {string} addon ключ data-addon / endpoint
|
||||
*/
|
||||
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 hideAddonValuesPanel(addon) {
|
||||
var pid = ADDON_VALUES_PANEL_ID[addon];
|
||||
if (pid) {
|
||||
var panel = document.getElementById(pid);
|
||||
if (panel) {
|
||||
panel.classList.add("hidden");
|
||||
panel.setAttribute("hidden", "hidden");
|
||||
panel.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
}
|
||||
if (addon === "istio-kiali") {
|
||||
["addons-values-yaml-istio-kiali", "addons-values-yaml-istio-base", "addons-values-yaml-istio-istiod"].forEach(
|
||||
function (tid) {
|
||||
var ta = document.getElementById(tid);
|
||||
if (ta) ta.value = "";
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
var ta = document.getElementById("addons-values-yaml-" + addon);
|
||||
if (ta) ta.value = "";
|
||||
}
|
||||
|
||||
function scheduleComposeValuesRefresh() {
|
||||
if (composeDebounceTimer) clearTimeout(composeDebounceTimer);
|
||||
composeDebounceTimer = setTimeout(function () {
|
||||
composeDebounceTimer = null;
|
||||
refreshAllComposeValuesFromServer();
|
||||
}, COMPOSE_DEBOUNCE_MS);
|
||||
function showAddonValuesPanel(addon) {
|
||||
var pid = ADDON_VALUES_PANEL_ID[addon];
|
||||
if (!pid) return;
|
||||
var panel = document.getElementById(pid);
|
||||
if (!panel) return;
|
||||
panel.classList.remove("hidden");
|
||||
panel.removeAttribute("hidden");
|
||||
panel.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
|
||||
function resetAllAddonValuesPanels() {
|
||||
Object.keys(ADDON_VALUES_PANEL_ID).forEach(function (a) {
|
||||
hideAddonValuesPanel(a);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /helm/addons/compose-values для одной карточки: версии и поля Grafana читаются в момент нажатия кнопки.
|
||||
* @param {string} addon
|
||||
* @param {HTMLButtonElement | null} triggerBtn
|
||||
*/
|
||||
async function loadComposeValuesForAddon(addon, triggerBtn) {
|
||||
setBootstrapMsg("");
|
||||
var body = { addon: addon };
|
||||
if (addon === "ingress-nginx") {
|
||||
var si = document.getElementById("addons-ing-version-select");
|
||||
if (si && si.value.trim()) body.chart_version = si.value.trim();
|
||||
} else if (addon === "kube-prometheus-stack") {
|
||||
var pv = document.getElementById("addons-prom-version-select");
|
||||
var gu = document.getElementById("addons-grafana-user");
|
||||
var gp = document.getElementById("addons-grafana-pass");
|
||||
if (pv && pv.value.trim()) body.chart_version = pv.value.trim();
|
||||
if (gu) body.grafana_admin_user = gu.value.trim() || null;
|
||||
if (gp) body.grafana_admin_password = gp.value || null;
|
||||
} else if (addon === "metrics-server") {
|
||||
var ms = document.getElementById("addons-ms-version-select");
|
||||
if (ms && ms.value.trim()) body.chart_version = ms.value.trim();
|
||||
} else if (addon === "istio-kiali") {
|
||||
var isel = document.getElementById("addons-istio-version-select");
|
||||
var ksel = document.getElementById("addons-kiali-version-select");
|
||||
if (isel && isel.value.trim()) body.istio_chart_version = isel.value.trim();
|
||||
if (ksel && ksel.value.trim()) body.kiali_chart_version = ksel.value.trim();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
var prevText = triggerBtn ? triggerBtn.textContent : "";
|
||||
if (triggerBtn) {
|
||||
triggerBtn.disabled = true;
|
||||
triggerBtn.textContent = "Загрузка…";
|
||||
}
|
||||
try {
|
||||
var r = await api("/helm/addons/compose-values", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (addon === "istio-kiali") {
|
||||
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 (tk) tk.value = r.values_yaml || "";
|
||||
if (tb) tb.value = r.istio_base_values_yaml || "";
|
||||
if (tiod) tiod.value = r.istio_istiod_values_yaml || "";
|
||||
} else {
|
||||
var t = document.getElementById("addons-values-yaml-" + addon);
|
||||
if (t) t.value = r.values_yaml || "";
|
||||
}
|
||||
showAddonValuesPanel(addon);
|
||||
} catch (e) {
|
||||
setBootstrapMsg("Не удалось загрузить values (helm show values): " + e.message);
|
||||
} finally {
|
||||
if (triggerBtn) {
|
||||
triggerBtn.disabled = false;
|
||||
triggerBtn.textContent = prevText || "Загрузить values";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addonEndpointInstalled(endpoint) {
|
||||
@@ -440,7 +605,143 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* В селекте отметить версию из кластера и подписать опцию; при отсутствии в списке репозитория — добавить опцию.
|
||||
*/
|
||||
function mergeInstalledChartVersion(selectEl, version, isInstalled) {
|
||||
if (!selectEl || !isInstalled || !version) return;
|
||||
var v = String(version).trim();
|
||||
if (!v) return;
|
||||
var found = false;
|
||||
for (var i = 0; i < selectEl.options.length; i++) {
|
||||
var o = selectEl.options[i];
|
||||
if (o.value === v) {
|
||||
o.textContent = v + INSTALLED_VER_SUFFIX;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
var nu = document.createElement("option");
|
||||
nu.value = v;
|
||||
nu.textContent = v + INSTALLED_VER_SUFFIX;
|
||||
selectEl.appendChild(nu);
|
||||
}
|
||||
selectEl.value = v;
|
||||
}
|
||||
|
||||
/** Подставить в селекты версии из lastAddonsStatus (после fillVersionSelect). */
|
||||
function mergeChartVersionsFromStatus(st) {
|
||||
if (!st) return;
|
||||
mergeInstalledChartVersion(
|
||||
document.getElementById("addons-ing-version-select"),
|
||||
st.ingress_nginx_chart_version,
|
||||
st.ingress_nginx,
|
||||
);
|
||||
mergeInstalledChartVersion(
|
||||
document.getElementById("addons-prom-version-select"),
|
||||
st.kube_prometheus_stack_chart_version,
|
||||
st.kube_prometheus_stack,
|
||||
);
|
||||
mergeInstalledChartVersion(
|
||||
document.getElementById("addons-ms-version-select"),
|
||||
st.metrics_server_chart_version,
|
||||
st.metrics_server,
|
||||
);
|
||||
var istioVer = st.istiod_chart_version || st.istio_base_chart_version;
|
||||
mergeInstalledChartVersion(
|
||||
document.getElementById("addons-istio-version-select"),
|
||||
istioVer,
|
||||
st.istio_mesh_ready,
|
||||
);
|
||||
mergeInstalledChartVersion(
|
||||
document.getElementById("addons-kiali-version-select"),
|
||||
st.kiali_server_chart_version,
|
||||
st.istio_mesh_ready,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Заполнить textarea из GET …/addons/installed-values; снять панели с аддонов, которые не установлены.
|
||||
* @param {object} iv ответ API
|
||||
*/
|
||||
function applyInstalledValuesPayload(iv) {
|
||||
var st = lastAddonsStatus;
|
||||
if (!iv || !st) return;
|
||||
|
||||
if (!st.ingress_nginx) {
|
||||
hideAddonValuesPanel("ingress-nginx");
|
||||
} else if (iv.ingress_nginx && iv.ingress_nginx.values_yaml) {
|
||||
var ti = document.getElementById("addons-values-yaml-ingress-nginx");
|
||||
if (ti) ti.value = iv.ingress_nginx.values_yaml;
|
||||
showAddonValuesPanel("ingress-nginx");
|
||||
}
|
||||
|
||||
if (!st.kube_prometheus_stack) {
|
||||
hideAddonValuesPanel("kube-prometheus-stack");
|
||||
} else if (iv.kube_prometheus_stack && iv.kube_prometheus_stack.values_yaml) {
|
||||
var tp = document.getElementById("addons-values-yaml-kube-prometheus-stack");
|
||||
if (tp) tp.value = iv.kube_prometheus_stack.values_yaml;
|
||||
showAddonValuesPanel("kube-prometheus-stack");
|
||||
}
|
||||
|
||||
if (!st.metrics_server) {
|
||||
hideAddonValuesPanel("metrics-server");
|
||||
} else if (iv.metrics_server && iv.metrics_server.values_yaml) {
|
||||
var tm = document.getElementById("addons-values-yaml-metrics-server");
|
||||
if (tm) tm.value = iv.metrics_server.values_yaml;
|
||||
showAddonValuesPanel("metrics-server");
|
||||
}
|
||||
|
||||
if (!st.istio_mesh_ready) {
|
||||
hideAddonValuesPanel("istio-kiali");
|
||||
} else if (iv.istio_kiali) {
|
||||
var ik = iv.istio_kiali;
|
||||
if (ik.kiali_values_yaml) {
|
||||
var tk = document.getElementById("addons-values-yaml-istio-kiali");
|
||||
if (tk) tk.value = ik.kiali_values_yaml;
|
||||
}
|
||||
if (ik.istio_base_values_yaml) {
|
||||
var tb = document.getElementById("addons-values-yaml-istio-base");
|
||||
if (tb) tb.value = ik.istio_base_values_yaml;
|
||||
}
|
||||
if (ik.istiod_values_yaml) {
|
||||
var tiod = document.getElementById("addons-values-yaml-istio-istiod");
|
||||
if (tiod) tiod.value = ik.istiod_values_yaml;
|
||||
}
|
||||
if (ik.kiali_values_yaml || ik.istio_base_values_yaml || ik.istiod_values_yaml) {
|
||||
showAddonValuesPanel("istio-kiali");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Версии в селектах + YAML из кластера для установленных аддонов (после статуса и списка версий репозитория).
|
||||
* @param {string} clusterName
|
||||
*/
|
||||
async function maybeSyncInstalledAddonsFromCluster(clusterName) {
|
||||
if (!clusterName || !lastAddonsStatus) return;
|
||||
mergeChartVersionsFromStatus(lastAddonsStatus);
|
||||
var st = lastAddonsStatus;
|
||||
var any =
|
||||
st.ingress_nginx ||
|
||||
st.kube_prometheus_stack ||
|
||||
st.metrics_server ||
|
||||
st.istio_mesh_ready;
|
||||
if (!any) {
|
||||
resetAllAddonValuesPanels();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var iv = await api("/clusters/" + encodeURIComponent(clusterName) + "/addons/installed-values");
|
||||
applyInstalledValuesPayload(iv);
|
||||
} catch (e) {
|
||||
setBootstrapMsg("Не удалось загрузить values из кластера: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChartVersions() {
|
||||
setBootstrapMsg("Загрузка списка версий Helm-чартов (helm repo update, может занять до минуты)…");
|
||||
try {
|
||||
const d = await api("/helm/chart-versions");
|
||||
setBootstrapMsg("");
|
||||
@@ -449,7 +750,10 @@
|
||||
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();
|
||||
resetAllAddonValuesPanels();
|
||||
chartVersionsReady = true;
|
||||
var n = getClusterName();
|
||||
if (n) await maybeSyncInstalledAddonsFromCluster(n);
|
||||
} catch (e) {
|
||||
setBootstrapMsg("Список версий чартов недоступен: " + e.message);
|
||||
}
|
||||
@@ -501,6 +805,7 @@
|
||||
updateAddonCardBadges(null);
|
||||
updateAddonActionButtons();
|
||||
updateAddonsCardsVisibility();
|
||||
resetAllAddonValuesPanels();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -516,6 +821,13 @@
|
||||
updateAddonActionButtons();
|
||||
}
|
||||
updateAddonsCardsVisibility();
|
||||
if (chartVersionsReady && getClusterName()) {
|
||||
try {
|
||||
await maybeSyncInstalledAddonsFromCluster(getClusterName());
|
||||
} catch (e2) {
|
||||
/* уже отражено в maybeSync */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runInstall(endpoint, payload) {
|
||||
@@ -526,12 +838,12 @@
|
||||
}
|
||||
const isUpdate = addonEndpointInstalled(endpoint);
|
||||
const hum = ADDON_HUMAN_LABEL[endpoint] || endpoint;
|
||||
revealHelmProgressSection();
|
||||
revealHelmProgressSection(endpoint);
|
||||
setAddonFormsDisabled(true);
|
||||
if (helmHint) {
|
||||
helmHint.textContent = isUpdate
|
||||
? "Обновление релиза «" + hum + "» через helm в контейнере; ниже — полный вывод по завершении."
|
||||
: "Установка релиза «" + hum + "» через helm в контейнере; ниже — полный вывод по завершении.";
|
||||
? "Обновление релиза «" + hum + "» через helm в контейнере; журнал под этой карточкой аддона."
|
||||
: "Установка релиза «" + hum + "» через helm в контейнере; журнал под этой карточкой аддона.";
|
||||
}
|
||||
updateHelmProgressBar(8, isUpdate ? "Обновление Helm…" : "Установка Helm…");
|
||||
setHelmLogText(
|
||||
@@ -603,11 +915,11 @@
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
revealHelmProgressSection();
|
||||
revealHelmProgressSection(endpoint);
|
||||
setAddonFormsDisabled(true);
|
||||
if (helmHint) {
|
||||
helmHint.textContent =
|
||||
"Удаление релиза «" + hum + "» (helm uninstall); ниже — полный вывод по завершении.";
|
||||
"Удаление релиза «" + hum + "» (helm uninstall); журнал под этой карточкой аддона.";
|
||||
}
|
||||
updateHelmProgressBar(8, "Удаление Helm…");
|
||||
setHelmLogText(
|
||||
@@ -716,6 +1028,13 @@
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".btn-addon-load-values").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var ad = btn.getAttribute("data-addon") || "";
|
||||
if (ad) loadComposeValuesForAddon(ad, btn);
|
||||
});
|
||||
});
|
||||
|
||||
const refreshBtn = document.getElementById("addons-refresh-status");
|
||||
if (refreshBtn) refreshBtn.addEventListener("click", function () { refreshStatus(); });
|
||||
|
||||
@@ -725,29 +1044,64 @@
|
||||
|
||||
bindConfirmModalControls();
|
||||
|
||||
[
|
||||
"addons-ing-version-select",
|
||||
"addons-prom-version-select",
|
||||
"addons-ms-version-select",
|
||||
"addons-istio-version-select",
|
||||
"addons-kiali-version-select",
|
||||
].forEach(function (id) {
|
||||
/** Смена версии — сбрасываем предпросмотр YAML (устаревший контент). */
|
||||
var versionSelectToAddon = {
|
||||
"addons-ing-version-select": "ingress-nginx",
|
||||
"addons-prom-version-select": "kube-prometheus-stack",
|
||||
"addons-ms-version-select": "metrics-server",
|
||||
"addons-istio-version-select": "istio-kiali",
|
||||
"addons-kiali-version-select": "istio-kiali",
|
||||
};
|
||||
Object.keys(versionSelectToAddon).forEach(function (id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.addEventListener("change", function () { scheduleComposeValuesRefresh(); });
|
||||
if (el) {
|
||||
el.addEventListener("change", function () {
|
||||
hideAddonValuesPanel(versionSelectToAddon[id]);
|
||||
});
|
||||
}
|
||||
});
|
||||
/* Логин/пароль Grafana: если правили после загрузки YAML — скрываем блок, чтобы не путать с устаревшим предпросмотром. */
|
||||
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(); });
|
||||
function onGrafanaCredChange() {
|
||||
hideAddonValuesPanel("kube-prometheus-stack");
|
||||
}
|
||||
if (grafanaUserEl) grafanaUserEl.addEventListener("input", onGrafanaCredChange);
|
||||
if (grafanaPassEl) grafanaPassEl.addEventListener("input", onGrafanaCredChange);
|
||||
|
||||
loadClusters()
|
||||
.then(function () {
|
||||
return refreshStatus();
|
||||
})
|
||||
.then(function () {
|
||||
return loadChartVersions();
|
||||
})
|
||||
.finally(function () {
|
||||
/**
|
||||
* Оверлей убираем после списка кластеров и статуса — без ожидания GET /helm/chart-versions.
|
||||
* Кластеры/статус оборачиваем в таймаут: иначе при «висящем» fetch await не завершается и finally не вызывается.
|
||||
*/
|
||||
armAddonsOverlayFailsafe(45000);
|
||||
|
||||
(async function bootstrapAddonsPage() {
|
||||
try {
|
||||
await promiseWithTimeout(
|
||||
loadClusters(),
|
||||
30000,
|
||||
"Таймаут: список кластеров не получен за 30 с. Проверьте GET /api/v1/clusters.",
|
||||
);
|
||||
await promiseWithTimeout(
|
||||
refreshStatus(),
|
||||
30000,
|
||||
"Таймаут: статус аддонов не получен за 30 с.",
|
||||
);
|
||||
} catch (e) {
|
||||
var msg0 = e && e.message ? e.message : String(e);
|
||||
setBootstrapMsg(msg0);
|
||||
} finally {
|
||||
hideAddonsPageLoadingOverlay();
|
||||
});
|
||||
}
|
||||
try {
|
||||
await promiseWithTimeout(
|
||||
loadChartVersions(),
|
||||
180000,
|
||||
"Таймаут: версии Helm-чартов не получены за 3 мин. (helm repo update).",
|
||||
);
|
||||
} catch (e) {
|
||||
var msg1 = e && e.message ? e.message : String(e);
|
||||
setBootstrapMsg("Не удалось загрузить версии чартов: " + msg1);
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -1557,6 +1557,125 @@
|
||||
bindActionTooltipHosts(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Скрыть секцию «Установленные аддоны» (#cluster-detail-addons-section) и очистить сетку.
|
||||
*/
|
||||
function clearClusterDetailAddonsStrip() {
|
||||
var strip = document.getElementById("cluster-detail-addons-strip");
|
||||
var section = document.getElementById("cluster-detail-addons-section");
|
||||
if (strip) strip.replaceChildren();
|
||||
if (section) {
|
||||
section.classList.add("hidden");
|
||||
section.setAttribute("hidden", "");
|
||||
section.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Мини-карточки установленных Helm-аддонов в отдельной section.card под hero.
|
||||
* @param {string} clusterName
|
||||
* @param {object} st ответ GET /clusters/…/addons/status
|
||||
*/
|
||||
function renderClusterDetailAddonsStrip(clusterName, st) {
|
||||
var strip = document.getElementById("cluster-detail-addons-strip");
|
||||
var section = document.getElementById("cluster-detail-addons-section");
|
||||
if (!strip || !section || !st) {
|
||||
clearClusterDetailAddonsStrip();
|
||||
return;
|
||||
}
|
||||
var items = [];
|
||||
if (st.ingress_nginx) {
|
||||
items.push({
|
||||
title: "ingress-nginx",
|
||||
ver: st.ingress_nginx_chart_version,
|
||||
ns: "ingress-nginx",
|
||||
});
|
||||
}
|
||||
if (st.kube_prometheus_stack) {
|
||||
items.push({
|
||||
title: "kube-prometheus-stack",
|
||||
ver: st.kube_prometheus_stack_chart_version,
|
||||
ns: "monitoring",
|
||||
});
|
||||
}
|
||||
if (st.metrics_server) {
|
||||
items.push({
|
||||
title: "metrics-server",
|
||||
ver: st.metrics_server_chart_version,
|
||||
ns: "kube-system",
|
||||
});
|
||||
}
|
||||
if (st.istio_mesh_ready) {
|
||||
var iv = st.istiod_chart_version || st.istio_base_chart_version;
|
||||
var kv = st.kiali_server_chart_version;
|
||||
var parts = [];
|
||||
if (iv) parts.push("Istio " + iv);
|
||||
if (kv) parts.push("Kiali " + kv);
|
||||
items.push({
|
||||
title: "Istio + Kiali",
|
||||
verLine: parts.length ? parts.join(" · ") : null,
|
||||
ns: "istio-system",
|
||||
});
|
||||
}
|
||||
strip.replaceChildren();
|
||||
if (!items.length) {
|
||||
section.classList.add("hidden");
|
||||
section.setAttribute("hidden", "");
|
||||
section.setAttribute("aria-hidden", "true");
|
||||
return;
|
||||
}
|
||||
section.classList.remove("hidden");
|
||||
section.removeAttribute("hidden");
|
||||
section.setAttribute("aria-hidden", "false");
|
||||
items.forEach(function (it) {
|
||||
var a = document.createElement("a");
|
||||
a.className = "cluster-detail-addon-mini";
|
||||
a.href = "/cluster-addons";
|
||||
a.setAttribute("role", "listitem");
|
||||
a.setAttribute(
|
||||
"title",
|
||||
"Открыть страницу аддонов Helm; выберите кластер «" + clusterName + "» для настройки.",
|
||||
);
|
||||
var t = document.createElement("span");
|
||||
t.className = "cluster-detail-addon-mini__title";
|
||||
t.textContent = it.title;
|
||||
a.appendChild(t);
|
||||
var v = document.createElement("span");
|
||||
v.className = "cluster-detail-addon-mini__ver";
|
||||
if (it.verLine) {
|
||||
v.textContent = it.verLine;
|
||||
} else if (it.ver) {
|
||||
v.textContent = "Чарт " + it.ver;
|
||||
} else {
|
||||
v.textContent = "Версия чарта не определена";
|
||||
}
|
||||
a.appendChild(v);
|
||||
var n = document.createElement("span");
|
||||
n.className = "cluster-detail-addon-mini__ns";
|
||||
n.textContent = "ns: " + it.ns;
|
||||
a.appendChild(n);
|
||||
strip.appendChild(a);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузить статус аддонов и обновить карточку «Установленные аддоны» (при наличии kubeconfig на диске).
|
||||
* @param {string} clusterName
|
||||
* @param {boolean} hasLocalKubeconfig
|
||||
*/
|
||||
async function refreshClusterDetailAddonsStrip(clusterName, hasLocalKubeconfig) {
|
||||
if (!clusterName || !hasLocalKubeconfig) {
|
||||
clearClusterDetailAddonsStrip();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var st = await api("/clusters/" + encodeURIComponent(clusterName) + "/addons/status");
|
||||
renderClusterDetailAddonsStrip(clusterName, st);
|
||||
} catch (e) {
|
||||
clearClusterDetailAddonsStrip();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка ``GET /clusters/{name}/overview`` и заполнение блоков страницы кластера.
|
||||
* @param {string} name
|
||||
@@ -1578,6 +1697,7 @@
|
||||
has_local_kubeconfig: !!d.has_local_kubeconfig,
|
||||
has_provision_log: !!d.has_provision_log,
|
||||
});
|
||||
await refreshClusterDetailAddonsStrip(d.cluster_name, !!d.has_local_kubeconfig);
|
||||
const cliRaw =
|
||||
(cachedHealthPayload && cachedHealthPayload.container_cli) || "docker";
|
||||
renderAggregateDonutsToRoot(
|
||||
@@ -1611,6 +1731,7 @@
|
||||
fillClusterK8sTablesFromOverview(d);
|
||||
} catch (e) {
|
||||
const errText = e && e.message ? e.message : String(e);
|
||||
clearClusterDetailAddonsStrip();
|
||||
if (msg) {
|
||||
msg.textContent =
|
||||
"Кластер или API недоступны: " +
|
||||
|
||||
Reference in New Issue
Block a user