Панель: редактирование конфига кластера, reapply при старте, UI и таблица
- PUT/GET конфигурации кластера, страница редактирования и модалка после сохранения - После смены kind-config: флаг в meta и start_cluster_reapply (kind delete + create) - Старт/стоп: полноэкранный спиннер до завершения job; модалки и документация API - Таблица кластеров: колонка Имя 40% при table-layout fixed; чекбоксы без width 100% - Карточки ресурсов узлов на странице кластера: до 3 в ряд; прочие правки стилей и dashboard.js
This commit is contained in:
+93
-28
@@ -26,8 +26,8 @@
|
||||
var pollLockCreateForm = false;
|
||||
/** @type {string | null} */
|
||||
var currentPollJobId = null;
|
||||
/** Имя кластера в открытой модалке «Состояние» (для скачивания kubeconfig). */
|
||||
var currentModalClusterName = null;
|
||||
/** Полноэкранный спиннер старт/стоп (не используется при создании кластера из формы). */
|
||||
var clusterLifecycleJobOverlayActive = false;
|
||||
/** Кэш блоков cluster_resources по имени кластера (последний успешный GET /stats). */
|
||||
var cachedClusterResourcesByName = {};
|
||||
/** Успешный снимок GET /health — для ряда карточек среды + «Каталогов». */
|
||||
@@ -84,6 +84,29 @@
|
||||
document.body.classList.remove("dashboard-home-loading");
|
||||
}
|
||||
|
||||
/** Показать оверлей со спиннером на время старта/остановки узлов (до завершения задания в pollJob). */
|
||||
function showClusterLifecycleJobOverlay(labelText) {
|
||||
const el = document.getElementById("cluster-lifecycle-job-overlay");
|
||||
const lab = document.getElementById("cluster-lifecycle-job-label");
|
||||
if (!el) return;
|
||||
clusterLifecycleJobOverlayActive = true;
|
||||
if (lab) lab.textContent = labelText || "Выполняется операция…";
|
||||
el.classList.remove("hidden");
|
||||
el.setAttribute("aria-busy", "true");
|
||||
el.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("cluster-lifecycle-job-busy");
|
||||
}
|
||||
|
||||
function hideClusterLifecycleJobOverlay() {
|
||||
const el = document.getElementById("cluster-lifecycle-job-overlay");
|
||||
if (!el) return;
|
||||
clusterLifecycleJobOverlayActive = false;
|
||||
el.classList.add("hidden");
|
||||
el.setAttribute("aria-busy", "false");
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("cluster-lifecycle-job-busy");
|
||||
}
|
||||
|
||||
function formatApiError(data, fallback) {
|
||||
if (!data) return fallback;
|
||||
if (typeof data.detail === "string") return data.detail;
|
||||
@@ -213,6 +236,8 @@
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>',
|
||||
restart:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
|
||||
edit:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',
|
||||
};
|
||||
|
||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||||
@@ -1249,7 +1274,7 @@
|
||||
host.appendChild(
|
||||
iconActionButton(
|
||||
ICONS.state,
|
||||
"Состояние: узлы и поды (kubectl get nodes / pods)",
|
||||
"Состояние: узлы и поды",
|
||||
"",
|
||||
function () {
|
||||
openWorkloadsModal(name);
|
||||
@@ -1257,6 +1282,17 @@
|
||||
false,
|
||||
),
|
||||
);
|
||||
host.appendChild(
|
||||
iconActionButton(
|
||||
ICONS.edit,
|
||||
"Редактировать kind-config.yaml и meta.json",
|
||||
"icon-btn--secondary",
|
||||
function () {
|
||||
window.location.href = "/cluster/" + encodeURIComponent(name) + "/edit";
|
||||
},
|
||||
false,
|
||||
),
|
||||
);
|
||||
if (c.registered_in_kind && c.kind_nodes_running) {
|
||||
host.appendChild(
|
||||
iconActionButton(
|
||||
@@ -1355,15 +1391,26 @@
|
||||
);
|
||||
appendClusterNodeCardsRow(document.getElementById("cluster-page-node-cards"), d.cluster_resources);
|
||||
const kubeErr = d.kubeconfig_error || null;
|
||||
const clusterStoppedInKind = !!d.registered_in_kind && !d.kind_nodes_running;
|
||||
if (hint) {
|
||||
if (kubeErr) {
|
||||
hint.textContent = kubeErr;
|
||||
hint.classList.remove("hidden");
|
||||
if (clusterStoppedInKind) {
|
||||
hint.classList.add("cluster-kube-hint--info");
|
||||
} else {
|
||||
hint.classList.remove("cluster-kube-hint--info");
|
||||
}
|
||||
} else {
|
||||
hint.textContent = "";
|
||||
hint.classList.add("hidden");
|
||||
hint.classList.remove("cluster-kube-hint--info");
|
||||
}
|
||||
}
|
||||
if (msg && clusterStoppedInKind) {
|
||||
msg.textContent =
|
||||
"Кластер остановлен. Данные Kubernetes появятся после запуска узлов (кнопка «Старт» в панели).";
|
||||
}
|
||||
fillClusterK8sTablesFromOverview(d);
|
||||
} catch (e) {
|
||||
const errText = e && e.message ? e.message : String(e);
|
||||
@@ -1635,24 +1682,24 @@
|
||||
"<span class=\"muted\" title=\"Нет в kind — только локальный каталог или кластер удалён из kind\">—</span>";
|
||||
}
|
||||
tr.innerHTML =
|
||||
"<td><a class=\"cluster-name-link\" href=\"/cluster/" +
|
||||
"<td class=\"col-cluster-name\"><a class=\"cluster-name-link\" href=\"/cluster/" +
|
||||
encodeURIComponent(c.name) +
|
||||
"\" title=\"Страница кластера: ресурсы и kubectl\"><code class=\"cluster-name\">" +
|
||||
nameEsc +
|
||||
"</code></a></td>" +
|
||||
"<td>" +
|
||||
"<td class=\"col-kind\">" +
|
||||
(c.registered_in_kind ? "<span class=\"badge badge-ok\">да</span>" : "<span class=\"badge\">нет</span>") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
"<td class=\"col-kubeconfig\">" +
|
||||
(c.has_local_kubeconfig ? "<span class=\"badge badge-ok\">да</span>" : "<span class=\"badge\">нет</span>") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
"<td class=\"col-version\">" +
|
||||
escapeHtml(String(ver)) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
"<td class=\"col-workers\">" +
|
||||
escapeHtml(String(wn)) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
"<td class=\"col-state\">" +
|
||||
stateCell +
|
||||
"</td>" +
|
||||
"<td class=\"actions\"><span class=\"actions-toolbar\"></span></td>";
|
||||
@@ -1725,6 +1772,7 @@
|
||||
const st = escapeHtml(j.status || "");
|
||||
var kindTag = "";
|
||||
if (j.kind === "start_cluster") kindTag = "[старт] ";
|
||||
else if (j.kind === "start_cluster_reapply") kindTag = "[старт после правки] ";
|
||||
else if (j.kind === "stop_containers") kindTag = "[стоп] ";
|
||||
else if (j.kind === "start_containers") kindTag = "[запуск узлов] ";
|
||||
var cellMsg = kindTag + (j.message || "").slice(0, 140);
|
||||
@@ -1774,15 +1822,12 @@
|
||||
const nodes = document.getElementById("modal-nodes");
|
||||
const pods = document.getElementById("modal-pods");
|
||||
const spin = document.getElementById("modal-spinner");
|
||||
const modalDlWrap = document.getElementById("modal-dl-wrap");
|
||||
if (!overlay) return;
|
||||
hideActionTooltip();
|
||||
currentModalClusterName = name;
|
||||
document.getElementById("modal-title").textContent = "Кластер «" + name + "»";
|
||||
sub.textContent = "";
|
||||
nodes.textContent = "";
|
||||
pods.textContent = "";
|
||||
if (modalDlWrap) modalDlWrap.classList.add("hidden");
|
||||
if (spin) spin.classList.remove("hidden");
|
||||
overlay.classList.remove("hidden");
|
||||
document.body.classList.add("modal-open");
|
||||
@@ -1792,10 +1837,9 @@
|
||||
sub.textContent = w.error;
|
||||
return;
|
||||
}
|
||||
sub.textContent = "kubectl: узлы rc=" + w.nodes_rc + ", поды rc=" + w.pods_rc;
|
||||
sub.textContent = "Узлы: код " + w.nodes_rc + " · Поды: код " + w.pods_rc;
|
||||
nodes.textContent = w.nodes_output || "(пусто)";
|
||||
pods.textContent = w.pods_output || "(пусто)";
|
||||
if (modalDlWrap) modalDlWrap.classList.remove("hidden");
|
||||
} catch (e) {
|
||||
sub.textContent = "Ошибка: " + e.message;
|
||||
} finally {
|
||||
@@ -1878,9 +1922,6 @@
|
||||
function closeModal() {
|
||||
hideActionTooltip();
|
||||
const overlay = document.getElementById("modal-overlay");
|
||||
const modalDlWrap = document.getElementById("modal-dl-wrap");
|
||||
if (modalDlWrap) modalDlWrap.classList.add("hidden");
|
||||
currentModalClusterName = null;
|
||||
if (overlay) overlay.classList.add("hidden");
|
||||
document.body.classList.remove("modal-open");
|
||||
}
|
||||
@@ -1908,7 +1949,10 @@
|
||||
}
|
||||
if (r.status === 202 && data.job_id) {
|
||||
setProgressHint(name, "stop");
|
||||
pollJob(data.job_id, { lockCreateForm: false });
|
||||
pollJob(data.job_id, {
|
||||
lockCreateForm: false,
|
||||
lifecycleOverlayLabel: "Останавливаем узлы кластера «" + name + "»…",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!r.ok) {
|
||||
@@ -1941,8 +1985,15 @@
|
||||
if (r.status === 202 && data.job_id) {
|
||||
var sm = data.mode || "kind_config";
|
||||
if (sm === "containers") setProgressHint(name, "start_containers");
|
||||
else if (sm === "kind_config_reapply") setProgressHint(name, "start_reapply");
|
||||
else setProgressHint(name, "start_config");
|
||||
pollJob(data.job_id, { lockCreateForm: false });
|
||||
var startLab =
|
||||
sm === "containers"
|
||||
? "Запуск узлов кластера «" + name + "»…"
|
||||
: sm === "kind_config_reapply"
|
||||
? "Пересоздание кластера «" + name + "» в kind…"
|
||||
: "Подъём кластера «" + name + "» по конфигурации…";
|
||||
pollJob(data.job_id, { lockCreateForm: false, lifecycleOverlayLabel: startLab });
|
||||
return;
|
||||
}
|
||||
if (!r.ok) {
|
||||
@@ -2015,7 +2066,7 @@
|
||||
/**
|
||||
* Подсказка над прогресс-баром (без технических имён команд).
|
||||
* @param {string | null} clusterName
|
||||
* @param {"create"|"start_config"|"start_containers"|"stop"} [mode]
|
||||
* @param {"create"|"start_config"|"start_reapply"|"start_containers"|"stop"} [mode]
|
||||
*/
|
||||
function setProgressHint(clusterName, mode) {
|
||||
const hint = document.getElementById("create-progress-hint");
|
||||
@@ -2026,6 +2077,11 @@
|
||||
hint.textContent = "Останавливаем кластер " + named + ". Ход операции — в журнале ниже.";
|
||||
} else if (mode === "start_containers") {
|
||||
hint.textContent = "Запускаем узлы кластера " + named + ". Ход операции — в журнале ниже.";
|
||||
} else if (mode === "start_reapply") {
|
||||
hint.textContent =
|
||||
"Пересоздаём кластер " +
|
||||
named +
|
||||
" в kind по обновлённому kind-config.yaml (сначала удаление записи, затем создание). Журнал ниже.";
|
||||
} else if (mode === "start_config") {
|
||||
hint.textContent = "Поднимаем кластер " + named + " по сохранённой конфигурации. Журнал ниже.";
|
||||
} else {
|
||||
@@ -2052,6 +2108,7 @@
|
||||
}
|
||||
|
||||
function stopPollJob() {
|
||||
hideClusterLifecycleJobOverlay();
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
@@ -2077,7 +2134,8 @@
|
||||
|
||||
/**
|
||||
* @param {string} jobId
|
||||
* @param {{ lockCreateForm?: boolean }} [opts]
|
||||
* @param {{ lockCreateForm?: boolean, lifecycleOverlayLabel?: string }} [opts]
|
||||
* ``lifecycleOverlayLabel`` — полноэкранный спиннер (старт/стоп с главной или страницы кластера).
|
||||
*/
|
||||
function pollJob(jobId, opts) {
|
||||
opts = opts || {};
|
||||
@@ -2096,6 +2154,9 @@
|
||||
if (pollLockCreateForm) {
|
||||
setCreateFormDisabled(true);
|
||||
}
|
||||
if (opts.lifecycleOverlayLabel) {
|
||||
showClusterLifecycleJobOverlay(opts.lifecycleOverlayLabel);
|
||||
}
|
||||
showCreateProgress(true);
|
||||
updateCreateProgressFromJob({ progress_percent: 0, progress_stage: "В очереди…", status: "queued" });
|
||||
|
||||
@@ -2105,6 +2166,14 @@
|
||||
const preEl = document.getElementById("job-json");
|
||||
if (preEl) preEl.textContent = JSON.stringify(j, null, 2);
|
||||
updateCreateProgressFromJob(j);
|
||||
if (clusterLifecycleJobOverlayActive) {
|
||||
const lab = document.getElementById("cluster-lifecycle-job-label");
|
||||
if (lab) {
|
||||
var st = j.progress_stage;
|
||||
if (!st && (j.status === "queued" || j.status === "running")) st = "В очереди…";
|
||||
if (st) lab.textContent = st;
|
||||
}
|
||||
}
|
||||
if (logEl) {
|
||||
if (j.progress_log && j.progress_log.length) {
|
||||
logEl.removeAttribute("data-placeholder");
|
||||
@@ -2123,6 +2192,8 @@
|
||||
if (msg) {
|
||||
if (j.status === "success") {
|
||||
if (j.kind === "start_cluster") msg.textContent = "Кластер поднят по сохранённому конфигу.";
|
||||
else if (j.kind === "start_cluster_reapply")
|
||||
msg.textContent = "Кластер пересоздан в kind по обновлённому конфигу.";
|
||||
else if (j.kind === "stop_containers") msg.textContent = "Узлы кластера остановлены.";
|
||||
else if (j.kind === "start_containers") msg.textContent = "Узлы кластера запущены.";
|
||||
else msg.textContent = "Кластер создан.";
|
||||
@@ -2131,6 +2202,7 @@
|
||||
}
|
||||
if (j.status === "success") {
|
||||
if (j.kind === "start_cluster") showToast("Кластер запущен", false);
|
||||
else if (j.kind === "start_cluster_reapply") showToast("Кластер пересоздан по новому конфигу", false);
|
||||
else if (j.kind === "stop_containers") showToast("Узлы остановлены", false);
|
||||
else if (j.kind === "start_containers") showToast("Узлы запущены", false);
|
||||
else showToast("Кластер создан", false);
|
||||
@@ -2255,13 +2327,6 @@
|
||||
const mClose = document.getElementById("modal-close");
|
||||
if (mClose) mClose.addEventListener("click", closeModal);
|
||||
|
||||
const modalBtnKube = document.getElementById("modal-btn-kubeconfig");
|
||||
if (modalBtnKube) {
|
||||
modalBtnKube.addEventListener("click", function () {
|
||||
if (currentModalClusterName) downloadKubeconfig(currentModalClusterName);
|
||||
});
|
||||
}
|
||||
|
||||
const overlay = document.getElementById("modal-overlay");
|
||||
if (overlay) {
|
||||
overlay.addEventListener("click", function (ev) {
|
||||
|
||||
Reference in New Issue
Block a user