Панель: редактирование конфига кластера, 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:
@@ -0,0 +1,811 @@
|
||||
/**
|
||||
* Страница редактирования кластера: GET/PUT /api/v1/clusters/{name}/config.
|
||||
* Расширенный режим — форма по документации kind (networking, nodes, extraPortMappings, kubeadmConfigPatches).
|
||||
*
|
||||
* Автор: Сергей Антропов
|
||||
* Сайт: https://devops.org.ru
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const body = document.body;
|
||||
const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
|
||||
const clusterName = (body.getAttribute("data-cluster-name") || "").trim();
|
||||
if (!clusterName) return;
|
||||
|
||||
/** @type {string} */
|
||||
let initialTag = "";
|
||||
/** @type {number | null} */
|
||||
let initialWorkers = null;
|
||||
/** @type {string} */
|
||||
let initialDescription = "";
|
||||
/** @type {string | null} */
|
||||
let loadedYamlSnapshot = null;
|
||||
/** @type {string} */
|
||||
let initialAdvancedSnapshot = "";
|
||||
|
||||
/**
|
||||
* @typedef {object} AdvancedFormState
|
||||
* @property {string} apiVersion
|
||||
* @property {string} image
|
||||
* @property {number} controlPlanes
|
||||
* @property {number} workers
|
||||
* @property {string} ipFamily
|
||||
* @property {string} apiServerAddress
|
||||
* @property {string} apiServerPort
|
||||
* @property {string} podSubnet
|
||||
* @property {string} serviceSubnet
|
||||
* @property {string} kubeProxyMode
|
||||
* @property {boolean} disableDefaultCNI
|
||||
* @property {boolean} portEnabled
|
||||
* @property {string} hostPort
|
||||
* @property {number} containerPort
|
||||
* @property {string} portListenAddress
|
||||
* @property {string} portProtocol
|
||||
* @property {string} kubeadmPatch
|
||||
*/
|
||||
|
||||
function formatApiError(data, fallback) {
|
||||
if (!data) return fallback;
|
||||
if (typeof data.detail === "string") return data.detail;
|
||||
if (Array.isArray(data.detail)) {
|
||||
return data.detail
|
||||
.map(function (x) {
|
||||
return (x.msg || x) + (x.loc ? " (" + x.loc.join(".") + ")" : "");
|
||||
})
|
||||
.join("; ");
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = formatApiError(data, text || r.statusText);
|
||||
const err = new Error(msg);
|
||||
err.status = r.status;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Собрать содержимое модалки: как на диске сохранилось и как это доходит до kind.
|
||||
* @param {boolean} updatedYaml
|
||||
* @param {boolean} registeredInKind
|
||||
* @returns {DocumentFragment}
|
||||
*/
|
||||
function buildSaveInfoModalContent(updatedYaml, registeredInKind) {
|
||||
const frag = document.createDocumentFragment();
|
||||
if (!updatedYaml) {
|
||||
const p = document.createElement("p");
|
||||
p.textContent =
|
||||
"Сохранены поля без изменения kind-config.yaml (например, только описание в meta.json). " +
|
||||
"Топология кластера в kind и уже запущенные узлы от этого не меняются.";
|
||||
frag.appendChild(p);
|
||||
return frag;
|
||||
}
|
||||
const lead = document.createElement("p");
|
||||
lead.className = "cluster-edit-save-modal-lead";
|
||||
lead.textContent = "Файл kind-config.yaml и связанные поля meta.json записаны на диск.";
|
||||
frag.appendChild(lead);
|
||||
|
||||
if (registeredInKind) {
|
||||
const pIntro = document.createElement("p");
|
||||
pIntro.textContent = "Кластер уже зарегистрирован в kind. Новый конфиг применяется так:";
|
||||
frag.appendChild(pIntro);
|
||||
const ul = document.createElement("ul");
|
||||
const lines = [
|
||||
"До нажатия «Старт» продолжают работать текущие узлы (старая топология и образы).",
|
||||
"При «Старт» на панели или на странице кластера выполняется пересоздание: удаление записи в kind без удаления каталога clusters/" +
|
||||
clusterName +
|
||||
"/, затем создание кластера по обновлённому YAML.",
|
||||
"«Стоп» перед «Старт» не обязателен — можно нажать «Старт» сразу после сохранения.",
|
||||
];
|
||||
lines.forEach(function (line) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = line;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
frag.appendChild(ul);
|
||||
const note = document.createElement("p");
|
||||
note.style.marginTop = "0.65rem";
|
||||
note.style.fontSize = "0.88rem";
|
||||
note.textContent =
|
||||
"Состояние Kubernetes внутри кластера при пересоздании начинается заново; kubeconfig в каталоге кластера обновится после успешного «Старт».";
|
||||
frag.appendChild(note);
|
||||
} else {
|
||||
const p2 = document.createElement("p");
|
||||
p2.textContent =
|
||||
"Кластер сейчас не в списке kind. При «Старт» он будет поднят по сохранённому kind-config.yaml (полный цикл создания, как при восстановлении из файла).";
|
||||
frag.appendChild(p2);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
|
||||
function openClusterEditSaveModal(updatedYaml, registeredInKind) {
|
||||
const overlay = document.getElementById("cluster-edit-save-modal-overlay");
|
||||
const titleEl = document.getElementById("cluster-edit-save-modal-title");
|
||||
const bodyEl = document.getElementById("cluster-edit-save-modal-body");
|
||||
if (!overlay || !titleEl || !bodyEl) return;
|
||||
|
||||
titleEl.textContent = updatedYaml ? "Конфигурация сохранена" : "Сохранено";
|
||||
bodyEl.replaceChildren();
|
||||
bodyEl.appendChild(buildSaveInfoModalContent(updatedYaml, registeredInKind));
|
||||
|
||||
overlay.classList.remove("hidden");
|
||||
overlay.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("modal-open");
|
||||
|
||||
const okBtn = document.getElementById("cluster-edit-save-modal-ok");
|
||||
if (okBtn) okBtn.focus();
|
||||
}
|
||||
|
||||
function closeClusterEditSaveModal() {
|
||||
const overlay = document.getElementById("cluster-edit-save-modal-overlay");
|
||||
if (!overlay) return;
|
||||
overlay.classList.add("hidden");
|
||||
overlay.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("modal-open");
|
||||
}
|
||||
|
||||
/** Закрытие модалки после сохранения и переход на страницу кластера (только кнопка «Понятно»). */
|
||||
function onSaveInfoModalOk() {
|
||||
closeClusterEditSaveModal();
|
||||
window.location.href = "/cluster/" + encodeURIComponent(clusterName);
|
||||
}
|
||||
|
||||
function wireSaveInfoModal() {
|
||||
const overlay = document.getElementById("cluster-edit-save-modal-overlay");
|
||||
const ok = document.getElementById("cluster-edit-save-modal-ok");
|
||||
if (!overlay || !ok) return;
|
||||
ok.addEventListener("click", onSaveInfoModalOk);
|
||||
overlay.addEventListener("click", function (ev) {
|
||||
if (ev.target === overlay) closeClusterEditSaveModal();
|
||||
});
|
||||
document.addEventListener("keydown", function (ev) {
|
||||
if (ev.key === "Escape" && !overlay.classList.contains("hidden")) {
|
||||
closeClusterEditSaveModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function wireVersionSelect(selId, inputId) {
|
||||
const sel = document.getElementById(selId);
|
||||
const verInput = document.getElementById(inputId);
|
||||
if (!sel || !verInput) return;
|
||||
sel.onchange = function () {
|
||||
if (!sel.value) return;
|
||||
if (inputId === "fld-adv-image") {
|
||||
verInput.value =
|
||||
sel.value === "latest" ? "kindest/node:latest" : "kindest/node:" + sel.value.replace(/^v/, "");
|
||||
} else {
|
||||
verInput.value = sel.value === "latest" ? "latest" : sel.value.replace(/^v/, "");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function loadVersions() {
|
||||
const ids = [
|
||||
["version-select-edit", "kubernetes_version_edit"],
|
||||
["version-select-adv", "fld-adv-image"],
|
||||
];
|
||||
ids.forEach(function (pair) {
|
||||
const sel = document.getElementById(pair[0]);
|
||||
if (sel) sel.innerHTML = "<option value=\"\">— загрузка —</option>";
|
||||
});
|
||||
try {
|
||||
const data = await api("/versions");
|
||||
ids.forEach(function (pair) {
|
||||
const sel = document.getElementById(pair[0]);
|
||||
if (!sel) return;
|
||||
sel.innerHTML = "";
|
||||
if (!data.tags || !data.tags.length) {
|
||||
sel.innerHTML = "<option value=\"\">(список пуст — введите вручную)</option>";
|
||||
return;
|
||||
}
|
||||
const opt0 = document.createElement("option");
|
||||
opt0.value = "";
|
||||
opt0.textContent = "— выберите тег —";
|
||||
sel.appendChild(opt0);
|
||||
data.tags.forEach(function (t) {
|
||||
const o = document.createElement("option");
|
||||
o.value = t;
|
||||
o.textContent = t === "latest" ? "latest (kindest/node)" : t;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
});
|
||||
wireVersionSelect("version-select-edit", "kubernetes_version_edit");
|
||||
wireVersionSelect("version-select-adv", "fld-adv-image");
|
||||
} catch (e) {
|
||||
ids.forEach(function (pair) {
|
||||
const sel = document.getElementById(pair[0]);
|
||||
if (sel) sel.innerHTML = "<option value=\"\">(ошибка загрузки тегов)</option>";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Разбор секции networking: (документация kind — cluster-wide networking).
|
||||
* @param {string} text
|
||||
* @returns {Pick<AdvancedFormState, 'ipFamily'|'apiServerAddress'|'apiServerPort'|'podSubnet'|'serviceSubnet'|'kubeProxyMode'|'disableDefaultCNI'>}
|
||||
*/
|
||||
function parseNetworkingSection(text) {
|
||||
const out = {
|
||||
ipFamily: "",
|
||||
apiServerAddress: "",
|
||||
apiServerPort: "",
|
||||
podSubnet: "",
|
||||
serviceSubnet: "",
|
||||
kubeProxyMode: "",
|
||||
disableDefaultCNI: false,
|
||||
};
|
||||
const lines = text.split("\n");
|
||||
let i = 0;
|
||||
while (i < lines.length) {
|
||||
if (/^\s*networking:\s*$/.test(lines[i])) {
|
||||
i++;
|
||||
while (i < lines.length) {
|
||||
const L = lines[i];
|
||||
const t = L.trim();
|
||||
if (!t) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (/^[a-zA-Z]/.test(L) && !/^\s/.test(L)) break;
|
||||
const kv = L.match(/^\s{2}([a-zA-Z0-9]+):\s*(.*)$/);
|
||||
if (kv) {
|
||||
const k = kv[1];
|
||||
let v = kv[2].trim();
|
||||
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
||||
v = v.slice(1, -1);
|
||||
}
|
||||
if (k === "ipFamily") out.ipFamily = v;
|
||||
else if (k === "apiServerAddress") out.apiServerAddress = v;
|
||||
else if (k === "apiServerPort") out.apiServerPort = v;
|
||||
else if (k === "podSubnet") out.podSubnet = v;
|
||||
else if (k === "serviceSubnet") out.serviceSubnet = v;
|
||||
else if (k === "kubeProxyMode") out.kubeProxyMode = v.replace(/^["']|["']$/g, "");
|
||||
else if (k === "disableDefaultCNI") out.disableDefaultCNI = v === "true";
|
||||
}
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AdvancedFormState} f
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildNetworkingYaml(f) {
|
||||
const rows = [];
|
||||
if (f.ipFamily && f.ipFamily !== "ipv4") rows.push(" ipFamily: " + f.ipFamily);
|
||||
const addr = (f.apiServerAddress || "").trim();
|
||||
if (addr) rows.push(' apiServerAddress: "' + addr.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"');
|
||||
const ap = String(f.apiServerPort || "").trim();
|
||||
if (ap) {
|
||||
const pn = parseInt(ap, 10);
|
||||
if (pn >= 1 && pn <= 65535) rows.push(" apiServerPort: " + pn);
|
||||
}
|
||||
const ps = (f.podSubnet || "").trim();
|
||||
if (ps) rows.push(' podSubnet: "' + ps.replace(/"/g, '\\"') + '"');
|
||||
const ss = (f.serviceSubnet || "").trim();
|
||||
if (ss) rows.push(' serviceSubnet: "' + ss.replace(/"/g, '\\"') + '"');
|
||||
const kpm = (f.kubeProxyMode || "").trim();
|
||||
if (kpm) rows.push(' kubeProxyMode: "' + kpm.replace(/"/g, '\\"') + '"');
|
||||
if (f.disableDefaultCNI) rows.push(" disableDefaultCNI: true");
|
||||
if (!rows.length) return "";
|
||||
return "networking:\n" + rows.join("\n") + "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {AdvancedFormState}
|
||||
*/
|
||||
function parseKindYamlToAdvancedForm(text) {
|
||||
/** @type {AdvancedFormState} */
|
||||
const out = {
|
||||
apiVersion: "kind.x-k8s.io/v1alpha4",
|
||||
image: "",
|
||||
controlPlanes: 1,
|
||||
workers: 0,
|
||||
ipFamily: "",
|
||||
apiServerAddress: "",
|
||||
apiServerPort: "",
|
||||
podSubnet: "",
|
||||
serviceSubnet: "",
|
||||
kubeProxyMode: "",
|
||||
disableDefaultCNI: false,
|
||||
portEnabled: false,
|
||||
hostPort: "",
|
||||
containerPort: 80,
|
||||
portListenAddress: "",
|
||||
portProtocol: "TCP",
|
||||
kubeadmPatch: "",
|
||||
};
|
||||
if (!text || !text.trim()) return out;
|
||||
const av = text.match(/^\s*apiVersion:\s*(\S+)/m);
|
||||
if (av) out.apiVersion = av[1].trim().replace(/['"]/g, "");
|
||||
const net = parseNetworkingSection(text);
|
||||
Object.assign(out, net);
|
||||
|
||||
const lines = text.split("\n");
|
||||
let inNodes = false;
|
||||
let cp = 0;
|
||||
let wk = 0;
|
||||
/** @type {string | null} */
|
||||
let pendingRole = null;
|
||||
const images = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const t = line.trim();
|
||||
if (t === "nodes:" || t.startsWith("nodes:")) {
|
||||
inNodes = true;
|
||||
pendingRole = null;
|
||||
continue;
|
||||
}
|
||||
if (!inNodes) continue;
|
||||
if (t && !line.startsWith(" ") && !line.startsWith("\t")) break;
|
||||
const roleM = line.match(/^\s*-\s*role:\s*(\S+)/);
|
||||
if (roleM) {
|
||||
pendingRole = roleM[1];
|
||||
if (pendingRole === "control-plane") cp++;
|
||||
else if (pendingRole === "worker") wk++;
|
||||
continue;
|
||||
}
|
||||
const imgM = line.match(/^\s*image:\s*(.+)$/);
|
||||
if (imgM && pendingRole) {
|
||||
images.push(imgM[1].trim().replace(/['"]/g, ""));
|
||||
}
|
||||
}
|
||||
if (cp > 0) out.controlPlanes = cp;
|
||||
out.workers = wk;
|
||||
const uniq = [];
|
||||
images.forEach(function (im) {
|
||||
if (uniq.indexOf(im) < 0) uniq.push(im);
|
||||
});
|
||||
out.image = uniq[0] || "";
|
||||
|
||||
const portSplit = text.split("extraPortMappings:");
|
||||
if (portSplit.length > 1) {
|
||||
const portBlock = portSplit[1];
|
||||
const hh = portBlock.match(/hostPort:\s*(\d+)/);
|
||||
const cc = portBlock.match(/containerPort:\s*(\d+)/);
|
||||
const la =
|
||||
portBlock.match(/listenAddress:\s*"([^"]+)"/) ||
|
||||
portBlock.match(/listenAddress:\s*'([^']+)'/);
|
||||
const pr = portBlock.match(/protocol:\s*(\w+)/);
|
||||
if (hh) {
|
||||
out.portEnabled = true;
|
||||
out.hostPort = hh[1];
|
||||
if (cc) out.containerPort = parseInt(cc[1], 10) || 80;
|
||||
if (la) out.portListenAddress = la[1];
|
||||
if (pr && /UDP|SCTP|TCP/i.test(pr[1])) out.portProtocol = pr[1].toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
const kubStart = text.indexOf("kubeadmConfigPatches:");
|
||||
if (kubStart >= 0) {
|
||||
const sub = text.slice(kubStart);
|
||||
const pipeM = sub.match(/kubeadmConfigPatches:\s*\n\s*-\s*\|\s*\n([\s\S]+)$/);
|
||||
if (pipeM) {
|
||||
const raw = pipeM[1];
|
||||
const linesPatch = raw.split("\n");
|
||||
const outLines = [];
|
||||
for (let j = 0; j < linesPatch.length; j++) {
|
||||
const ln = linesPatch[j];
|
||||
if (/^\s*-\s*role:\s*/.test(ln)) break;
|
||||
outLines.push(ln.replace(/^ /, ""));
|
||||
}
|
||||
out.kubeadmPatch = outLines.join("\n").replace(/\n+$/, "");
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ensureApiVersionOption(apiVersion) {
|
||||
const sel = document.getElementById("fld-adv-api-version");
|
||||
if (!sel || !apiVersion) return;
|
||||
let found = false;
|
||||
sel.querySelectorAll("option").forEach(function (o) {
|
||||
if (o.value === apiVersion) found = true;
|
||||
});
|
||||
if (!found) {
|
||||
const o = document.createElement("option");
|
||||
o.value = apiVersion;
|
||||
o.textContent = apiVersion;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AdvancedFormState} f
|
||||
* @returns {string}
|
||||
*/
|
||||
function serializeAdvancedKindCluster(f) {
|
||||
const api = f.apiVersion || "kind.x-k8s.io/v1alpha4";
|
||||
const img = (f.image || "").trim();
|
||||
if (!img) throw new Error("Укажите образ узла (kindest/node:…).");
|
||||
const cp = Math.min(5, Math.max(1, parseInt(String(f.controlPlanes), 10) || 1));
|
||||
const wk = Math.min(20, Math.max(0, parseInt(String(f.workers), 10) || 0));
|
||||
|
||||
const lines = ["kind: Cluster", "apiVersion: " + api];
|
||||
const netBlock = buildNetworkingYaml(f);
|
||||
if (netBlock) lines.push(netBlock.trimEnd());
|
||||
|
||||
lines.push("nodes:");
|
||||
const firstExtras = [];
|
||||
if (f.portEnabled && f.hostPort) {
|
||||
const hp = parseInt(String(f.hostPort), 10);
|
||||
const ctp = parseInt(String(f.containerPort), 10) || 80;
|
||||
if (hp >= 1 && hp <= 65535) {
|
||||
firstExtras.push(" extraPortMappings:");
|
||||
firstExtras.push(" - containerPort: " + ctp);
|
||||
firstExtras.push(" hostPort: " + hp);
|
||||
const listen = (f.portListenAddress || "").trim();
|
||||
if (listen) {
|
||||
firstExtras.push(' listenAddress: "' + listen.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"');
|
||||
}
|
||||
const proto = (f.portProtocol || "TCP").toUpperCase();
|
||||
if (proto === "TCP" || proto === "UDP" || proto === "SCTP") {
|
||||
firstExtras.push(" protocol: " + proto);
|
||||
}
|
||||
}
|
||||
}
|
||||
const patch = (f.kubeadmPatch || "").trim();
|
||||
if (patch) {
|
||||
firstExtras.push(" kubeadmConfigPatches:");
|
||||
firstExtras.push(" - |");
|
||||
patch.split("\n").forEach(function (ln) {
|
||||
firstExtras.push(" " + ln);
|
||||
});
|
||||
}
|
||||
let i;
|
||||
for (i = 0; i < cp; i++) {
|
||||
lines.push(" - role: control-plane");
|
||||
lines.push(" image: " + img);
|
||||
if (i === 0 && firstExtras.length) {
|
||||
firstExtras.forEach(function (x) {
|
||||
lines.push(x);
|
||||
});
|
||||
}
|
||||
}
|
||||
for (i = 0; i < wk; i++) {
|
||||
lines.push(" - role: worker");
|
||||
lines.push(" image: " + img);
|
||||
}
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {AdvancedFormState}
|
||||
*/
|
||||
function readAdvancedFormFromDom() {
|
||||
const pick = function (id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value : "";
|
||||
};
|
||||
const pickNum = function (id, def) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || !el.value.trim()) return def;
|
||||
const n = parseInt(el.value, 10);
|
||||
return Number.isNaN(n) ? def : n;
|
||||
};
|
||||
const pickChk = function (id) {
|
||||
const el = document.getElementById(id);
|
||||
return !!(el && el.checked);
|
||||
};
|
||||
return {
|
||||
apiVersion: pick("fld-adv-api-version") || "kind.x-k8s.io/v1alpha4",
|
||||
image: pick("fld-adv-image").trim(),
|
||||
controlPlanes: pickNum("fld-adv-cp", 1),
|
||||
workers: pickNum("fld-adv-workers", 0),
|
||||
ipFamily: pick("fld-adv-ip-family"),
|
||||
apiServerAddress: pick("fld-adv-api-addr"),
|
||||
apiServerPort: pick("fld-adv-api-port"),
|
||||
podSubnet: pick("fld-adv-pod-subnet"),
|
||||
serviceSubnet: pick("fld-adv-svc-subnet"),
|
||||
kubeProxyMode: pick("fld-adv-kube-proxy"),
|
||||
disableDefaultCNI: pickChk("fld-adv-disable-cni"),
|
||||
portEnabled: pickChk("fld-adv-port-enable"),
|
||||
hostPort: pick("fld-adv-host-port"),
|
||||
containerPort: pickNum("fld-adv-container-port", 80),
|
||||
portListenAddress: pick("fld-adv-listen-addr"),
|
||||
portProtocol: pick("fld-adv-port-proto") || "TCP",
|
||||
kubeadmPatch: (function () {
|
||||
const el = document.getElementById("fld-adv-kubeadm");
|
||||
return el ? el.value : "";
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
function advancedFormSnapshotString() {
|
||||
try {
|
||||
return JSON.stringify(readAdvancedFormFromDom());
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AdvancedFormState} f
|
||||
*/
|
||||
function applyAdvancedFormToDom(f) {
|
||||
const set = function (id, v) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = v != null ? String(v) : "";
|
||||
};
|
||||
ensureApiVersionOption(f.apiVersion || "kind.x-k8s.io/v1alpha4");
|
||||
set("fld-adv-api-version", f.apiVersion || "kind.x-k8s.io/v1alpha4");
|
||||
set("fld-adv-ip-family", f.ipFamily || "");
|
||||
set("fld-adv-api-addr", f.apiServerAddress || "");
|
||||
set("fld-adv-api-port", f.apiServerPort || "");
|
||||
set("fld-adv-pod-subnet", f.podSubnet || "");
|
||||
set("fld-adv-svc-subnet", f.serviceSubnet || "");
|
||||
set("fld-adv-kube-proxy", f.kubeProxyMode || "");
|
||||
const cni = document.getElementById("fld-adv-disable-cni");
|
||||
if (cni) cni.checked = !!f.disableDefaultCNI;
|
||||
set("fld-adv-image", f.image || "");
|
||||
set("fld-adv-cp", String(Math.min(5, Math.max(1, f.controlPlanes || 1))));
|
||||
set("fld-adv-workers", String(Math.min(20, Math.max(0, f.workers || 0))));
|
||||
const pe = document.getElementById("fld-adv-port-enable");
|
||||
if (pe) pe.checked = !!f.portEnabled;
|
||||
set("fld-adv-host-port", f.hostPort || "");
|
||||
set("fld-adv-container-port", String(f.containerPort || 80));
|
||||
set("fld-adv-listen-addr", f.portListenAddress || "");
|
||||
set("fld-adv-port-proto", f.portProtocol || "TCP");
|
||||
const kub = document.getElementById("fld-adv-kubeadm");
|
||||
if (kub) kub.value = f.kubeadmPatch || "";
|
||||
togglePortRow();
|
||||
}
|
||||
|
||||
function togglePortRow() {
|
||||
const pe = document.getElementById("fld-adv-port-enable");
|
||||
const row = document.getElementById("adv-port-fields-row");
|
||||
const on = !!(pe && pe.checked);
|
||||
if (row) {
|
||||
row.classList.toggle("hidden", !on);
|
||||
row.setAttribute("aria-hidden", on ? "false" : "true");
|
||||
}
|
||||
}
|
||||
|
||||
function updateAdvancedPreview() {
|
||||
const pre = document.getElementById("adv-yaml-preview");
|
||||
if (!pre) return;
|
||||
try {
|
||||
pre.textContent = serializeAdvancedKindCluster(readAdvancedFormFromDom());
|
||||
} catch (e) {
|
||||
pre.textContent = "# " + (e.message || "Заполните образ узла");
|
||||
}
|
||||
}
|
||||
|
||||
function setModeUi(isAdvanced) {
|
||||
const simple = document.getElementById("edit-simple-block");
|
||||
const adv = document.getElementById("edit-advanced-block");
|
||||
const hint = document.getElementById("cluster-edit-mode-hint");
|
||||
const radios = document.querySelectorAll('input[name="save_mode"]');
|
||||
radios.forEach(function (r) {
|
||||
r.checked = (isAdvanced && r.value === "yaml") || (!isAdvanced && r.value === "simple");
|
||||
});
|
||||
if (simple) {
|
||||
simple.classList.toggle("hidden", isAdvanced);
|
||||
simple.setAttribute("aria-hidden", isAdvanced ? "true" : "false");
|
||||
}
|
||||
if (adv) {
|
||||
adv.classList.toggle("hidden", !isAdvanced);
|
||||
adv.setAttribute("aria-hidden", !isAdvanced ? "true" : "false");
|
||||
}
|
||||
if (hint) {
|
||||
hint.textContent = isAdvanced
|
||||
? "Секции соответствуют kind Cluster: networking, nodes, extraPortMappings и kubeadmConfigPatches (к первой control-plane)."
|
||||
: "Только тег kindest/node и число worker-нод; конфиг пересобирается как при создании кластера.";
|
||||
}
|
||||
if (isAdvanced) updateAdvancedPreview();
|
||||
}
|
||||
|
||||
function getSaveMode() {
|
||||
const c = document.querySelector('input[name="save_mode"]:checked');
|
||||
return c && c.value === "yaml" ? "yaml" : "simple";
|
||||
}
|
||||
|
||||
function normTag(s) {
|
||||
return String(s || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^v/, "");
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
const noteEl = document.getElementById("edit-kind-note");
|
||||
const msg = document.getElementById("edit-msg");
|
||||
const verInput = document.getElementById("kubernetes_version_edit");
|
||||
const workersEl = document.getElementById("fld-workers-edit");
|
||||
const descEl = document.getElementById("fld-description");
|
||||
try {
|
||||
const d = await api("/clusters/" + encodeURIComponent(clusterName) + "/config");
|
||||
if (noteEl) {
|
||||
noteEl.textContent = d.kind_note || "";
|
||||
}
|
||||
const meta = d.meta || {};
|
||||
initialTag = meta.kubernetes_version_tag != null ? String(meta.kubernetes_version_tag) : "";
|
||||
if (verInput) {
|
||||
const tag = initialTag;
|
||||
verInput.value = tag ? tag.replace(/^v/i, "") : "";
|
||||
}
|
||||
if (meta.worker_nodes != null) {
|
||||
const wn = Number(meta.worker_nodes);
|
||||
initialWorkers = Number.isNaN(wn) ? 0 : wn;
|
||||
} else {
|
||||
initialWorkers = null;
|
||||
}
|
||||
if (workersEl) {
|
||||
workersEl.value = String(initialWorkers !== null ? initialWorkers : 0);
|
||||
}
|
||||
initialDescription = meta.description != null ? String(meta.description) : "";
|
||||
if (descEl) {
|
||||
descEl.value = initialDescription;
|
||||
}
|
||||
const yamlText = d.kind_config_yaml != null ? String(d.kind_config_yaml) : "";
|
||||
loadedYamlSnapshot = yamlText;
|
||||
const advParsed = parseKindYamlToAdvancedForm(yamlText);
|
||||
if (!advParsed.image && meta.node_image) {
|
||||
advParsed.image = String(meta.node_image);
|
||||
}
|
||||
if (advParsed.controlPlanes < 1) advParsed.controlPlanes = 1;
|
||||
applyAdvancedFormToDom(advParsed);
|
||||
initialAdvancedSnapshot = advancedFormSnapshotString();
|
||||
updateAdvancedPreview();
|
||||
if (msg) msg.textContent = "";
|
||||
} catch (e) {
|
||||
if (noteEl) noteEl.textContent = "";
|
||||
if (msg) {
|
||||
msg.textContent = e.status === 404 ? "Каталог кластера не найден." : "Ошибка загрузки: " + e.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bindAdvancedInputs() {
|
||||
const ids = [
|
||||
"fld-adv-api-version",
|
||||
"fld-adv-ip-family",
|
||||
"fld-adv-api-addr",
|
||||
"fld-adv-api-port",
|
||||
"fld-adv-pod-subnet",
|
||||
"fld-adv-svc-subnet",
|
||||
"fld-adv-kube-proxy",
|
||||
"fld-adv-disable-cni",
|
||||
"fld-adv-image",
|
||||
"fld-adv-cp",
|
||||
"fld-adv-workers",
|
||||
"fld-adv-port-enable",
|
||||
"fld-adv-host-port",
|
||||
"fld-adv-container-port",
|
||||
"fld-adv-listen-addr",
|
||||
"fld-adv-port-proto",
|
||||
"fld-adv-kubeadm",
|
||||
];
|
||||
ids.forEach(function (id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.addEventListener("input", updateAdvancedPreview);
|
||||
el.addEventListener("change", updateAdvancedPreview);
|
||||
});
|
||||
const pe = document.getElementById("fld-adv-port-enable");
|
||||
if (pe) {
|
||||
pe.addEventListener("change", function () {
|
||||
togglePortRow();
|
||||
updateAdvancedPreview();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bindForm() {
|
||||
const form = document.getElementById("form-cluster-edit");
|
||||
if (!form) return;
|
||||
document.querySelectorAll('input[name="save_mode"]').forEach(function (r) {
|
||||
r.addEventListener("change", function () {
|
||||
setModeUi(getSaveMode() === "yaml");
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async function (ev) {
|
||||
ev.preventDefault();
|
||||
const msg = document.getElementById("edit-msg");
|
||||
const mode = getSaveMode();
|
||||
const descEl = document.getElementById("fld-description");
|
||||
const descVal = descEl ? descEl.value : "";
|
||||
/** @type {Record<string, unknown>} */
|
||||
const payload = { description: descVal };
|
||||
|
||||
if (mode === "yaml") {
|
||||
let yamlText;
|
||||
try {
|
||||
yamlText = serializeAdvancedKindCluster(readAdvancedFormFromDom());
|
||||
} catch (e) {
|
||||
if (msg) msg.textContent = e.message || "Проверьте поля расширенного режима.";
|
||||
return;
|
||||
}
|
||||
const yamlChanged = yamlText !== (loadedYamlSnapshot || "");
|
||||
const advChanged = advancedFormSnapshotString() !== initialAdvancedSnapshot;
|
||||
const descChanged = descVal !== initialDescription;
|
||||
if (!yamlChanged && !advChanged && !descChanged) {
|
||||
if (msg) msg.textContent = "Изменений нет.";
|
||||
return;
|
||||
}
|
||||
if (yamlChanged || advChanged) {
|
||||
payload.kind_config_yaml = yamlText;
|
||||
}
|
||||
} else {
|
||||
const verInput = document.getElementById("kubernetes_version_edit");
|
||||
const workersEl = document.getElementById("fld-workers-edit");
|
||||
const ver = verInput ? verInput.value.trim() : "";
|
||||
const wParsed = workersEl ? parseInt(workersEl.value, 10) : NaN;
|
||||
const workersOk = !Number.isNaN(wParsed);
|
||||
const effInitW = initialWorkers !== null && initialWorkers !== undefined ? initialWorkers : 0;
|
||||
const workersChanged = workersOk && wParsed !== effInitW;
|
||||
const verChanged = normTag(ver) !== normTag(initialTag);
|
||||
const descChanged = descVal !== initialDescription;
|
||||
if (!verChanged && !workersChanged && !descChanged) {
|
||||
if (msg) msg.textContent = "Изменений нет.";
|
||||
return;
|
||||
}
|
||||
if (verChanged) {
|
||||
if (!ver) {
|
||||
if (msg) msg.textContent = "Укажите тег образа или откатите поле к прежнему значению.";
|
||||
return;
|
||||
}
|
||||
payload.kubernetes_version = ver;
|
||||
}
|
||||
if (workersChanged) {
|
||||
if (!workersOk || wParsed < 0 || wParsed > 20) {
|
||||
if (msg) msg.textContent = "Число worker-нод должно быть от 0 до 20.";
|
||||
return;
|
||||
}
|
||||
payload.workers = wParsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (msg) msg.textContent = "Сохранение…";
|
||||
try {
|
||||
const r = await api("/clusters/" + encodeURIComponent(clusterName) + "/config", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (msg) {
|
||||
msg.textContent = r.message || "Сохранено.";
|
||||
}
|
||||
openClusterEditSaveModal(!!r.updated_kind_config_yaml, !!r.registered_in_kind);
|
||||
await loadConfig();
|
||||
} catch (e) {
|
||||
if (msg) msg.textContent = "Ошибка: " + e.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
wireSaveInfoModal();
|
||||
|
||||
loadVersions();
|
||||
loadConfig().then(function () {
|
||||
bindAdvancedInputs();
|
||||
togglePortRow();
|
||||
});
|
||||
bindForm();
|
||||
setModeUi(false);
|
||||
})();
|
||||
+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