Документация и kubectl из контейнера; Kind Clusters Dashboard
- Цель make docker|podman kubectl CLUSTER=… (KUBECTL_ARGS) — exec kubectl в kind-k8s-web - README: без kubectl на хосте; раздел про проверку API из контейнера - create_cluster/cluster_status: подсказки для UI, make kubectl и exec в контейнере - app/docs: api_routes.md и README.md про kubectl и API workloads - Прочее: переименование проекта, документация, UI документации (ранее в рабочем дереве)
This commit is contained in:
+212
-31
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Страница /documentation: загрузка README через API и рендер Markdown.
|
||||
* Страница /documentation: README и файлы app/docs/*.md через API, рендер Markdown.
|
||||
* Зависимости из репозитория: /static/js/vendor/marked.min.js, purify.min.js
|
||||
*
|
||||
* Автор: Сергей Антропов
|
||||
@@ -8,8 +8,9 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const body = document.body;
|
||||
const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
|
||||
var body = document.body;
|
||||
var API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
|
||||
var DOC_BASE = "/documentation";
|
||||
|
||||
function showErr(el, msg) {
|
||||
if (!el) return;
|
||||
@@ -17,56 +18,236 @@
|
||||
el.classList.remove("hidden");
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const article = document.getElementById("readme-doc-article");
|
||||
const errEl = document.getElementById("readme-error");
|
||||
const loadEl = document.getElementById("readme-loading");
|
||||
if (!article || !loadEl) return;
|
||||
function hideErr(el) {
|
||||
if (!el) return;
|
||||
el.textContent = "";
|
||||
el.classList.add("hidden");
|
||||
}
|
||||
|
||||
function getPathFromLocation() {
|
||||
var q = new URLSearchParams(window.location.search || "");
|
||||
var p = (q.get("path") || "").trim();
|
||||
return p || null;
|
||||
}
|
||||
|
||||
function docUrlForPath(relPath) {
|
||||
if (!relPath) return DOC_BASE;
|
||||
return DOC_BASE + "?path=" + encodeURIComponent(relPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Внутренняя ссылка на .md под app/docs/: полный путь или относительно текущего файла.
|
||||
*/
|
||||
function resolveInternalMdHref(href, currentDocPath) {
|
||||
if (!href) return null;
|
||||
var h = href.trim();
|
||||
if (h.startsWith("#")) return null;
|
||||
if (/^https?:\/\//i.test(h)) return null;
|
||||
if (/^mailto:/i.test(h)) return null;
|
||||
|
||||
var abs = h.match(/^(\.\/)?(app\/docs\/[^?#]+\.md)([\?#].*)?$/i);
|
||||
if (abs) {
|
||||
return { path: abs[2].replace(/\\/g, "/"), tail: abs[3] || "" };
|
||||
}
|
||||
if (!currentDocPath || currentDocPath.indexOf("app/docs/") !== 0) return null;
|
||||
if (h.indexOf("..") >= 0) return null;
|
||||
|
||||
var baseDir = currentDocPath.replace(/[^/]+\.md$/i, "");
|
||||
if (!baseDir) baseDir = "app/docs/";
|
||||
try {
|
||||
var u = new URL(h, "http://doc.local/" + baseDir);
|
||||
var pathname = decodeURIComponent(u.pathname.replace(/^\/+/, ""));
|
||||
if (pathname.indexOf("..") >= 0) return null;
|
||||
if (!pathname.startsWith("app/docs/") || !pathname.endsWith(".md")) return null;
|
||||
var tail = (u.hash || "") + (u.search || "");
|
||||
return { path: pathname, tail: tail };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function rewriteMdLinks(container, currentDocPath) {
|
||||
container.querySelectorAll("a[href]").forEach(function (a) {
|
||||
var href = a.getAttribute("href");
|
||||
var resolved = resolveInternalMdHref(href, currentDocPath);
|
||||
if (!resolved) return;
|
||||
a.setAttribute("href", docUrlForPath(resolved.path) + resolved.tail);
|
||||
a.classList.add("doc-md-link");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* До первого H2 — одна карточка; для каждого H2 — карточка заголовка и карточка тела до следующего H2.
|
||||
*/
|
||||
function sectionizeFromNodes(sourceRoot) {
|
||||
var page = document.createElement("div");
|
||||
page.className = "readme-doc-page-inner";
|
||||
var root = document.createElement("div");
|
||||
root.className = "readme-doc-root";
|
||||
page.appendChild(root);
|
||||
|
||||
var nodes = Array.prototype.slice.call(sourceRoot.childNodes);
|
||||
var i = 0;
|
||||
|
||||
function appendCard(classExtra, chunk) {
|
||||
if (!chunk.length) return;
|
||||
var card = document.createElement("section");
|
||||
card.className = "card readme-section-card markdown-body " + classExtra;
|
||||
chunk.forEach(function (n) {
|
||||
card.appendChild(n);
|
||||
});
|
||||
root.appendChild(card);
|
||||
}
|
||||
|
||||
function flushIntro() {
|
||||
var chunk = [];
|
||||
while (i < nodes.length) {
|
||||
var n = nodes[i];
|
||||
if (n.nodeType === 1 && n.tagName === "H2") break;
|
||||
chunk.push(n);
|
||||
i++;
|
||||
}
|
||||
appendCard("readme-section-card--intro", chunk);
|
||||
}
|
||||
|
||||
function flushH2Pair() {
|
||||
if (i >= nodes.length) return;
|
||||
var h2 = nodes[i];
|
||||
if (h2.nodeType !== 1 || h2.tagName !== "H2") return;
|
||||
i++;
|
||||
appendCard("readme-section-card--head", [h2]);
|
||||
|
||||
var bodyChunk = [];
|
||||
while (i < nodes.length) {
|
||||
var n = nodes[i];
|
||||
if (n.nodeType === 1 && n.tagName === "H2") break;
|
||||
bodyChunk.push(n);
|
||||
i++;
|
||||
}
|
||||
appendCard("readme-section-card--body", bodyChunk);
|
||||
}
|
||||
|
||||
flushIntro();
|
||||
while (i < nodes.length) flushH2Pair();
|
||||
return page;
|
||||
}
|
||||
|
||||
function renderMarkdownToRoot(text, docPath, rootEl, errEl, loadEl) {
|
||||
if (typeof marked === "undefined" || typeof DOMPurify === "undefined") {
|
||||
showErr(errEl, "Не загружены скрипты marked или DOMPurify из /static/js/vendor/.");
|
||||
loadEl.classList.add("hidden");
|
||||
if (loadEl) loadEl.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch(API + "/docs/readme", {
|
||||
headers: { Accept: "text/markdown, text/plain, */*" },
|
||||
});
|
||||
const text = await r.text();
|
||||
if (!r.ok) {
|
||||
var detail = text;
|
||||
try {
|
||||
var j = JSON.parse(text);
|
||||
if (j.detail) detail = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
|
||||
} catch (e) {
|
||||
/* сырой текст */
|
||||
}
|
||||
showErr(errEl, "Не удалось загрузить README: " + (detail || r.statusText));
|
||||
loadEl.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
/* marked v15: переносы строк в параграфах (breaks). */
|
||||
try {
|
||||
if (marked.defaults && typeof marked.defaults === "object") {
|
||||
marked.defaults.breaks = true;
|
||||
marked.defaults.gfm = true;
|
||||
}
|
||||
} catch (e) {
|
||||
/* игнорируем, если defaults защищены от записи */
|
||||
} catch (e1) {
|
||||
/* ignore */
|
||||
}
|
||||
var rawHtml =
|
||||
typeof marked.parse === "function" ? marked.parse(text, { async: false }) : marked(text);
|
||||
article.innerHTML = DOMPurify.sanitize(rawHtml);
|
||||
article.removeAttribute("hidden");
|
||||
|
||||
var temp = document.createElement("div");
|
||||
temp.innerHTML = DOMPurify.sanitize(rawHtml);
|
||||
rewriteMdLinks(temp, docPath);
|
||||
|
||||
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
|
||||
var pageInner = sectionizeFromNodes(temp);
|
||||
rootEl.appendChild(pageInner);
|
||||
rootEl.removeAttribute("hidden");
|
||||
} catch (e2) {
|
||||
showErr(errEl, "Ошибка разбора: " + (e2.message || String(e2)));
|
||||
}
|
||||
}
|
||||
|
||||
function fetchUrlForPath(docPath) {
|
||||
if (!docPath) return API + "/docs/readme";
|
||||
return API + "/docs/file?path=" + encodeURIComponent(docPath);
|
||||
}
|
||||
|
||||
async function loadDocumentation(docPath, opts) {
|
||||
opts = opts || {};
|
||||
var push = opts.pushState === true;
|
||||
var replace = opts.replaceState === true;
|
||||
|
||||
var rootEl = document.getElementById("readme-doc-root");
|
||||
var errEl = document.getElementById("readme-error");
|
||||
var loadEl = document.getElementById("readme-loading");
|
||||
if (!rootEl || !loadEl) return;
|
||||
|
||||
hideErr(errEl);
|
||||
rootEl.setAttribute("hidden", "hidden");
|
||||
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
|
||||
loadEl.classList.remove("hidden");
|
||||
|
||||
var url = fetchUrlForPath(docPath);
|
||||
try {
|
||||
var r = await fetch(url, {
|
||||
headers: { Accept: "text/markdown, text/plain, */*" },
|
||||
});
|
||||
var text = await r.text();
|
||||
if (!r.ok) {
|
||||
var detail = text;
|
||||
try {
|
||||
var j = JSON.parse(text);
|
||||
if (j.detail) detail = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
|
||||
} catch (e0) {
|
||||
/* raw */
|
||||
}
|
||||
showErr(errEl, "Не удалось загрузить документ: " + (detail || r.statusText));
|
||||
loadEl.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
renderMarkdownToRoot(text, docPath, rootEl, errEl, loadEl);
|
||||
loadEl.classList.add("hidden");
|
||||
|
||||
var newUrl = docUrlForPath(docPath);
|
||||
if (push) {
|
||||
window.history.pushState({ docPath: docPath }, "", newUrl);
|
||||
} else if (replace) {
|
||||
window.history.replaceState({ docPath: docPath }, "", newUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
showErr(errEl, "Ошибка: " + (e.message || String(e)));
|
||||
loadEl.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function onPopState() {
|
||||
loadDocumentation(getPathFromLocation(), {});
|
||||
}
|
||||
|
||||
function onRootClick(e) {
|
||||
var a = e.target && e.target.closest ? e.target.closest("a.doc-md-link") : null;
|
||||
if (!a || !a.getAttribute("href")) return;
|
||||
var href = a.getAttribute("href");
|
||||
if (href.indexOf(DOC_BASE) !== 0) return;
|
||||
e.preventDefault();
|
||||
try {
|
||||
var u = new URL(href, window.location.origin);
|
||||
var p = (u.searchParams.get("path") || "").trim() || null;
|
||||
loadDocumentation(p, { pushState: true });
|
||||
} catch (e1) {
|
||||
loadDocumentation(getPathFromLocation(), { pushState: true });
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
var rootEl = document.getElementById("readme-doc-root");
|
||||
if (!rootEl) return;
|
||||
|
||||
window.addEventListener("popstate", onPopState);
|
||||
rootEl.addEventListener("click", onRootClick);
|
||||
|
||||
var initial = getPathFromLocation();
|
||||
loadDocumentation(initial, { replaceState: true });
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", run);
|
||||
} else {
|
||||
|
||||
+66
-40
@@ -1,4 +1,4 @@
|
||||
/* Минимальные стили веб-интерфейса kind-k8s-develop.
|
||||
/* Минимальные стили веб-интерфейса Kind Clusters Dashboard.
|
||||
Автор: Сергей Антропов — https://devops.org.ru */
|
||||
|
||||
:root {
|
||||
@@ -146,6 +146,21 @@ body.modal-open {
|
||||
background: linear-gradient(145deg, rgba(59, 130, 246, 0.35), rgba(59, 130, 246, 0.15));
|
||||
}
|
||||
|
||||
/* Активный раздел (панель или документация) */
|
||||
.nav-link.nav-pill.nav-pill--active:not(.nav-pill--ext) {
|
||||
border-color: rgba(59, 130, 246, 0.85);
|
||||
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.35), 0 2px 14px rgba(59, 130, 246, 0.18);
|
||||
color: #93c5fd;
|
||||
}
|
||||
.nav-link.nav-pill.nav-pill--home.nav-pill--active {
|
||||
background: linear-gradient(145deg, rgba(59, 130, 246, 0.42), rgba(59, 130, 246, 0.18));
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
.nav-link.nav-pill.nav-pill--active:not(.nav-pill--ext) {
|
||||
color: #1e40af;
|
||||
}
|
||||
}
|
||||
|
||||
/* Внешние окна: иконка «новое окно» */
|
||||
.nav-link.nav-pill--ext::after {
|
||||
content: "↗";
|
||||
@@ -887,78 +902,89 @@ button.btn-danger:hover {
|
||||
filter: brightness(1.12);
|
||||
}
|
||||
|
||||
/* Страница «Документация»: README.md → HTML (Markdown) */
|
||||
.readme-doc-card {
|
||||
/* Страница «Документация»: Markdown → карточки секций */
|
||||
.readme-doc-shell {
|
||||
max-width: 52rem;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
.readme-doc-toolbar {
|
||||
align-items: center;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.readme-doc-toolbar .page-title {
|
||||
margin: 0;
|
||||
}
|
||||
.readme-doc-lead {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.88rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.readme-doc-loading {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.readme-doc.markdown-body {
|
||||
.readme-doc-page {
|
||||
width: 100%;
|
||||
}
|
||||
.readme-doc-page-inner {
|
||||
width: 100%;
|
||||
}
|
||||
.readme-doc-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.readme-section-card {
|
||||
margin: 0;
|
||||
}
|
||||
.readme-section-card--head h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
.readme-section-card.markdown-body {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.readme-doc.markdown-body h1,
|
||||
.readme-doc.markdown-body h2,
|
||||
.readme-doc.markdown-body h3,
|
||||
.readme-doc.markdown-body h4 {
|
||||
.readme-section-card.markdown-body h1,
|
||||
.readme-section-card.markdown-body h2,
|
||||
.readme-section-card.markdown-body h3,
|
||||
.readme-section-card.markdown-body h4 {
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
line-height: 1.25;
|
||||
font-weight: 650;
|
||||
}
|
||||
.readme-doc.markdown-body h1 {
|
||||
.readme-section-card.markdown-body h1 {
|
||||
font-size: 1.45rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.35rem;
|
||||
}
|
||||
.readme-doc.markdown-body h2 {
|
||||
.readme-section-card.markdown-body h2 {
|
||||
font-size: 1.2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.readme-doc.markdown-body h3 {
|
||||
.readme-section-card--head.markdown-body h2 {
|
||||
margin: 0;
|
||||
}
|
||||
.readme-section-card.markdown-body h3 {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.readme-doc.markdown-body p {
|
||||
.readme-section-card.markdown-body p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.readme-doc.markdown-body ul,
|
||||
.readme-doc.markdown-body ol {
|
||||
.readme-section-card.markdown-body ul,
|
||||
.readme-section-card.markdown-body ol {
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1.35rem;
|
||||
}
|
||||
.readme-doc.markdown-body li {
|
||||
.readme-section-card.markdown-body li {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
.readme-doc.markdown-body a {
|
||||
.readme-section-card.markdown-body a {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.readme-doc.markdown-body a:hover {
|
||||
.readme-section-card.markdown-body a:hover {
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
.readme-doc.markdown-body code {
|
||||
.readme-section-card.markdown-body code {
|
||||
font-size: 0.88em;
|
||||
padding: 0.12em 0.35em;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.readme-doc.markdown-body pre {
|
||||
.readme-section-card.markdown-body pre {
|
||||
margin: 0.65rem 0;
|
||||
padding: 0.65rem 0.85rem;
|
||||
overflow-x: auto;
|
||||
@@ -968,45 +994,45 @@ button.btn-danger:hover {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.readme-doc.markdown-body pre code {
|
||||
.readme-section-card.markdown-body pre code {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: inherit;
|
||||
}
|
||||
.readme-doc.markdown-body blockquote {
|
||||
.readme-section-card.markdown-body blockquote {
|
||||
margin: 0.65rem 0;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-left: 4px solid var(--accent);
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
color: var(--muted);
|
||||
}
|
||||
.readme-doc.markdown-body table {
|
||||
.readme-section-card.markdown-body table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0.75rem 0;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.readme-doc.markdown-body th,
|
||||
.readme-doc.markdown-body td {
|
||||
.readme-section-card.markdown-body th,
|
||||
.readme-section-card.markdown-body td {
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.4rem 0.55rem;
|
||||
text-align: left;
|
||||
}
|
||||
.readme-doc.markdown-body th {
|
||||
.readme-section-card.markdown-body th {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
font-weight: 600;
|
||||
}
|
||||
.readme-doc.markdown-body hr {
|
||||
.readme-section-card.markdown-body hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 1.25rem 0;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
.readme-doc.markdown-body code {
|
||||
.readme-section-card.markdown-body code {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.readme-doc.markdown-body pre {
|
||||
.readme-section-card.markdown-body pre {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user