Веб-UI: темы, навигация API, статистика и доработки API
- Шапка: логотип Kubernetes, ссылка на главную, выпадающее меню API (Swagger/ReDoc/Health), переключатель светлой/тёмной темы (localStorage). - Светлая тема в синей гамме; выравнивание кнопки темы в ряду с пилюлями. - Дашборд: единая карточка ошибки health/stats, подсказка Docker/Podman, поле container_cli в GET /stats, total_workers_from_meta всегда число (0 без meta). - Правки кластеров, job_store, compose, документация и частичные шаблоны.
This commit is contained in:
+160
-15
@@ -6,29 +6,127 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{% block page_title %}{{ app_title }}{% endblock %} — kind</title>
|
||||
{# Тема до первой отрисовки: localStorage kind_k8s_theme или prefers-color-scheme #}
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var k = "kind_k8s_theme";
|
||||
var v = localStorage.getItem(k);
|
||||
var t =
|
||||
v === "light" || v === "dark"
|
||||
? v
|
||||
: window.matchMedia("(prefers-color-scheme: light)").matches
|
||||
? "light"
|
||||
: "dark";
|
||||
document.documentElement.setAttribute("data-theme", t);
|
||||
} catch (e) {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<script src="/static/js/theme.js" defer></script>
|
||||
{% block head_extra %}{% endblock %}
|
||||
</head>
|
||||
<body
|
||||
class="app-body"
|
||||
class="app-body{% block body_extra_class %}{% endblock %}"
|
||||
data-api-base="/api/v1"
|
||||
data-app-title="{{ app_title | e }}"
|
||||
{% block body_attrs %}{% endblock %}
|
||||
>
|
||||
<a class="skip-link" href="#main-content">К основному содержимому</a>
|
||||
|
||||
<header class="top-nav" role="banner">
|
||||
<div class="top-nav-inner">
|
||||
<div class="nav-brand">
|
||||
<span class="nav-logo" aria-hidden="true">◈</span>
|
||||
{# Логотип Kubernetes (SVG из набора Simple Icons, CC0) + название — одна ссылка на главную. #}
|
||||
<a href="/" class="nav-brand nav-brand-link">
|
||||
<img
|
||||
src="/static/icons/kubernetes.svg"
|
||||
alt=""
|
||||
class="nav-logo-img"
|
||||
width="28"
|
||||
height="28"
|
||||
decoding="async"
|
||||
/>
|
||||
<span class="nav-title">{{ app_title }}</span>
|
||||
</div>
|
||||
<nav class="nav-links" aria-label="Разделы">
|
||||
{# Панель подсвечивается только на дашборде; на странице документации — обычная пилюля, как Swagger/ReDoc. #}
|
||||
</a>
|
||||
{# Один ряд flex: Панель, Документация, API, тема — общий align-items: center #}
|
||||
<nav class="nav-links top-nav-actions" aria-label="Разделы и оформление">
|
||||
<a href="/" class="nav-link nav-pill{% if nav_active|default('') == 'panel' %} nav-pill--active{% endif %}">Панель</a>
|
||||
<a href="/documentation" class="nav-link nav-pill{% if nav_active|default('') == 'documentation' %} nav-pill--active{% endif %}">Документация</a>
|
||||
<a href="/docs" class="nav-link nav-pill nav-pill--ext" data-open-window="kind_swagger">Swagger</a>
|
||||
<a href="/redoc" class="nav-link nav-pill nav-pill--ext" data-open-window="kind_redoc">ReDoc</a>
|
||||
<a href="/api/v1/health" class="nav-link nav-pill nav-pill--ext" data-open-window="kind_health">Health</a>
|
||||
<div class="nav-dropdown">
|
||||
<button
|
||||
type="button"
|
||||
class="nav-link nav-pill nav-dropdown__trigger"
|
||||
id="nav-api-trigger"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-controls="nav-api-menu"
|
||||
>
|
||||
API
|
||||
</button>
|
||||
<ul class="nav-dropdown__menu" id="nav-api-menu" role="menu" hidden>
|
||||
<li role="none">
|
||||
<a
|
||||
role="menuitem"
|
||||
href="/docs"
|
||||
class="nav-dropdown__link nav-pill--ext"
|
||||
data-open-window="kind_swagger"
|
||||
>Swagger</a>
|
||||
</li>
|
||||
<li role="none">
|
||||
<a
|
||||
role="menuitem"
|
||||
href="/redoc"
|
||||
class="nav-dropdown__link nav-pill--ext"
|
||||
data-open-window="kind_redoc"
|
||||
>ReDoc</a>
|
||||
</li>
|
||||
<li role="none">
|
||||
<a
|
||||
role="menuitem"
|
||||
href="/api/v1/health"
|
||||
class="nav-dropdown__link nav-pill--ext"
|
||||
data-open-window="kind_health"
|
||||
>Health</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" id="theme-toggle" class="theme-toggle theme-toggle--nav-end">
|
||||
<svg
|
||||
class="theme-toggle__icon theme-toggle__icon--sun"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
height="18"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
d="M12 2v2M12 20v2M2 12h2M20 12h2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M19.07 4.93l-1.41 1.41M6.34 17.66l-1.41 1.41"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="theme-toggle__icon theme-toggle__icon--moon"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
height="18"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linejoin="round"
|
||||
d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -53,16 +151,63 @@
|
||||
{% endblock %}
|
||||
</footer>
|
||||
|
||||
{# Отдельное окно браузера для документации и health (не вкладка). #}
|
||||
{# Отдельное окно для Swagger/ReDoc/Health; выпадающее меню API. #}
|
||||
<script>
|
||||
(function () {
|
||||
var opts = "noopener,noreferrer,width=1240,height=840,scrollbars=yes,resizable=yes";
|
||||
document.querySelectorAll(".nav-link[data-open-window]").forEach(function (a) {
|
||||
a.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
var name = a.getAttribute("data-open-window") || "kind_ext";
|
||||
window.open(a.getAttribute("href"), name, opts);
|
||||
function bindOpenWindow(scope) {
|
||||
(scope || document).querySelectorAll("[data-open-window]").forEach(function (a) {
|
||||
if (a.tagName !== "A") return;
|
||||
a.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
var name = a.getAttribute("data-open-window") || "kind_ext";
|
||||
window.open(a.getAttribute("href"), name, opts);
|
||||
});
|
||||
});
|
||||
}
|
||||
bindOpenWindow();
|
||||
|
||||
var dd = document.querySelector(".nav-dropdown");
|
||||
if (!dd) return;
|
||||
var btn = document.getElementById("nav-api-trigger");
|
||||
var menu = document.getElementById("nav-api-menu");
|
||||
if (!btn || !menu) return;
|
||||
|
||||
function closeApiMenu() {
|
||||
dd.classList.remove("nav-dropdown--open");
|
||||
btn.setAttribute("aria-expanded", "false");
|
||||
menu.setAttribute("hidden", "");
|
||||
}
|
||||
function openApiMenu() {
|
||||
dd.classList.add("nav-dropdown--open");
|
||||
btn.setAttribute("aria-expanded", "true");
|
||||
menu.removeAttribute("hidden");
|
||||
}
|
||||
function toggleApiMenu() {
|
||||
if (menu.hasAttribute("hidden")) openApiMenu();
|
||||
else closeApiMenu();
|
||||
}
|
||||
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
toggleApiMenu();
|
||||
});
|
||||
|
||||
menu.querySelectorAll("a[role='menuitem']").forEach(function (a) {
|
||||
a.addEventListener("click", function () {
|
||||
closeApiMenu();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("click", function () {
|
||||
closeApiMenu();
|
||||
});
|
||||
dd.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") closeApiMenu();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user