/** * Страница 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(/\/$/, ""); /** 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} */ 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] */ 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 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 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; } /** Скрыть полноэкранный спиннер первой загрузки страницы (как на панели и странице кластера). */ 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"); } /** * Текстовое сообщение в полосе статуса (нет кластера / ошибка). * @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); } /** Максимальная длина поля 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} 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 | 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"); } } }); } /** * Заполнить выпадающий список версиями чарта; первая опция — «последняя». * @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"); 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) { setBootstrapMsg("Список версий чартов недоступен: " + e.message); } } async function loadClusters() { 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) { 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); setBootstrapMsg("Ошибка GET /clusters: " + e.message); } } async function refreshStatus() { const n = getClusterName(); if (!statusBar) { updateAddonsCardsVisibility(); return; } if (!n) { lastAddonsStatus = null; clearAddonToolbarStatusBar(); updateAddonCardBadges(null); updateAddonActionButtons(); updateAddonsCardsVisibility(); return; } try { const st = await api("/clusters/" + encodeURIComponent(n) + "/addons/status"); lastAddonsStatus = st; clearAddonToolbarStatusBar(); updateAddonCardBadges(st); updateAddonActionButtons(); } catch (e) { lastAddonsStatus = null; renderAddonStatusMessage("Статус недоступен: " + e.message); updateAddonCardBadges(null); updateAddonActionButtons(); } updateAddonsCardsVisibility(); } async function runInstall(endpoint, payload) { const n = getClusterName(); if (!n) { alert("Выберите кластер."); return; } 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) : "{}", }); 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) { var failLines = [ "Кластер: «" + n + "»", (isUpdate ? "POST (обновление)" : "POST (установка)") + " /addons/" + endpoint, "", "Ошибка: " + e.message, ]; setHelmLogText(failLines.join("\n")); updateHelmProgressBar(100, "Ошибка"); if (helmHint) { helmHint.textContent = "Запрос не удался. При ответе сервера об ошибке Helm в журнале кластера могла появиться запись со статусом failed."; } } finally { setAddonFormsDisabled(false); } } async function runDelete(endpoint) { const n = getClusterName(); if (!n) { alert("Выберите кластер."); return; } 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" }, }); 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) { var failDel = [ "Кластер: «" + n + "»", "DELETE /addons/" + endpoint, "", "Ошибка: " + e.message, ]; setHelmLogText(failDel.join("\n")); updateHelmProgressBar(100, "Ошибка"); if (helmHint) { helmHint.textContent = "Запрос не удался. При ошибке Helm на сервере в журнале кластера могла появиться запись со статусом failed."; } } finally { setAddonFormsDisabled(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(); const payload = {}; if (v) payload.chart_version = v; if (!attachAddonYamlToPayload(ep, payload)) return; runInstall(ep, payload); 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(); if (!attachAddonYamlToPayload(ep, payload)) return; 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(); if (!attachAddonYamlToPayload(ep, payload)) return; runInstall(ep, 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(); if (!attachAddonYamlToPayload(ep, payload)) return; 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(); }); } 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(); }) .then(function () { return loadChartVersions(); }) .finally(function () { hideAddonsPageLoadingOverlay(); }); })();