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
@@ -1216,4 +1216,41 @@ dialog button.bg-primary:hover {
background: rgba(99, 102, 241, 0.5) !important;
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);
}
/* 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);
}
}
+79 -11
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>' : ''
].filter(Boolean).join('');
const isLoading = pluginLoadingStates.get(plugin.domain) || false;
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-1">
<div class="flex items-center gap-4 mb-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>` : ''}
${escapeHtml(plugin.name)}
${isLoading ? '<span class="ml-2 text-sm animate-spin" style="color: var(--primary);">⟳</span>' : ''}
</h3>
<div class="ml-2">
${statusBadge}
@@ -251,7 +254,7 @@ function renderPluginCard(plugin) {
${featuresHtml ? `<div class="flex gap-2 mt-2">${featuresHtml}</div>` : ''}
</div>
<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>
${(['p2ns.admin', 'global.profile'].includes(plugin.domain) ? `
<div class="flex items-center gap-2">
@@ -264,17 +267,18 @@ function renderPluginCard(plugin) {
</span>
</div>
` : `
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
class="sr-only peer"
<label class="relative inline-flex items-center ${isLoading ? 'cursor-not-allowed' : 'cursor-pointer'}">
<input
type="checkbox"
class="sr-only peer"
${plugin.enabled !== false ? 'checked' : ''}
${isLoading ? 'disabled' : ''}
onchange="togglePluginEnabled('${plugin.domain}', this.checked)"
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>
<span class="ml-3 text-sm font-medium theme-text-primary min-w-[70px]">
${plugin.enabled !== false ? '<span style="color: var(--success);">Enabled</span>' : '<span style="color: var(--error);">Disabled</span>'}
<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] plugin-status-text">
${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>
</label>
`)}
@@ -666,6 +670,9 @@ async function togglePluginEnabled(domain, enabled) {
return;
}
// Set loading state
setPluginLoadingState(domain, true);
try {
const res = await fetch(`/api/plugins/${domain}/toggle`, {
method: 'POST',
@@ -674,9 +681,9 @@ async function togglePluginEnabled(domain, enabled) {
},
body: JSON.stringify({ enabled })
});
const data = await res.json();
if (res.ok && data.success) {
if (window.showNotification) {
window.showNotification(`Plugin "${domain}" ${enabled ? 'enabled' : 'disabled'} successfully`, 'success');
@@ -697,6 +704,9 @@ async function togglePluginEnabled(domain, enabled) {
}
// Refresh to reset toggle state
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
window.renderPlugins = renderPlugins;
window.executeAction = executeAction;
@@ -834,5 +901,6 @@ window.filterPlugins = filterPlugins;
window.initializePluginTerminal = initializePluginTerminal;
window.cleanupPluginTerminal = cleanupPluginTerminal;
window.showPluginLogs = showPluginLogs;
window.setPluginLoadingState = setPluginLoadingState;
@@ -138,6 +138,14 @@ function connectWebSocket() {
if (window.renderStats) window.renderStats();
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 (window.fetchSubnets) window.fetchSubnets();
if (window.genericFetch) window.genericFetch('settings', true);