f0b24c8901
- Навигация: выезжающее меню при узком экране (nav-mobile.js) - Журнал: карточки <620px, компактная пагинация, время в две строки <920px - Создание кластера: оверлей загрузки, инкрементальное обновление таблицы заданий - Документация: полноэкранный спиннер при загрузке и навигации - Главная: масштабирование CTA, статистика 2 колонки <520px, донаты перенос <710px - README: env.example, новые фичи UI, автор в конце файла - api_routes: маршрут /cluster-create, спиннеры, шаблоны; автор в конце - env.example: автор перенесён в конец файла
280 lines
11 KiB
HTML
280 lines
11 KiB
HTML
{# Общий каркас страниц веб-интерфейса Kind Clusters Dashboard.
|
|
Автор: Сергей Антропов — https://devops.org.ru #}
|
|
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
|
|
<link rel="alternate icon" href="/favicon.ico" />
|
|
<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>
|
|
<script src="/static/js/nav-mobile.js" defer></script>
|
|
{% block head_extra %}{% endblock %}
|
|
</head>
|
|
<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">
|
|
{# Логотип 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>
|
|
</a>
|
|
{# При ширине < 920px — «гамбургер»; панель #top-nav-drawer справа (nav-mobile.js). #}
|
|
<button
|
|
type="button"
|
|
class="nav-menu-toggle"
|
|
id="nav-menu-toggle"
|
|
aria-expanded="false"
|
|
aria-controls="top-nav-drawer"
|
|
aria-label="Открыть меню разделов"
|
|
>
|
|
<span class="nav-menu-toggle__bars" aria-hidden="true">
|
|
<span class="nav-menu-toggle__bar"></span>
|
|
<span class="nav-menu-toggle__bar"></span>
|
|
<span class="nav-menu-toggle__bar"></span>
|
|
</span>
|
|
</button>
|
|
<div
|
|
class="nav-menu-backdrop"
|
|
id="nav-menu-backdrop"
|
|
hidden
|
|
tabindex="-1"
|
|
aria-hidden="true"
|
|
></div>
|
|
{# Один ряд flex (десктоп): пилюли, API, тема; на мобильных — содержимое в выезжающей панели #}
|
|
<nav class="nav-links top-nav-actions" id="top-nav-drawer" aria-label="Разделы и оформление">
|
|
{# Шапка панели только на узких экранах (CSS); кнопка дублирует закрытие с «гамбургера». #}
|
|
<div class="nav-drawer-header">
|
|
<span class="nav-drawer-title">Меню</span>
|
|
<button
|
|
type="button"
|
|
class="nav-drawer-close"
|
|
id="nav-drawer-close"
|
|
aria-label="Закрыть меню"
|
|
>
|
|
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false">
|
|
<path
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
d="M6 6l12 12M18 6L6 18"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{# Прокрутка только у пилюль: overflow-x на nav обрезал бы выпадающее меню API (position: absolute). #}
|
|
<div class="nav-links-scroll">
|
|
<a href="/" class="nav-link nav-pill{% if nav_active|default('') == 'panel' %} nav-pill--active{% endif %}">Панель</a>
|
|
<a href="/clusters" class="nav-link nav-pill{% if nav_active|default('') == 'clusters' %} nav-pill--active{% endif %}">Кластеры</a>
|
|
<a href="/cluster-addons" class="nav-link nav-pill{% if nav_active|default('') == 'cluster_addons' %} nav-pill--active{% endif %}">Аддоны</a>
|
|
<a href="/journal" class="nav-link nav-pill{% if nav_active|default('') == 'journal' %} nav-pill--active{% endif %}">Журнал</a>
|
|
<a href="/documentation" class="nav-link nav-pill{% if nav_active|default('') == 'documentation' %} nav-pill--active{% endif %}">Документация</a>
|
|
</div>
|
|
<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>
|
|
<li role="none">
|
|
<a
|
|
role="menuitem"
|
|
href="https://hub.docker.com/r/kindest/node/tags"
|
|
class="nav-dropdown__link nav-pill--ext"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
title="Список тегов kindest/node на Docker Hub (новое окно)"
|
|
>Теги образов</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>
|
|
|
|
<div id="toast" class="toast hidden" role="status" aria-live="polite"></div>
|
|
{# Плавающие подсказки для иконок (position:fixed; не режутся overflow таблицы). #}
|
|
<div id="action-tooltip" class="action-tooltip-floating hidden" role="tooltip"></div>
|
|
|
|
<main id="main-content" class="app-main" tabindex="-1">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<footer class="footer app-footer">
|
|
{% block footer %}
|
|
<div class="footer-inner">
|
|
<p class="muted footer-line">Данные: том <code>clusters/</code> на хосте</p>
|
|
<p class="footer-copyright">
|
|
© {{ app_title }} ·
|
|
<a href="https://devops.org.ru" target="_blank" rel="noopener">devops.org.ru</a>
|
|
</p>
|
|
</div>
|
|
{% endblock %}
|
|
</footer>
|
|
|
|
{# Отдельное окно для Swagger/ReDoc/Health; выпадающее меню API. #}
|
|
<script>
|
|
(function () {
|
|
var opts = "noopener,noreferrer,width=1240,height=840,scrollbars=yes,resizable=yes";
|
|
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();
|
|
});
|
|
});
|
|
|
|
/* Закрытие по клику вне блока API (фаза capture: не конфликтует с открытием по кнопке). */
|
|
document.addEventListener(
|
|
"click",
|
|
function (e) {
|
|
if (!dd.contains(e.target)) closeApiMenu();
|
|
},
|
|
true,
|
|
);
|
|
|
|
document.addEventListener("keydown", function (e) {
|
|
if (e.key === "Escape") closeApiMenu();
|
|
});
|
|
})();
|
|
</script>
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|