admin-console: fast namespace list + async detail loading per ns
This commit is contained in:
+61
-15
@@ -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, '""') + '"'));
|
||||
|
||||
Reference in New Issue
Block a user