show loading indicators when actions are pending

This commit is contained in:
Raven Scott
2025-12-17 20:42:00 -05:00
parent b5eaf9993d
commit a977320bfc
7 changed files with 337 additions and 222 deletions
@@ -56,9 +56,12 @@ async function handleDomainsRoutes(req, res) {
} catch (err) {
// Fallback if plugin system not available
}
// Only add internal domains that aren't already in the resolved list
for (const d of internalDomains) {
if (!resolved.some(r => r.domain === d)) {
resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true });
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(resolved.sort((a, b) => a.domain.localeCompare(b.domain))));
} catch (err) {
+6 -6
View File
@@ -337,7 +337,7 @@
Settings
<button onclick="openInfoModal('settings')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
</h2>
<button onclick="saveSettings()" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Save Settings</button>
<button id="save-settings-btn" onclick="saveSettings(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Save Settings</button>
</div>
<div class="mb-6">
<input id="search-settings" type="text" placeholder="Search settings..." class="w-full p-3 rounded-lg theme-input focus:outline-none focus:ring-2 focus:ring-primary" oninput="filterSettings()">
@@ -380,7 +380,7 @@
</table>
</div>
<div id="backupsPagination" class="flex justify-center mt-4 space-x-2 flex-shrink-0"></div>
<button onclick="createBackup()" class="mt-4 px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover flex-shrink-0">Create Backup</button>
<button id="create-backup-btn" onclick="createBackup(this)" class="mt-4 px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover flex-shrink-0">Create Backup</button>
</div>
<div id="stats" class="tab-content hidden">
@@ -916,7 +916,7 @@
<input id="local-ttl" type="number" placeholder="TTL" value="3600" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('localDnsModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
<button id="local-submit" onclick="submitLocalDns()" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
<button id="local-submit" onclick="submitLocalDns(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
</div>
</dialog>
@@ -1063,7 +1063,7 @@
</button>
<button
id="reset-identity-submit"
onclick="resetIdentity()"
onclick="resetIdentity(this)"
disabled
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
@@ -1101,7 +1101,7 @@
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('subnetModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
<button id="subnet-submit" onclick="submitSubnet()" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
<button id="subnet-submit" onclick="submitSubnet(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
</div>
</dialog>
@@ -1110,7 +1110,7 @@
<input id="dns-server-ip" placeholder="DNS Server IP (e.g., 1.1.1.1)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('dnsServerModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
<button id="dns-server-submit" onclick="submitDnsServer()" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
<button id="dns-server-submit" onclick="submitDnsServer(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
</div>
</dialog>
</div>
+32 -9
View File
@@ -133,8 +133,8 @@ function loadBackupsBatch() {
<td class="p-3">${backup.version || 'unknown'}</td>
<td class="p-3">
<button onclick="viewBackupDetails('${backup.name}')" class="px-2 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 mr-2">Details</button>
<button onclick="restoreBackup('${backup.name}')" class="px-2 py-1 bg-green-500 text-white rounded hover:bg-green-600 mr-2">Restore</button>
<button onclick="deleteBackup('${backup.name}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>
<button id="restore-btn-${backup.name.replace(/[^a-zA-Z0-9]/g, '-')}" onclick="restoreBackup('${backup.name}', this)" class="px-2 py-1 bg-green-500 text-white rounded hover:bg-green-600 mr-2">Restore</button>
<button id="delete-btn-${backup.name.replace(/[^a-zA-Z0-9]/g, '-')}" onclick="deleteBackup('${backup.name}', this)" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>
</td>
`;
container.appendChild(tr);
@@ -201,9 +201,15 @@ function setupBackupsObserver(container) {
// Pagination function removed - using infinite scroll instead
// Create backup
async function createBackup() {
async function createBackup(buttonElement) {
if (!buttonElement) return;
if (window.showConfirm) {
window.showConfirm('Create a new backup? This may take a moment.', async () => {
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Creating... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/backups/create', {
method: 'POST'
@@ -217,15 +223,22 @@ async function createBackup() {
} catch (err) {
console.error('Failed to create backup:', err);
if (window.showNotification) window.showNotification('Failed to create backup: ' + err.message, 'error');
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
});
}
}
// Restore backup
async function restoreBackup(backupName) {
if (window.showConfirm) {
window.showConfirm(`Restore from backup "${backupName}"? This will create a backup of current state first, then restore.`, async () => {
async function restoreBackup(backupName, buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Restoring... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/backups/restore', {
method: 'POST',
@@ -249,15 +262,22 @@ async function restoreBackup(backupName) {
} catch (err) {
console.error('Failed to restore backup:', err);
if (window.showNotification) window.showNotification('Failed to restore backup: ' + err.message, 'error');
}
});
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
// Delete backup
async function deleteBackup(backupName) {
async function deleteBackup(backupName, buttonElement) {
if (!buttonElement) return;
if (window.showConfirm) {
window.showConfirm(`Delete backup "${backupName}"? This action cannot be undone.`, async () => {
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Deleting... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch(`/api/backups/${encodeURIComponent(backupName)}`, {
method: 'DELETE'
@@ -273,6 +293,9 @@ async function deleteBackup(backupName) {
} catch (err) {
console.error('Failed to delete backup:', err);
if (window.showNotification) window.showNotification('Failed to delete backup: ' + err.message, 'error');
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
});
}
+44 -6
View File
@@ -483,7 +483,8 @@ function renderSubscriptionDomains() {
<div class="text-sm text-gray-600 dark:text-gray-400">Port: ${service.port} | Protocol: ${service.protocol}</div>
</div>
<button
onclick="${isSubscribed ? `unsubscribeFromService('${domainData.domain}', '${service.name}')` : `subscribeToService('${domainData.domain}', '${service.name}', '${service.key}', ${service.port}, '${service.protocol}')`}"
id="btn-${domainData.domain.replace(/[^a-zA-Z0-9]/g, '-')}-${service.name.replace(/[^a-zA-Z0-9]/g, '-')}"
onclick="${isSubscribed ? `unsubscribeFromService('${domainData.domain}', '${service.name}', this)` : `subscribeToService('${domainData.domain}', '${service.name}', '${service.key}', ${service.port}, '${service.protocol}', this)`}"
class="px-4 py-2 ${isSubscribed ? 'bg-red-500 hover:bg-red-600' : 'bg-primary hover:bg-primary-hover'} text-white rounded"
>
${isSubscribed ? 'Unsubscribe' : 'Subscribe'}
@@ -503,7 +504,8 @@ function renderSubscriptionDomains() {
${isSubscribeAll ? '<span class="px-2 py-1 text-xs bg-green-500 text-white rounded">Auto-Subscribe Enabled</span>' : ''}
</div>
<button
onclick="${isSubscribeAll ? `unsubscribeAllFromDomain('${domainData.domain}')` : `subscribeAllToDomain('${domainData.domain}')`}"
id="btn-subscribe-all-${domainData.domain.replace(/[^a-zA-Z0-9]/g, '-')}"
onclick="${isSubscribeAll ? `unsubscribeAllFromDomain('${domainData.domain}', this)` : `subscribeAllToDomain('${domainData.domain}', this)`}"
class="px-3 py-1 text-sm ${isSubscribeAll ? 'bg-red-500 hover:bg-red-600' : 'bg-green-600 hover:bg-green-700'} text-white rounded"
>
${isSubscribeAll ? 'Disable Auto-Subscribe' : 'Enable Auto-Subscribe'}
@@ -517,7 +519,13 @@ function renderSubscriptionDomains() {
}).join('');
}
async function subscribeToService(domain, serviceName, key, port, protocol) {
async function subscribeToService(domain, serviceName, key, port, protocol, buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Subscribing... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/service-subscribe', {
method: 'POST',
@@ -548,10 +556,19 @@ async function subscribeToService(domain, serviceName, key, port, protocol) {
if (window.showNotification) {
window.showNotification('Failed to subscribe: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
async function unsubscribeFromService(domain, serviceName) {
async function unsubscribeFromService(domain, serviceName, buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Unsubscribing... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/service-unsubscribe', {
method: 'POST',
@@ -582,10 +599,19 @@ async function unsubscribeFromService(domain, serviceName) {
if (window.showNotification) {
window.showNotification('Failed to unsubscribe: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
async function subscribeAllToDomain(domain) {
async function subscribeAllToDomain(domain, buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Enabling... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/subscribe-all', {
method: 'POST',
@@ -633,10 +659,19 @@ async function subscribeAllToDomain(domain) {
if (window.showNotification) {
window.showNotification('Failed to enable auto-subscribe: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
async function unsubscribeAllFromDomain(domain) {
async function unsubscribeAllFromDomain(domain, buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Disabling... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/unsubscribe-all', {
method: 'POST',
@@ -668,6 +703,9 @@ async function unsubscribeAllFromDomain(domain) {
if (window.showNotification) {
window.showNotification('Failed to disable auto-subscribe: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
+11 -2
View File
@@ -122,7 +122,14 @@ function updateLocalForm(rec = null) {
}
}
async function submitLocalDns() {
async function submitLocalDns(buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Saving... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const nameEl = document.getElementById('local-name');
const typeEl = document.getElementById('local-type');
const ttlEl = document.getElementById('local-ttl');
@@ -168,7 +175,6 @@ async function submitLocalDns() {
const isEdit = window.editLocalIndex >= 0;
const url = isEdit ? '/api/update-local-dns' : '/api/add-local-dns';
const body = isEdit ? JSON.stringify({ index: window.editLocalIndex, record }) : JSON.stringify(record);
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -184,6 +190,9 @@ async function submitLocalDns() {
} catch (err) {
console.error('Failed to submit local DNS record:', err);
if (window.showNotification) window.showNotification('Failed to submit record: ' + err.message, 'error');
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
+45 -6
View File
@@ -583,7 +583,14 @@ async function recheckPeers() {
window.recheckPeers = recheckPeers;
// Save settings
async function saveSettings() {
async function saveSettings(buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Saving... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const container = document.getElementById('settingsContainer');
if (!container) return;
@@ -599,8 +606,6 @@ async function saveSettings() {
settings[key] = input.value;
}
});
try {
const response = await fetch('/api/update-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -638,6 +643,9 @@ async function saveSettings() {
if (window.showNotification) {
window.showNotification('Failed to save settings: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
@@ -807,7 +815,14 @@ function deleteSubnet(index) {
}
// Submit subnet (add or update)
async function submitSubnet() {
async function submitSubnet(buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Saving... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const nameEl = document.getElementById('subnet-name');
const baseEl = document.getElementById('subnet-base');
const cidrEl = document.getElementById('subnet-cidr');
@@ -860,6 +875,10 @@ async function submitSubnet() {
const modal = document.getElementById('subnetModal');
if (modal) modal.close();
window.editingSubnetIndex = null;
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
// Make functions globally accessible
@@ -904,7 +923,13 @@ function onResetIdentityConfirmChange() {
}
// Reset identity (delete keypair)
async function resetIdentity() {
async function resetIdentity(buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Resetting... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const response = await fetch('/api/reset-identity', {
method: 'POST',
@@ -947,6 +972,9 @@ async function resetIdentity() {
if (window.showNotification) {
window.showNotification('Failed to reset identity: ' + err.message, 'error');
}
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
@@ -1109,7 +1137,14 @@ function deleteDnsServer(index) {
}
// Submit DNS server (add or update)
async function submitDnsServer() {
async function submitDnsServer(buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Saving... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span>';
try {
const ipEl = document.getElementById('dns-server-ip');
const ip = ipEl?.value.trim() || '';
@@ -1161,6 +1196,10 @@ async function submitDnsServer() {
const modal = document.getElementById('dnsServerModal');
if (modal) modal.close();
window.editingDnsServerIndex = null;
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
window.onResetIdentityConfirmChange = onResetIdentityConfirmChange;
+3
View File
@@ -48,9 +48,12 @@ async function handleDomainsRoutes(req, res) {
} catch (err) {
// Fallback if plugin system not available
}
// Only add internal domains that aren't already in the resolved list
for (const d of internalDomains) {
if (!resolved.some(r => r.domain === d)) {
resolved.push({ domain: d, hash: 'internal', isLocal: true });
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(resolved.sort((a, b) => a.domain.localeCompare(b.domain))));
} catch (err) {