Add loading indicator while enabling a plugin
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user