Add loading indicator while enabling a plugin

This commit is contained in:
Raven Scott
2025-12-19 23:02:51 -05:00
parent 3275f937e8
commit ed2e3c8b5a
3 changed files with 124 additions and 11 deletions
+37
View File
@@ -1217,3 +1217,40 @@ dialog button.bg-primary:hover {
border-color: rgba(99, 102, 241, 0.7); border-color: rgba(99, 102, 241, 0.7);
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.3), 0 4px 6px -2px rgba(99, 102, 241, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2); box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.3), 0 4px 6px -2px rgba(99, 102, 241, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
/* Plugin loading states */
.plugin-card.plugin-loading {
opacity: 0.8;
pointer-events: none;
}
.plugin-toggle-container.loading {
animation: pulse-loading 1.5s ease-in-out infinite;
}
.plugin-toggle-switch.loading {
opacity: 0.7;
cursor: not-allowed !important;
}
.plugin-toggle-switch.loading::after {
animation: spin 1s linear infinite;
}
@keyframes pulse-loading {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
+74 -6
View File
@@ -230,14 +230,17 @@ function renderPluginCard(plugin) {
plugin.hasDatabase ? '<span class="text-xs bg-orange-500 px-2 py-1 rounded" style="color: var(--text-primary);">Database</span>' : '' plugin.hasDatabase ? '<span class="text-xs bg-orange-500 px-2 py-1 rounded" style="color: var(--text-primary);">Database</span>' : ''
].filter(Boolean).join(''); ].filter(Boolean).join('');
const isLoading = pluginLoadingStates.get(plugin.domain) || false;
return ` return `
<div class="theme-card p-6 plugin-card" data-plugin-domain="${plugin.domain}"> <div class="theme-card p-6 plugin-card ${isLoading ? 'plugin-loading' : ''}" data-plugin-domain="${plugin.domain}">
<div class="flex justify-between items-start mb-4"> <div class="flex justify-between items-start mb-4">
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-4 mb-2"> <div class="flex items-center gap-4 mb-2">
<h3 class="text-xl font-bold theme-text-primary flex items-center gap-2"> <h3 class="text-xl font-bold theme-text-primary flex items-center gap-2">
${plugin.icon ? `<i class="fa-solid fa-${escapeHtml(plugin.icon)}"></i>` : ''} ${plugin.icon ? `<i class="fa-solid fa-${escapeHtml(plugin.icon)}"></i>` : ''}
${escapeHtml(plugin.name)} ${escapeHtml(plugin.name)}
${isLoading ? '<span class="ml-2 text-sm animate-spin" style="color: var(--primary);">⟳</span>' : ''}
</h3> </h3>
<div class="ml-2"> <div class="ml-2">
${statusBadge} ${statusBadge}
@@ -251,7 +254,7 @@ function renderPluginCard(plugin) {
${featuresHtml ? `<div class="flex gap-2 mt-2">${featuresHtml}</div>` : ''} ${featuresHtml ? `<div class="flex gap-2 mt-2">${featuresHtml}</div>` : ''}
</div> </div>
<div class="flex flex-col gap-2 items-end"> <div class="flex flex-col gap-2 items-end">
<div class="flex items-center gap-2 theme-glass px-3 py-2 rounded-lg"> <div class="flex items-center gap-2 theme-glass px-3 py-2 rounded-lg plugin-toggle-container ${isLoading ? 'loading' : ''}">
<span class="text-xs font-semibold theme-text-secondary uppercase tracking-wide">Status</span> <span class="text-xs font-semibold theme-text-secondary uppercase tracking-wide">Status</span>
${(['p2ns.admin', 'global.profile'].includes(plugin.domain) ? ` ${(['p2ns.admin', 'global.profile'].includes(plugin.domain) ? `
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -264,17 +267,18 @@ function renderPluginCard(plugin) {
</span> </span>
</div> </div>
` : ` ` : `
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center ${isLoading ? 'cursor-not-allowed' : 'cursor-pointer'}">
<input <input
type="checkbox" type="checkbox"
class="sr-only peer" class="sr-only peer"
${plugin.enabled !== false ? 'checked' : ''} ${plugin.enabled !== false ? 'checked' : ''}
${isLoading ? 'disabled' : ''}
onchange="togglePluginEnabled('${plugin.domain}', this.checked)" onchange="togglePluginEnabled('${plugin.domain}', this.checked)"
id="toggle-${plugin.domain}" id="toggle-${plugin.domain}"
> >
<div class="w-11 h-6 rounded-full peer peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:rounded-full after:h-5 after:w-5 after:transition-all plugin-toggle-switch" style="background: var(--bg-glass); border: 1px solid var(--border-color); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);"></div> <div class="w-11 h-6 rounded-full peer peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:rounded-full after:h-5 after:w-5 after:transition-all plugin-toggle-switch ${isLoading ? 'loading' : ''}" style="background: var(--bg-glass); border: 1px solid var(--border-color); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);"></div>
<span class="ml-3 text-sm font-medium theme-text-primary min-w-[70px]"> <span class="ml-3 text-sm font-medium theme-text-primary min-w-[70px] plugin-status-text">
${plugin.enabled !== false ? '<span style="color: var(--success);">Enabled</span>' : '<span style="color: var(--error);">Disabled</span>'} ${isLoading ? '<span style="color: var(--primary);">Loading...</span>' : (plugin.enabled !== false ? '<span style="color: var(--success);">Enabled</span>' : '<span style="color: var(--error);">Disabled</span>')}
</span> </span>
</label> </label>
`)} `)}
@@ -666,6 +670,9 @@ async function togglePluginEnabled(domain, enabled) {
return; return;
} }
// Set loading state
setPluginLoadingState(domain, true);
try { try {
const res = await fetch(`/api/plugins/${domain}/toggle`, { const res = await fetch(`/api/plugins/${domain}/toggle`, {
method: 'POST', method: 'POST',
@@ -697,6 +704,9 @@ async function togglePluginEnabled(domain, enabled) {
} }
// Refresh to reset toggle state // Refresh to reset toggle state
await renderPlugins(); await renderPlugins();
} finally {
// Clear loading state
setPluginLoadingState(domain, false);
} }
} }
@@ -823,6 +833,63 @@ function filterPlugins() {
}); });
} }
// Plugin loading state management
const pluginLoadingStates = new Map();
// Set loading state for a plugin
function setPluginLoadingState(domain, loading) {
if (loading) {
pluginLoadingStates.set(domain, true);
} else {
pluginLoadingStates.delete(domain);
}
// Update UI immediately
const card = document.querySelector(`.plugin-card[data-plugin-domain="${domain}"]`);
if (card) {
const toggleContainer = card.querySelector('.plugin-toggle-container');
const toggleSwitch = card.querySelector('.plugin-toggle-switch');
const statusText = card.querySelector('.plugin-status-text');
if (loading) {
// Add loading class to card
card.classList.add('plugin-loading');
// Update toggle switch appearance
if (toggleContainer) {
toggleContainer.classList.add('loading');
}
if (toggleSwitch) {
toggleSwitch.classList.add('loading');
}
// Update status text to show loading
if (statusText) {
const originalText = statusText.textContent;
statusText.setAttribute('data-original-text', originalText);
statusText.innerHTML = '<span style="color: var(--primary);">Loading...</span>';
}
} else {
// Remove loading class from card
card.classList.remove('plugin-loading');
// Reset toggle switch appearance
if (toggleContainer) {
toggleContainer.classList.remove('loading');
}
if (toggleSwitch) {
toggleSwitch.classList.remove('loading');
}
// Restore original status text
if (statusText && statusText.hasAttribute('data-original-text')) {
statusText.innerHTML = statusText.getAttribute('data-original-text');
statusText.removeAttribute('data-original-text');
}
}
}
}
// Make functions globally available // Make functions globally available
window.renderPlugins = renderPlugins; window.renderPlugins = renderPlugins;
window.executeAction = executeAction; window.executeAction = executeAction;
@@ -834,5 +901,6 @@ window.filterPlugins = filterPlugins;
window.initializePluginTerminal = initializePluginTerminal; window.initializePluginTerminal = initializePluginTerminal;
window.cleanupPluginTerminal = cleanupPluginTerminal; window.cleanupPluginTerminal = cleanupPluginTerminal;
window.showPluginLogs = showPluginLogs; window.showPluginLogs = showPluginLogs;
window.setPluginLoadingState = setPluginLoadingState;
@@ -138,6 +138,14 @@ function connectWebSocket() {
if (window.renderStats) window.renderStats(); if (window.renderStats) window.renderStats();
return; return;
} }
if (data.type === 'update-plugins' && window.activeTab === 'plugins') {
// Clear loading state for the updated plugin
if (window.setPluginLoadingState) {
window.setPluginLoadingState(data.domain, false);
}
if (window.renderPlugins) window.renderPlugins();
return;
}
if (data.type === 'update-settings' && window.activeTab === 'settings') { if (data.type === 'update-settings' && window.activeTab === 'settings') {
if (window.fetchSubnets) window.fetchSubnets(); if (window.fetchSubnets) window.fetchSubnets();
if (window.genericFetch) window.genericFetch('settings', true); if (window.genericFetch) window.genericFetch('settings', true);