feat(webui/loading): shared boxed spinner loader for page/panel loading states
Pages and panels showed inconsistent loading states: the Backup center and several admin pages (System, SSH, admin Overview), the Overview Migrate/Peers panels, the per-app updater section and the backup engine details modal rendered a bare 'Loading…' text line (updater-empty / backup-empty-state) with no spinner, while Services/Config/Tasks used a boxed card + spinner. Add one shared loader — window.lpLoadingBox(message) + .lp-loading CSS in the core/loading subsystem (the boxed card + accent spinner the good tabs already use) — and route those bare-text loaders through it. The system metric graph keeps its absolute overlay but gains the same spinner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
9e1adef2b8
commit
ab0822c46b
@ -654,6 +654,7 @@ table.sys-apps tr:hover td { background: rgba(var(--text-rgb), 0.03); }
|
||||
.sys-detail-empty {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 8px;
|
||||
color: rgba(var(--text-rgb), 0.45);
|
||||
font-size: 0.92rem;
|
||||
pointer-events: none;
|
||||
|
||||
@ -14,7 +14,7 @@ class OverviewPage {
|
||||
|
||||
async init() {
|
||||
const r = this.root();
|
||||
if (r) r.innerHTML = '<div class="admin-page"><div class="backup-empty-state">Loading…</div></div>';
|
||||
if (r) r.innerHTML = '<div class="admin-page">' + lpLoadingBox() + '</div>';
|
||||
this.bindEvents();
|
||||
const [upd, verify, backup, ssh, disk, mem, info] = await Promise.all([
|
||||
this.fetchJson('/data/system/update_status.json'),
|
||||
|
||||
@ -17,7 +17,7 @@ class SshPage {
|
||||
|
||||
async init() {
|
||||
const r = this.root();
|
||||
if (r) r.innerHTML = '<div class="ssh-page"><div class="backup-empty-state">Loading…</div></div>';
|
||||
if (r) r.innerHTML = '<div class="ssh-page">' + lpLoadingBox() + '</div>';
|
||||
this.bindEvents();
|
||||
await this.refresh();
|
||||
this.render();
|
||||
|
||||
@ -195,7 +195,7 @@ class SystemMetricPage {
|
||||
</section>
|
||||
<section class="sys-metric-canvas">
|
||||
<div class="sys-detail-empty" hidden>No samples in this range yet — check back in a minute.</div>
|
||||
<div class="sys-detail-loading">Loading history…</div>
|
||||
<div class="sys-detail-loading"><span class="lp-loading-spinner" aria-hidden="true"></span><span>Loading history…</span></div>
|
||||
<svg class="sys-detail-svg" preserveAspectRatio="none" aria-hidden="true"></svg>
|
||||
<div class="sys-detail-tooltip" hidden></div>
|
||||
</section>
|
||||
|
||||
@ -84,7 +84,7 @@ class SystemPage {
|
||||
}
|
||||
|
||||
// Default: index view.
|
||||
r.innerHTML = '<div class="admin-page"><div class="backup-empty-state">Loading system stats…</div></div>';
|
||||
r.innerHTML = '<div class="admin-page">' + lpLoadingBox('Loading system stats…') + '</div>';
|
||||
await this.refresh();
|
||||
this.bind();
|
||||
if (window.LiveSystem) {
|
||||
|
||||
@ -586,7 +586,7 @@ class AppTabbedManager {
|
||||
});
|
||||
}
|
||||
|
||||
section.innerHTML = '<div class="updater-empty">Loading…</div>';
|
||||
section.innerHTML = lpLoadingBox();
|
||||
await this.appUpdater.refreshAll();
|
||||
const app = (this.appUpdater.apps || []).find((x) => x.name === this.currentApp);
|
||||
// Always lead with the title block + recessed dark container, so the tab
|
||||
|
||||
@ -235,7 +235,7 @@ class OverviewManager {
|
||||
try { window.migratePage.refreshAll().then(() => window.migratePage.render()).catch(() => {}); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
panel.innerHTML = '<div class="updater-empty">Loading…</div>';
|
||||
panel.innerHTML = lpLoadingBox();
|
||||
try {
|
||||
if (typeof MigratePage === 'undefined' && window.spaClean && window.spaClean.loadScript) {
|
||||
await window.spaClean.loadScript('/components/apps/overview/migrate/js/migrate-page.js');
|
||||
@ -259,7 +259,7 @@ class OverviewManager {
|
||||
try { window.peersPage.refreshAll().then(() => window.peersPage.render()).catch(() => {}); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
panel.innerHTML = '<div class="updater-empty">Loading…</div>';
|
||||
panel.innerHTML = lpLoadingBox();
|
||||
try {
|
||||
if (typeof PeersPage === 'undefined' && window.spaClean && window.spaClean.loadScript) {
|
||||
await window.spaClean.loadScript('/components/admin/peers/js/peers-page.js');
|
||||
@ -616,7 +616,7 @@ class OverviewManager {
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
pane.innerHTML = '<div class="updater-empty">Loading backup center…</div>';
|
||||
pane.innerHTML = lpLoadingBox('Loading backup center…');
|
||||
this._loadBackupCenter(pane, sub);
|
||||
}
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ Object.assign(BackupPage.prototype, {
|
||||
const row = triggerEl?.closest('.backup-engine-input-row');
|
||||
const sel = row?.querySelector('select, input');
|
||||
if (sel && sel.value) engineId = sel.value.trim();
|
||||
body.innerHTML = `<div class="backup-empty-state">Loading engine details…</div>`;
|
||||
body.innerHTML = lpLoadingBox('Loading engine details…');
|
||||
modal.classList.add('open');
|
||||
|
||||
const data = await this.fetchJson(`/data/backup/generated/engines/${encodeURIComponent(engineId)}.json?t=${Date.now()}`);
|
||||
|
||||
@ -539,3 +539,52 @@
|
||||
.error-list-container li {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Shared in-page section loader (window.lpLoadingBox()).
|
||||
|
||||
One consistent "boxed card + spinner + text" used everywhere a
|
||||
page or panel is fetching data, so every loading state matches
|
||||
instead of some showing a bare text line with no spinner. Mirrors
|
||||
the boxed loading card the Services/Config/Tasks tabs already use.
|
||||
`@keyframes spin` is defined globally in core/theme/css/base.css.
|
||||
============================================================ */
|
||||
.lp-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.7rem;
|
||||
padding: 40px 20px;
|
||||
min-height: 180px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(var(--text-rgb), 0.1);
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary, var(--text-muted));
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.lp-loading-spinner {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px solid rgba(var(--text-rgb), 0.15);
|
||||
border-top-color: var(--accent-color, var(--accent));
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.lp-loading-text {
|
||||
line-height: 1.35;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Compact variant for small inline overlays (e.g. metric graphs) — no
|
||||
box, just a centered spinner + text laid out in a row. */
|
||||
.lp-loading.lp-loading-inline {
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@ -398,3 +398,17 @@ class LoadingUI {
|
||||
this.systemCards.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Shared in-page section loader markup. Returns the HTML for one consistent
|
||||
// "boxed card + spinner + text" so every page/panel that's fetching data
|
||||
// shows the same loading state (see core/loading/css/loading-screen.css).
|
||||
// el.innerHTML = lpLoadingBox(); // default "Loading…"
|
||||
// el.innerHTML = lpLoadingBox('Loading apps…'); // custom message
|
||||
// el.innerHTML = lpLoadingBox('Loading…', { inline: true }); // compact, no box
|
||||
window.lpLoadingBox = function (message = 'Loading…', opts = {}) {
|
||||
const cls = `lp-loading${opts.inline ? ' lp-loading-inline' : ''}${opts.className ? ` ${opts.className}` : ''}`;
|
||||
return `<div class="${cls}" role="status" aria-live="polite">`
|
||||
+ `<div class="lp-loading-spinner" aria-hidden="true"></div>`
|
||||
+ `<div class="lp-loading-text">${message}</div>`
|
||||
+ `</div>`;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user