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