Use tray spinner for pending UI loading states
Release rolling / release (push) Has been cancelled

Replace em-dash and ad-hoc loaders with the shared job-tray spinner
for deploy templates, host/swarm/tunnels/events, health latency,
container stats/logs/processes, and registry/volume browsers.
This commit is contained in:
Raven Scott
2026-07-16 14:30:59 -04:00
parent 0a758a1051
commit 5b1acdf324
7 changed files with 211 additions and 52 deletions
+68 -19
View File
@@ -21,7 +21,16 @@ import {
import { initAddContainerPage } from './libs/addContainer.js';
import { initRegistryManager, openPushImageModal, pullImageWithAuth } from './libs/registryManager.js';
import { showContainerSkeleton, createProgressBar, updateProgressBar, removeProgressBar } from './libs/loadingStates.js';
import { closeAllModals, showStatusIndicator, hideStatusIndicator, updateStatusIndicator, showAlert } from './libs/uiUtils.js';
import {
closeAllModals,
showStatusIndicator,
hideStatusIndicator,
updateStatusIndicator,
showAlert,
jobSpinnerHtml,
jobSpinnerPending,
jobSpinnerLoadingBlock,
} from './libs/uiUtils.js';
import notificationManager from './libs/notifications.js';
import { initOpsApp } from './ui/ops-app.js';
import { presentError, formatResponseError, isBackgroundMethod } from './client/errors.js';
@@ -2071,12 +2080,21 @@ function updateHealthBadge(info, conn) {
const docker = info?.docker ?? conn?.dockerHealth;
const status = info?.status ?? conn?.healthStatus ?? 'unknown';
const role = conn?.role || info?.role || null;
const pending = conn?.connected || status === 'connecting' || status === 'unknown';
lat.textContent = latency != null ? `${latency} ms` : '—';
if (latency != null) {
lat.textContent = `${latency} ms`;
} else if (pending) {
lat.innerHTML = jobSpinnerPending();
} else {
lat.textContent = '—';
}
if (docker?.ok === true) {
dock.textContent = `Docker ${docker.apiVersion || 'ok'}`;
} else if (docker?.ok === false) {
dock.textContent = 'Docker down';
} else if (pending) {
dock.innerHTML = `Docker ${jobSpinnerPending()}`;
} else {
dock.textContent = 'Docker —';
}
@@ -2427,8 +2445,7 @@ function loadContainerTop(containerId) {
containerTopState.containerId = containerId;
const el = document.getElementById('container-top-content');
if (el) {
el.innerHTML =
'<div class="procs-loading"><i class="fas fa-circle-notch fa-spin"></i> Loading processes…</div>';
el.innerHTML = `<div class="procs-loading">${jobSpinnerHtml('Loading processes…')}</div>`;
}
updateProcsResultCount(null, null);
sendCommand('containerTop', { id: containerId });
@@ -5664,7 +5681,7 @@ function populateNetworkingTab(config) {
if (!joinPanel) return;
joinPanel.hidden = false;
if (joinSelect) {
joinSelect.innerHTML = '<option value="">Loading…</option>';
joinSelect.innerHTML = '<option value="">Loading networks…</option>';
try {
const res = await manager.request(Methods.listNetworks, {});
const list = res?.data || res?.networks || [];
@@ -6007,6 +6024,18 @@ function updateContainerDetailsStats(container) {
ensureDetailsStatsWired();
detailsStatsState.containerId = container.Id;
ensureHistorySeries(container.Id);
// Pending summary chips until history/live samples arrive
for (const id of [
'detail-stats-window',
'detail-stats-cpu-p95',
'detail-stats-mem-peak',
'detail-stats-updated',
]) {
const el = document.getElementById(id);
if (el && (!el.textContent || el.textContent === '—' || el.querySelector?.('.job-spinner'))) {
el.innerHTML = jobSpinnerPending();
}
}
updateContainerDetailsStatsLive(container.Id);
fetchServerStatsHistory(container.Id, true);
}
@@ -6428,8 +6457,7 @@ async function startDetailsLogs(containerId) {
btn.classList.toggle('active', btn.dataset.filter === 'all');
});
logsContent.innerHTML =
'<div class="text-muted text-center logs-loading">Loading logs...</div>';
logsContent.innerHTML = `<div class="text-muted text-center logs-loading">${jobSpinnerHtml('Loading logs…')}</div>`;
const handler = (logData) => appendDetailsLogChunk(logData, logsContent, gen);
handler._pdDetailsLogs = true;
@@ -7788,9 +7816,18 @@ function setDeployTemplates(templates, stats) {
}
window.__peardockSetDeployTemplates = setDeployTemplates;
/** Show tray-style spinner in the template count line while lists load. */
function setDeployTemplateCountLoading(message = 'Loading templates…') {
const countEl = document.getElementById('deploy-template-count');
if (!countEl) return;
countEl.classList.add('d-flex', 'align-items-center', 'gap-2', 'deploy-template-count--loading');
countEl.innerHTML = jobSpinnerHtml(message);
}
function updateDeployTemplateCount(query = '') {
const countEl = document.getElementById('deploy-template-count');
if (!countEl) return;
countEl.classList.remove('deploy-template-count--loading');
const total = deployViewTemplates.length;
const q = String(query || '').trim();
const filtered = q ? filterTemplatesByQuery(deployViewTemplates, q).length : total;
@@ -7856,11 +7893,12 @@ async function loadDeployView() {
});
}
// Show loading state only when we don't have cache
// Show tray-style loading state only when we don't have cache
if (!deployViewTemplates.length) {
const n = getTemplateListUrls().length;
templateListContainer.innerHTML =
`<div class="text-center text-muted py-4"><i class="fas fa-spinner fa-spin me-2"></i>Loading templates from ${n} list${n === 1 ? '' : 's'}…</div>`;
const msg = `Loading templates from ${n} list${n === 1 ? '' : 's'}`;
setDeployTemplateCountLoading(msg);
templateListContainer.innerHTML = jobSpinnerLoadingBlock(msg);
}
try {
@@ -7881,6 +7919,11 @@ async function loadDeployView() {
updateDeployTemplateCount(query);
} catch (error) {
console.error('[ERROR] Failed to fetch templates:', error.message);
const countEl = document.getElementById('deploy-template-count');
if (countEl) {
countEl.classList.remove('deploy-template-count--loading');
countEl.textContent = 'Failed to load templates';
}
templateListContainer.innerHTML =
'<div class="text-center text-danger py-4"><i class="fas fa-exclamation-triangle me-2"></i>Failed to load templates. Check network access and Settings → Deploy template lists.</div>';
}
@@ -10176,9 +10219,12 @@ function applyStatsToRow(row, stats) {
const ipEl = row.querySelector('.ip-address');
const cpuText = formatCpuDisplay(stats.cpu);
if (cpuEl && cpuEl.textContent !== cpuText) {
cpuEl.textContent = cpuText;
cpuEl.title = cpuText;
if (cpuEl) {
// Clear pending spinner HTML when real stats arrive
if (cpuEl.querySelector?.('.job-spinner') || cpuEl.textContent !== cpuText) {
cpuEl.textContent = cpuText;
cpuEl.title = cpuText;
}
}
if (cpuBar) {
// Bar is % of one full core-equivalent up to 100% (cap display)
@@ -10189,10 +10235,12 @@ function applyStatsToRow(row, stats) {
}
const memText = formatMemDisplay(stats.memory);
if (memoryEl && memoryEl.textContent !== memText) {
memoryEl.textContent = memText;
const lim = stats.memoryLimit ? ` / ${formatMemDisplay(stats.memoryLimit)}` : '';
memoryEl.title = `${memText}${lim}`;
if (memoryEl) {
if (memoryEl.querySelector?.('.job-spinner') || memoryEl.textContent !== memText) {
memoryEl.textContent = memText;
const lim = stats.memoryLimit ? ` / ${formatMemDisplay(stats.memoryLimit)}` : '';
memoryEl.title = `${memText}${lim}`;
}
}
if (memoryBar) {
const memoryPercent = memoryBarPercent(stats.memory, stats.memoryLimit);
@@ -10493,8 +10541,9 @@ function buildContainerRow(container) {
const state = container.State || 'Unknown';
const statusClass = containerStatusClass(state);
const prior = smoothedStats[containerId];
const cpuText = prior ? formatCpuDisplay(prior.cpu) : '—';
const memText = prior ? formatMemDisplay(prior.memory) : '—';
// Pending first stats sample → tray spinner (not em-dash)
const cpuText = prior ? formatCpuDisplay(prior.cpu) : jobSpinnerPending();
const memText = prior ? formatMemDisplay(prior.memory) : jobSpinnerPending();
const cpuWidth = prior ? Math.min(100, Math.max(0, prior.cpu || 0)) : 0;
const memWidth = prior
? memoryBarPercent(prior.memory, prior.memoryLimit)
+19 -15
View File
@@ -53,8 +53,8 @@
</div>
<div id="connection-health-badge" class="connection-health-badge" title="Connection health">
<span class="health-dot health-dot--unknown" id="health-dot"></span>
<span id="health-latency" class="health-latency"></span>
<span id="health-docker" class="health-docker">Docker </span>
<span id="health-latency" class="health-latency"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span></span>
<span id="health-docker" class="health-docker">Docker <span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span></span>
<span id="health-role" class="health-role"></span>
</div>
<!-- Recent activity (job history) -->
@@ -451,10 +451,14 @@
<div class="d-flex align-items-center gap-2 mb-2">
<input type="search" id="deploy-template-search-input" class="form-control bg-dark text-white" placeholder="Search templates (debounced)…" autocomplete="off" spellcheck="false">
</div>
<div class="small text-muted mb-2" id="deploy-template-count"></div>
<div class="small text-muted mb-2 deploy-template-count d-flex align-items-center gap-2" id="deploy-template-count">
<span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span>
<span>Loading templates…</span>
</div>
<div id="deploy-template-list-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; min-height: 0;">
<div class="text-center text-muted py-4">
<i class="fas fa-spinner fa-spin me-2"></i>Loading templates...
<div class="text-center text-muted py-4 d-flex align-items-center justify-content-center gap-2">
<span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span>
<span>Loading templates…</span>
</div>
</div>
</div>
@@ -1972,10 +1976,10 @@
<div class="detail-stats-summary" id="detail-stats-summary">
<div class="detail-stats-sum-item"><span>Samples</span><strong id="detail-stats-samples">0</strong></div>
<div class="detail-stats-sum-item"><span>Window</span><strong id="detail-stats-window"></strong></div>
<div class="detail-stats-sum-item"><span>CPU p95</span><strong id="detail-stats-cpu-p95"></strong></div>
<div class="detail-stats-sum-item"><span>Mem peak</span><strong id="detail-stats-mem-peak"></strong></div>
<div class="detail-stats-sum-item"><span>Updated</span><strong id="detail-stats-updated"></strong></div>
<div class="detail-stats-sum-item"><span>Window</span><strong id="detail-stats-window"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span></strong></div>
<div class="detail-stats-sum-item"><span>CPU p95</span><strong id="detail-stats-cpu-p95"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span></strong></div>
<div class="detail-stats-sum-item"><span>Mem peak</span><strong id="detail-stats-mem-peak"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span></strong></div>
<div class="detail-stats-sum-item"><span>Updated</span><strong id="detail-stats-updated"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span></strong></div>
</div>
<div id="stats-charts-container" class="detail-stats-charts">
@@ -2189,7 +2193,7 @@
<div class="table-responsive">
<table class="table table-dark table-hover table-sm">
<thead><tr><th>Name</th><th>Image</th><th>Replicas</th><th>ID</th><th></th></tr></thead>
<tbody id="swarm-services-body"><tr><td colspan="5" class="text-muted"></td></tr></tbody>
<tbody id="swarm-services-body"><tr class="pd-loading-row"><td colspan="5" class="text-muted"><span class="pd-job-loading d-inline-flex align-items-center gap-2"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span><span>Loading…</span></span></td></tr></tbody>
</table>
</div>
</div>
@@ -2197,7 +2201,7 @@
<div class="table-responsive">
<table class="table table-dark table-hover table-sm">
<thead><tr><th>Hostname</th><th>Role</th><th>Status</th><th>Availability</th><th>ID</th></tr></thead>
<tbody id="swarm-nodes-body"><tr><td colspan="5" class="text-muted"></td></tr></tbody>
<tbody id="swarm-nodes-body"><tr class="pd-loading-row"><td colspan="5" class="text-muted"><span class="pd-job-loading d-inline-flex align-items-center gap-2"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span><span>Loading…</span></span></td></tr></tbody>
</table>
</div>
</div>
@@ -2205,7 +2209,7 @@
<div class="table-responsive">
<table class="table table-dark table-hover table-sm">
<thead><tr><th>Service</th><th>Node</th><th>Desired</th><th>Current</th><th>ID</th></tr></thead>
<tbody id="swarm-tasks-body"><tr><td colspan="5" class="text-muted"></td></tr></tbody>
<tbody id="swarm-tasks-body"><tr class="pd-loading-row"><td colspan="5" class="text-muted"><span class="pd-job-loading d-inline-flex align-items-center gap-2"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span><span>Loading…</span></span></td></tr></tbody>
</table>
</div>
</div>
@@ -2221,7 +2225,7 @@
<div class="table-responsive">
<table class="table table-dark table-hover table-sm">
<thead><tr><th>Name</th><th>Created</th><th>ID</th></tr></thead>
<tbody id="swarm-secrets-body"><tr><td colspan="3" class="text-muted"></td></tr></tbody>
<tbody id="swarm-secrets-body"><tr class="pd-loading-row"><td colspan="3" class="text-muted"><span class="pd-job-loading d-inline-flex align-items-center gap-2"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span><span>Loading…</span></span></td></tr></tbody>
</table>
</div>
</div>
@@ -2237,7 +2241,7 @@
<div class="table-responsive">
<table class="table table-dark table-hover table-sm">
<thead><tr><th>Name</th><th>Created</th><th>ID</th></tr></thead>
<tbody id="swarm-configs-body"><tr><td colspan="3" class="text-muted"></td></tr></tbody>
<tbody id="swarm-configs-body"><tr class="pd-loading-row"><td colspan="3" class="text-muted"><span class="pd-job-loading d-inline-flex align-items-center gap-2"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span><span>Loading…</span></span></td></tr></tbody>
</table>
</div>
</div>
@@ -2489,7 +2493,7 @@ services:
<div class="col-lg-6">
<div class="settings-section">
<h3>Metrics</h3>
<div id="host-metrics-summary" class="small text-muted"></div>
<div id="host-metrics-summary" class="small text-muted"><span class="pd-job-loading d-inline-flex align-items-center gap-2"><span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span><span>Waiting for peer…</span></span></div>
</div>
</div>
<div class="col-12">
+9 -4
View File
@@ -4,7 +4,12 @@
*/
import { manager, Methods } from '../client/manager.js'
import { presentError } from '../client/errors.js'
import { showAlert, showStatusIndicator, hideStatusIndicator } from './uiUtils.js'
import {
showAlert,
showStatusIndicator,
hideStatusIndicator,
jobSpinnerHtml,
} from './uiUtils.js'
import {
createProgressBar,
updateProgressBar,
@@ -431,8 +436,8 @@ export async function loadRegistryCatalog(opts = {}) {
return
}
if (status) {
status.textContent = 'Loading catalog…'
status.className = 'small text-muted mb-2'
status.className = 'small text-muted mb-2 d-flex align-items-center gap-2'
status.innerHTML = jobSpinnerHtml('Loading catalog…')
}
try {
const args = {
@@ -572,7 +577,7 @@ export async function openRepository(repo) {
const empty = document.getElementById('registry-tags-empty')
if (empty) {
empty.classList.remove('d-none')
empty.textContent = `Loading tags for ${name}`
empty.innerHTML = jobSpinnerHtml(`Loading tags for ${name}`)
}
try {
+10 -3
View File
@@ -1,5 +1,12 @@
// Import dependencies first (ES6 imports must be at top)
import { closeAllModals, showStatusIndicator, hideStatusIndicator, updateStatusIndicator, showAlert } from './uiUtils.js';
import {
closeAllModals,
showStatusIndicator,
hideStatusIndicator,
updateStatusIndicator,
showAlert,
jobSpinnerLoadingBlock,
} from './uiUtils.js';
import { fetchMergedTemplates, getTemplateListUrls } from '../client/templateLists.js';
import {
isStackTemplate,
@@ -1439,8 +1446,8 @@ async function loadDirectoryContents(path) {
return;
}
// Show loading
fileBrowserContent.innerHTML = '<div class="text-center p-4"><i class="fas fa-spinner fa-spin"></i> Loading...</div>';
// Show tray-style loading
fileBrowserContent.innerHTML = jobSpinnerLoadingBlock('Loading…');
// Update breadcrumb
if (fileBrowserBreadcrumb) {
+63
View File
@@ -421,7 +421,70 @@ function escapeToast(s) {
.replace(/"/g, '&quot;');
}
/**
* Job-tray spinner (same markup as client/jobs.js active step).
* Use for pending / loading UI instead of em-dash "—" placeholders.
*/
export const JOB_SPINNER_HTML =
'<span class="job-spinner" role="status" aria-label="Loading"><span class="job-spinner-ring"></span></span>';
/**
* @param {string} [label]
* @param {{ center?: boolean, className?: string }} [opts]
* @returns {string}
*/
export function jobSpinnerHtml(label = 'Loading…', opts = {}) {
const cls = [
'pd-job-loading',
'd-inline-flex',
'align-items-center',
'gap-2',
opts.center ? 'justify-content-center' : '',
opts.className || '',
]
.filter(Boolean)
.join(' ');
if (!label) {
return `<span class="${cls}">${JOB_SPINNER_HTML}</span>`;
}
const safe = String(label)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
return `<span class="${cls}">${JOB_SPINNER_HTML}<span>${safe}</span></span>`;
}
/**
* Compact spinner for table cells / metric chips (no label).
* @returns {string}
*/
export function jobSpinnerPending() {
return JOB_SPINNER_HTML;
}
/**
* Loading row for table bodies.
* @param {number} colspan
* @param {string} [label]
* @returns {string}
*/
export function jobSpinnerLoadingRow(colspan, label = 'Loading…') {
return `<tr class="pd-loading-row"><td colspan="${colspan}" class="text-muted">${jobSpinnerHtml(label)}</td></tr>`;
}
/**
* Centered block for list containers / empty panes.
* @param {string} [label]
* @returns {string}
*/
export function jobSpinnerLoadingBlock(label = 'Loading…') {
return `<div class="text-center text-muted py-4 d-flex align-items-center justify-content-center gap-2">${jobSpinnerHtml(label)}</div>`;
}
// Bridge for jobs.js (avoids circular import) — mark feedback when jobs complete
if (typeof globalThis !== 'undefined') {
globalThis.__peardockMarkFeedback = markFeedbackShown
globalThis.__peardockJobSpinnerHtml = jobSpinnerHtml
globalThis.__peardockJobSpinnerPending = jobSpinnerPending
}
+38 -9
View File
@@ -30,6 +30,8 @@ import {
showAlert,
markFeedbackShown,
clearTopToasts,
jobSpinnerHtml,
jobSpinnerLoadingRow,
} from '../libs/uiUtils.js'
import {
warmSnapshot,
@@ -887,7 +889,8 @@ export async function loadHostView(opts = {}) {
if (summary) summary.textContent = 'Not connected'
return
}
if (summary && !silent) summary.innerHTML = '<span class="text-muted">Loading…</span>'
if (summary && !silent) summary.innerHTML = jobSpinnerHtml('Loading host…')
if (metricsEl && !silent) metricsEl.innerHTML = jobSpinnerHtml('Loading metrics…')
try {
const [snap, metrics, sys] = await Promise.all([
getSnapshot({ force: true }),
@@ -915,14 +918,18 @@ export async function loadHostView(opts = {}) {
.then((m) => m.loadSchedulesPanel?.())
.catch(() => {})
}
if (metricsEl && metrics) {
metricsEl.innerHTML = `
if (metricsEl) {
if (metrics) {
metricsEl.innerHTML = `
<div>Uptime: ${Math.round((metrics.uptimeMs || 0) / 1000)}s</div>
<div>RPC total: ${metrics.rpc?.total ?? 0} · errors: ${metrics.rpc?.errors ?? 0}</div>
<div>Latency p50/p99: ${metrics.rpc?.latencyMs?.p50 ?? 0} / ${metrics.rpc?.latencyMs?.p99 ?? 0} ms</div>
<div>RSS: ${formatBytes(metrics.process?.rss)}</div>
<div>Features: swarm=${metrics.features?.swarm} plugins=${metrics.features?.plugins} holesail=${metrics.features?.holesail}</div>
`
} else {
metricsEl.textContent = 'Metrics unavailable'
}
}
if (raw) {
raw.textContent = JSON.stringify(sys?.data || eng.info || {}, null, 2)
@@ -937,6 +944,7 @@ export async function loadEventsView(opts = {}) {
const silent = opts.silent === true
const host = document.getElementById('events-full-list')
if (!host || !manager.active?.connected) return
if (!silent) host.innerHTML = `<div class="text-muted p-3">${jobSpinnerHtml('Loading events…')}</div>`
try {
const res = await manager.request(Methods.getDockerEvents, { timeoutMs: 1500 })
const filter = (document.getElementById('events-filter')?.value || '').toLowerCase()
@@ -991,6 +999,14 @@ export async function loadTunnelsView(opts = {}) {
return
}
if (!silent) {
if (banner) {
banner.className = 'alert alert-secondary small mb-3'
banner.innerHTML = jobSpinnerHtml('Loading tunnels…')
}
if (list) list.innerHTML = `<div class="text-muted small p-2">${jobSpinnerHtml('Loading…')}</div>`
}
try {
const statusRes = await manager.request(Methods.getHolesailStatus, {}).catch(() => null)
const st = statusRes?.status || {}
@@ -1188,6 +1204,24 @@ export async function loadSwarmView(opts = {}) {
}
return
}
const svcBody = document.getElementById('swarm-services-body')
const nodeBody = document.getElementById('swarm-nodes-body')
const taskBody = document.getElementById('swarm-tasks-body')
const secBody = document.getElementById('swarm-secrets-body')
const cfgBody = document.getElementById('swarm-configs-body')
if (!silent) {
if (svcBody) svcBody.innerHTML = jobSpinnerLoadingRow(5, 'Loading services…')
if (nodeBody) nodeBody.innerHTML = jobSpinnerLoadingRow(5, 'Loading nodes…')
if (taskBody) taskBody.innerHTML = jobSpinnerLoadingRow(5, 'Loading tasks…')
if (secBody) secBody.innerHTML = jobSpinnerLoadingRow(3, 'Loading secrets…')
if (cfgBody) cfgBody.innerHTML = jobSpinnerLoadingRow(3, 'Loading configs…')
if (banner) {
banner.className = 'alert alert-secondary small mb-3'
banner.innerHTML = jobSpinnerHtml('Loading Swarm…')
}
}
try {
const inspect = await manager.request(Methods.swarmInspect, {})
if (banner) {
@@ -1202,11 +1236,6 @@ export async function loadSwarmView(opts = {}) {
manager.request(Methods.listSecrets, {}).catch(() => ({ data: [] })),
manager.request(Methods.listConfigs, {}).catch(() => ({ data: [] })),
])
const svcBody = document.getElementById('swarm-services-body')
const nodeBody = document.getElementById('swarm-nodes-body')
const taskBody = document.getElementById('swarm-tasks-body')
const secBody = document.getElementById('swarm-secrets-body')
const cfgBody = document.getElementById('swarm-configs-body')
const svcs = services?.data || []
const nds = nodes?.data || []
const tks = tasks?.data || []
@@ -1493,7 +1522,7 @@ export function loadSettingsView(forceTab) {
if (!c?.connected) {
feat.textContent = 'Connect a peer to inspect feature flags.'
} else {
feat.innerHTML = '<span class="text-muted">Loading…</span>'
feat.innerHTML = jobSpinnerHtml('Loading feature flags…')
manager
.request(Methods.getMetrics, {})
.then((res) => {
+4 -2
View File
@@ -3,7 +3,7 @@
* create, schedules, registry hub search, fleet env filter, stack env file.
*/
import { manager, Methods } from '../client/manager.js'
import { showAlert } from '../libs/uiUtils.js'
import { showAlert, jobSpinnerHtml } from '../libs/uiUtils.js'
import { presentError } from '../client/errors.js'
import { confirmDialog } from './components.js'
@@ -74,7 +74,9 @@ async function refreshVolumeBrowser() {
const list = document.getElementById('volume-browse-list')
const pathEl = document.getElementById('volume-browse-path')
if (pathEl) pathEl.textContent = volumeBrowseState.path
if (list) list.innerHTML = '<div class="list-group-item text-muted">Loading…</div>'
if (list) {
list.innerHTML = `<div class="list-group-item text-muted">${jobSpinnerHtml('Loading…')}</div>`
}
try {
const res = await manager.request(Methods.browseVolume, {
name: volumeBrowseState.name,