admin-console: fast namespace list + async detail loading per ns

This commit is contained in:
“Naeel”
2026-05-25 10:40:49 +04:00
parent d570a6b8a5
commit 21ab7b9d6b
3 changed files with 111 additions and 28 deletions
+61 -15
View File
@@ -38,6 +38,9 @@
.pill-blue { background: #1e3a5f; color: #60a5fa; }
.err { color: #f87171; font-size: 13px; padding: 16px 0; }
.loader { color: #6b7280; font-size: 13px; padding: 16px 0; }
.cnt { color: #6b7280; font-size: 12px; }
.cnt.loading { animation: pulse 1.5s infinite; }
@keyframes pulse { 0%,100%{opacity:.3} 50%{opacity:1} }
</style>
</head>
<body>
@@ -67,19 +70,28 @@
<script>
const API = '/admin/api';
let TOKEN = '';
let NS_LIST = [];
document.getElementById('loginForm').addEventListener('submit', async e => {
e.preventDefault();
TOKEN = document.getElementById('tokenInput').value.trim();
const r = await fetch(`${API}/namespaces`, { headers: { Authorization: `Bearer ${TOKEN}` } });
if (r.ok) {
document.getElementById('login').style.display = 'none';
document.getElementById('app').style.display = 'block';
showTab('namespaces', document.querySelector('nav button'));
} else {
try {
const r = await fetch(`${API}/namespaces`, { headers: { Authorization: `Bearer ${TOKEN}` } });
if (r.ok) {
const data = await r.json();
NS_LIST = data.namespaces || [];
document.getElementById('login').style.display = 'none';
document.getElementById('app').style.display = 'block';
showTab('namespaces', document.querySelector('nav button'));
} else {
const err = document.getElementById('loginErr');
err.style.display = 'block';
err.textContent = 'Неверный токен';
}
} catch(e) {
const err = document.getElementById('loginErr');
err.style.display = 'block';
err.textContent = 'Неверный токен';
err.textContent = 'Ошибка соединения: ' + e.message;
}
});
@@ -91,6 +103,12 @@
async function loadTab(tab) {
const content = document.getElementById('content');
if (tab === 'namespaces') {
content.innerHTML = renderNamespaces(NS_LIST);
// загружаем CRD-счётчики асинхронно
loadAllDetails();
return;
}
content.innerHTML = '<p class="loader">Загрузка…</p>';
try {
const r = await fetch(`${API}/${tab}`, { headers: { Authorization: `Bearer ${TOKEN}` } });
@@ -103,26 +121,54 @@
}
function renderTab(tab, data) {
if (tab === 'namespaces') return renderNamespaces(data.namespaces);
if (tab === 'usage') return renderUsage(data.usage);
if (tab === 'users') return renderUsers(data.users);
if (tab === 'usage') return renderUsage(data.usage);
if (tab === 'users') return renderUsers(data.users);
return '';
}
function renderNamespaces(rows) {
if (!rows?.length) return '<p class="loader">Нет namespace-ов</p>';
return `<table>
<tr><th>Namespace</th><th>Email</th><th>Функции</th><th>Пакеты</th><th>Создан</th></tr>
${rows.map(n => `<tr>
<tr><th>Namespace</th><th>Email</th><th>Fn</th><th>Pkg</th><th>Env</th><th>HTTP</th><th>Time</th><th>Создан</th></tr>
${rows.map((n,i) => `<tr>
<td><span class="pill pill-blue">${n.name}</span></td>
<td>${n.user_email || '—'}</td>
<td>${n.functions}</td>
<td>${n.packages}</td>
<td class="cnt loading" id="ns${i}-fn">…</td>
<td class="cnt loading" id="ns${i}-pkg">…</td>
<td class="cnt loading" id="ns${i}-env">…</td>
<td class="cnt loading" id="ns${i}-http">…</td>
<td class="cnt loading" id="ns${i}-time">…</td>
<td>${n.created_at ? n.created_at.slice(0,10) : '—'}</td>
</tr>`).join('')}
</table>`;
}
async function loadAllDetails() {
for (let i = 0; i < NS_LIST.length; i++) {
loadDetail(i, NS_LIST[i].name);
}
}
async function loadDetail(idx, name) {
try {
const r = await fetch(`${API}/namespaces/${encodeURIComponent(name)}`, {
headers: { Authorization: `Bearer ${TOKEN}` }
});
if (!r.ok) return;
const d = await r.json();
setCnt(idx, 'fn', d.functions);
setCnt(idx, 'pkg', d.packages);
setCnt(idx, 'env', d.environments);
setCnt(idx, 'http', d.http_triggers);
setCnt(idx, 'time', d.time_triggers);
} catch(e) {}
}
function setCnt(idx, type, val) {
const el = document.getElementById(`ns${idx}-${type}`);
if (el) { el.textContent = val ?? 0; el.classList.remove('loading'); }
}
function renderUsage(rows) {
if (!rows?.length) return '<p class="loader">Нет данных</p>';
const btn = `<div style="margin-bottom:12px">
@@ -144,7 +190,7 @@
function exportCSV() {
const table = document.querySelector('table');
if (!table) return;
let csv = '\uFEFF'; // BOM для Excel
let csv = '\uFEFF';
table.querySelectorAll('tr').forEach(row => {
const cells = [];
row.querySelectorAll('th,td').forEach(c => cells.push('"' + c.textContent.replace(/"/g, '""') + '"'));