show loading indicators when actions are pending
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user