You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							2533 lines
						
					
					
						
							87 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							2533 lines
						
					
					
						
							87 KiB
						
					
					
				| // SeaweedFS Dashboard JavaScript | |
|  | |
| // Global variables | |
| let bucketToDelete = ''; | |
| 
 | |
| // Initialize dashboard when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', function() { | |
|     initializeDashboard(); | |
|     initializeEventHandlers(); | |
|     setupFormValidation(); | |
|     setupFileManagerEventHandlers(); | |
|      | |
|     // Initialize delete button visibility on file browser page | |
|     if (window.location.pathname === '/files') { | |
|         updateDeleteSelectedButton(); | |
|     } | |
| }); | |
| 
 | |
| function initializeDashboard() { | |
|     // Set up HTMX event listeners | |
|     setupHTMXListeners(); | |
|      | |
|     // Initialize tooltips | |
|     initializeTooltips(); | |
|      | |
|     // Set up periodic refresh | |
|     setupAutoRefresh(); | |
|      | |
|     // Set active navigation | |
|     setActiveNavigation(); | |
|      | |
|     // Set up submenu behavior | |
|     setupSubmenuBehavior(); | |
| } | |
| 
 | |
| // HTMX event listeners | |
| function setupHTMXListeners() { | |
|     // Show loading indicator on requests | |
|     document.body.addEventListener('htmx:beforeRequest', function(evt) { | |
|         showLoadingIndicator(); | |
|     }); | |
|      | |
|     // Hide loading indicator on completion | |
|     document.body.addEventListener('htmx:afterRequest', function(evt) { | |
|         hideLoadingIndicator(); | |
|     }); | |
|      | |
|     // Handle errors | |
|     document.body.addEventListener('htmx:responseError', function(evt) { | |
|         handleHTMXError(evt); | |
|     }); | |
| } | |
| 
 | |
| // Initialize Bootstrap tooltips | |
| function initializeTooltips() { | |
|     var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); | |
|     var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { | |
|         return new bootstrap.Tooltip(tooltipTriggerEl); | |
|     }); | |
| } | |
| 
 | |
| // Set up auto-refresh for dashboard data | |
| function setupAutoRefresh() { | |
|     // Refresh dashboard data every 30 seconds | |
|     setInterval(function() { | |
|         if (window.location.pathname === '/dashboard') { | |
|             htmx.trigger('#dashboard-content', 'refresh'); | |
|         } | |
|     }, 30000); | |
| } | |
| 
 | |
| // Set active navigation item | |
| function setActiveNavigation() { | |
|     const currentPath = window.location.pathname; | |
|     const navLinks = document.querySelectorAll('.sidebar .nav-link'); | |
|      | |
|     navLinks.forEach(function(link) { | |
|         const href = link.getAttribute('href'); | |
|         let isActive = false; | |
|          | |
|         if (href === currentPath) { | |
|             isActive = true; | |
|         } else if (currentPath === '/' && href === '/admin') { | |
|             isActive = true; | |
|         } else if (currentPath.startsWith('/s3/') && href === '/s3/buckets') { | |
|             isActive = true; | |
|         } | |
|         // Note: Removed the problematic cluster condition that was highlighting all submenu items | |
|          | |
|         if (isActive) { | |
|             link.classList.add('active'); | |
|         } else { | |
|             link.classList.remove('active'); | |
|         } | |
|     }); | |
| } | |
| 
 | |
| // Set up submenu behavior | |
| function setupSubmenuBehavior() { | |
|     const currentPath = window.location.pathname; | |
|      | |
|     // If we're on a cluster page, expand the cluster submenu | |
|     if (currentPath.startsWith('/cluster/')) { | |
|         const clusterSubmenu = document.getElementById('clusterSubmenu'); | |
|         if (clusterSubmenu) { | |
|             clusterSubmenu.classList.add('show'); | |
|              | |
|             // Update the parent toggle button state | |
|             const toggleButton = document.querySelector('[data-bs-target="#clusterSubmenu"]'); | |
|             if (toggleButton) { | |
|                 toggleButton.classList.remove('collapsed'); | |
|                 toggleButton.setAttribute('aria-expanded', 'true'); | |
|             } | |
|         } | |
|     } | |
|      | |
|     // If we're on an object store page, expand the object store submenu | |
|     if (currentPath.startsWith('/object-store/')) { | |
|         const objectStoreSubmenu = document.getElementById('objectStoreSubmenu'); | |
|         if (objectStoreSubmenu) { | |
|             objectStoreSubmenu.classList.add('show'); | |
|              | |
|             // Update the parent toggle button state | |
|             const toggleButton = document.querySelector('[data-bs-target="#objectStoreSubmenu"]'); | |
|             if (toggleButton) { | |
|                 toggleButton.classList.remove('collapsed'); | |
|                 toggleButton.setAttribute('aria-expanded', 'true'); | |
|             } | |
|         } | |
|     } | |
|      | |
|     // If we're on a maintenance page, expand the maintenance submenu | |
|     if (currentPath.startsWith('/maintenance')) { | |
|         const maintenanceSubmenu = document.getElementById('maintenanceSubmenu'); | |
|         if (maintenanceSubmenu) { | |
|             maintenanceSubmenu.classList.add('show'); | |
|              | |
|             // Update the parent toggle button state | |
|             const toggleButton = document.querySelector('[data-bs-target="#maintenanceSubmenu"]'); | |
|             if (toggleButton) { | |
|                 toggleButton.classList.remove('collapsed'); | |
|                 toggleButton.setAttribute('aria-expanded', 'true'); | |
|             } | |
|         } | |
|     } | |
|      | |
|     // Prevent submenu from collapsing when clicking on submenu items | |
|     const clusterSubmenuLinks = document.querySelectorAll('#clusterSubmenu .nav-link'); | |
|     clusterSubmenuLinks.forEach(function(link) { | |
|         link.addEventListener('click', function(e) { | |
|             // Don't prevent the navigation, just stop the collapse behavior | |
|             e.stopPropagation(); | |
|         }); | |
|     }); | |
|      | |
|     const objectStoreSubmenuLinks = document.querySelectorAll('#objectStoreSubmenu .nav-link'); | |
|     objectStoreSubmenuLinks.forEach(function(link) { | |
|         link.addEventListener('click', function(e) { | |
|             // Don't prevent the navigation, just stop the collapse behavior | |
|             e.stopPropagation(); | |
|         }); | |
|     }); | |
|      | |
|     const maintenanceSubmenuLinks = document.querySelectorAll('#maintenanceSubmenu .nav-link'); | |
|     maintenanceSubmenuLinks.forEach(function(link) { | |
|         link.addEventListener('click', function(e) { | |
|             // Don't prevent the navigation, just stop the collapse behavior | |
|             e.stopPropagation(); | |
|         }); | |
|     }); | |
|      | |
|     // Handle the main cluster toggle | |
|     const clusterToggle = document.querySelector('[data-bs-target="#clusterSubmenu"]'); | |
|     if (clusterToggle) { | |
|         clusterToggle.addEventListener('click', function(e) { | |
|             e.preventDefault(); | |
|              | |
|             const submenu = document.getElementById('clusterSubmenu'); | |
|             const isExpanded = submenu.classList.contains('show'); | |
|              | |
|             if (isExpanded) { | |
|                 // Collapse | |
|                 submenu.classList.remove('show'); | |
|                 this.classList.add('collapsed'); | |
|                 this.setAttribute('aria-expanded', 'false'); | |
|             } else { | |
|                 // Expand | |
|                 submenu.classList.add('show'); | |
|                 this.classList.remove('collapsed'); | |
|                 this.setAttribute('aria-expanded', 'true'); | |
|             } | |
|         }); | |
|     } | |
|      | |
|     // Handle the main object store toggle | |
|     const objectStoreToggle = document.querySelector('[data-bs-target="#objectStoreSubmenu"]'); | |
|     if (objectStoreToggle) { | |
|         objectStoreToggle.addEventListener('click', function(e) { | |
|             e.preventDefault(); | |
|              | |
|             const submenu = document.getElementById('objectStoreSubmenu'); | |
|             const isExpanded = submenu.classList.contains('show'); | |
|              | |
|             if (isExpanded) { | |
|                 // Collapse | |
|                 submenu.classList.remove('show'); | |
|                 this.classList.add('collapsed'); | |
|                 this.setAttribute('aria-expanded', 'false'); | |
|             } else { | |
|                 // Expand | |
|                 submenu.classList.add('show'); | |
|                 this.classList.remove('collapsed'); | |
|                 this.setAttribute('aria-expanded', 'true'); | |
|             } | |
|         }); | |
|     } | |
|      | |
|     // Handle the main maintenance toggle | |
|     const maintenanceToggle = document.querySelector('[data-bs-target="#maintenanceSubmenu"]'); | |
|     if (maintenanceToggle) { | |
|         maintenanceToggle.addEventListener('click', function(e) { | |
|             e.preventDefault(); | |
|              | |
|             const submenu = document.getElementById('maintenanceSubmenu'); | |
|             const isExpanded = submenu.classList.contains('show'); | |
|              | |
|             if (isExpanded) { | |
|                 // Collapse | |
|                 submenu.classList.remove('show'); | |
|                 this.classList.add('collapsed'); | |
|                 this.setAttribute('aria-expanded', 'false'); | |
|             } else { | |
|                 // Expand | |
|                 submenu.classList.add('show'); | |
|                 this.classList.remove('collapsed'); | |
|                 this.setAttribute('aria-expanded', 'true'); | |
|             } | |
|         }); | |
|     } | |
| } | |
| 
 | |
| // Loading indicator functions | |
| function showLoadingIndicator() { | |
|     const indicator = document.getElementById('loading-indicator'); | |
|     if (indicator) { | |
|         indicator.style.display = 'block'; | |
|     } | |
|      | |
|     // Add loading class to body | |
|     document.body.classList.add('loading'); | |
| } | |
| 
 | |
| function hideLoadingIndicator() { | |
|     const indicator = document.getElementById('loading-indicator'); | |
|     if (indicator) { | |
|         indicator.style.display = 'none'; | |
|     } | |
|      | |
|     // Remove loading class from body | |
|     document.body.classList.remove('loading'); | |
| } | |
| 
 | |
| // Handle HTMX errors | |
| function handleHTMXError(evt) { | |
|     console.error('HTMX Request Error:', evt.detail); | |
|      | |
|     // Show error toast or message | |
|     showErrorMessage('Request failed. Please try again.'); | |
|      | |
|     hideLoadingIndicator(); | |
| } | |
| 
 | |
| // Utility functions | |
| function showErrorMessage(message) { | |
|     // Create toast element | |
|     const toast = document.createElement('div'); | |
|     toast.className = 'toast align-items-center text-white bg-danger border-0'; | |
|     toast.setAttribute('role', 'alert'); | |
|     toast.setAttribute('aria-live', 'assertive'); | |
|     toast.setAttribute('aria-atomic', 'true'); | |
|      | |
|     toast.innerHTML = ` | |
|         <div class="d-flex"> | |
|             <div class="toast-body"> | |
|                 <i class="fas fa-exclamation-triangle me-2"></i> | |
|                 ${message} | |
|             </div> | |
|             <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> | |
|         </div> | |
|     `; | |
|      | |
|     // Add to toast container or create one | |
|     let toastContainer = document.getElementById('toast-container'); | |
|     if (!toastContainer) { | |
|         toastContainer = document.createElement('div'); | |
|         toastContainer.id = 'toast-container'; | |
|         toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3'; | |
|         toastContainer.style.zIndex = '1055'; | |
|         document.body.appendChild(toastContainer); | |
|     } | |
|      | |
|     toastContainer.appendChild(toast); | |
|      | |
|     // Show toast | |
|     const bsToast = new bootstrap.Toast(toast); | |
|     bsToast.show(); | |
|      | |
|     // Remove toast element after it's hidden | |
|     toast.addEventListener('hidden.bs.toast', function() { | |
|         toast.remove(); | |
|     }); | |
| } | |
| 
 | |
| function showSuccessMessage(message) { | |
|     // Similar to showErrorMessage but with success styling | |
|     const toast = document.createElement('div'); | |
|     toast.className = 'toast align-items-center text-white bg-success border-0'; | |
|     toast.setAttribute('role', 'alert'); | |
|     toast.setAttribute('aria-live', 'assertive'); | |
|     toast.setAttribute('aria-atomic', 'true'); | |
|      | |
|     toast.innerHTML = ` | |
|         <div class="d-flex"> | |
|             <div class="toast-body"> | |
|                 <i class="fas fa-check-circle me-2"></i> | |
|                 ${message} | |
|             </div> | |
|             <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> | |
|         </div> | |
|     `; | |
|      | |
|     let toastContainer = document.getElementById('toast-container'); | |
|     if (!toastContainer) { | |
|         toastContainer = document.createElement('div'); | |
|         toastContainer.id = 'toast-container'; | |
|         toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3'; | |
|         toastContainer.style.zIndex = '1055'; | |
|         document.body.appendChild(toastContainer); | |
|     } | |
|      | |
|     toastContainer.appendChild(toast); | |
|      | |
|     const bsToast = new bootstrap.Toast(toast); | |
|     bsToast.show(); | |
|      | |
|     toast.addEventListener('hidden.bs.toast', function() { | |
|         toast.remove(); | |
|     }); | |
| } | |
| 
 | |
| // Format bytes for display | |
| function formatBytes(bytes, decimals = 2) { | |
|     if (bytes === 0) return '0 Bytes'; | |
|      | |
|     const k = 1024; | |
|     const dm = decimals < 0 ? 0 : decimals; | |
|     const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; | |
|      | |
|     const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
|      | |
|     return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; | |
| } | |
| 
 | |
| // Format numbers with commas | |
| function formatNumber(num) { | |
|     return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |
| } | |
| 
 | |
| // Helper function to format disk types for CSV export | |
| function formatDiskTypes(diskTypesText) { | |
|     // Remove any HTML tags and clean up the text | |
|     return diskTypesText.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); | |
| } | |
| 
 | |
| // Confirm action dialogs | |
| function confirmAction(message, callback) { | |
|     if (confirm(message)) { | |
|         callback(); | |
|     } | |
| } | |
| 
 | |
| // Global error handler | |
| window.addEventListener('error', function(e) { | |
|     console.error('Global error:', e.error); | |
|     showErrorMessage('An unexpected error occurred.'); | |
| }); | |
| 
 | |
| // Export functions for global use | |
| window.Dashboard = { | |
|     showErrorMessage, | |
|     showSuccessMessage, | |
|     formatBytes, | |
|     formatNumber, | |
|     confirmAction | |
| };  | |
| 
 | |
| // Initialize event handlers | |
| function initializeEventHandlers() { | |
|     // S3 Bucket Management | |
|     const createBucketForm = document.getElementById('createBucketForm'); | |
|     if (createBucketForm) { | |
|         createBucketForm.addEventListener('submit', handleCreateBucket); | |
|     } | |
| 
 | |
|     // Delete bucket buttons | |
|     document.addEventListener('click', function(e) { | |
|         if (e.target.closest('.delete-bucket-btn')) { | |
|             const button = e.target.closest('.delete-bucket-btn'); | |
|             const bucketName = button.getAttribute('data-bucket-name'); | |
|             confirmDeleteBucket(bucketName); | |
|         } | |
|          | |
|         // Quota management buttons | |
|         if (e.target.closest('.quota-btn')) { | |
|             const button = e.target.closest('.quota-btn'); | |
|             const bucketName = button.getAttribute('data-bucket-name'); | |
|             const currentQuota = parseInt(button.getAttribute('data-current-quota')) || 0; | |
|             const quotaEnabled = button.getAttribute('data-quota-enabled') === 'true'; | |
|             showQuotaModal(bucketName, currentQuota, quotaEnabled); | |
|         } | |
|     }); | |
| 
 | |
|     // Quota form submission | |
|     const quotaForm = document.getElementById('quotaForm'); | |
|     if (quotaForm) { | |
|         quotaForm.addEventListener('submit', handleUpdateQuota); | |
|     } | |
| 
 | |
|     // Enable quota checkbox for create bucket form | |
|     const enableQuotaCheckbox = document.getElementById('enableQuota'); | |
|     if (enableQuotaCheckbox) { | |
|         enableQuotaCheckbox.addEventListener('change', function() { | |
|             const quotaSettings = document.getElementById('quotaSettings'); | |
|             if (this.checked) { | |
|                 quotaSettings.style.display = 'block'; | |
|             } else { | |
|                 quotaSettings.style.display = 'none'; | |
|             } | |
|         }); | |
|     } | |
| 
 | |
|     // Enable quota checkbox for quota modal | |
|     const quotaEnabledCheckbox = document.getElementById('quotaEnabled'); | |
|     if (quotaEnabledCheckbox) { | |
|         quotaEnabledCheckbox.addEventListener('change', function() { | |
|             const quotaSizeSettings = document.getElementById('quotaSizeSettings'); | |
|             if (this.checked) { | |
|                 quotaSizeSettings.style.display = 'block'; | |
|             } else { | |
|                 quotaSizeSettings.style.display = 'none'; | |
|             } | |
|         }); | |
|     } | |
| } | |
| 
 | |
| // Setup form validation | |
| function setupFormValidation() { | |
|     // Bucket name validation | |
|     const bucketNameInput = document.getElementById('bucketName'); | |
|     if (bucketNameInput) { | |
|         bucketNameInput.addEventListener('input', validateBucketName); | |
|     } | |
| } | |
| 
 | |
| // S3 Bucket Management Functions | |
|  | |
| // Handle create bucket form submission | |
| async function handleCreateBucket(event) { | |
|     event.preventDefault(); | |
|      | |
|     const form = event.target; | |
|     const formData = new FormData(form); | |
|     const bucketData = { | |
|         name: formData.get('name'), | |
|         region: formData.get('region') || 'us-east-1', | |
|         quota_enabled: formData.get('quota_enabled') === 'on', | |
|         quota_size: parseInt(formData.get('quota_size')) || 0, | |
|         quota_unit: formData.get('quota_unit') || 'MB' | |
|     }; | |
| 
 | |
|     try { | |
|         const response = await fetch('/api/s3/buckets', { | |
|             method: 'POST', | |
|             headers: { | |
|                 'Content-Type': 'application/json', | |
|             }, | |
|             body: JSON.stringify(bucketData) | |
|         }); | |
| 
 | |
|         const result = await response.json(); | |
| 
 | |
|         if (response.ok) { | |
|             // Success | |
|             showAlert('success', `Bucket "${bucketData.name}" created successfully!`); | |
|              | |
|             // Close modal | |
|             const modal = bootstrap.Modal.getInstance(document.getElementById('createBucketModal')); | |
|             modal.hide(); | |
|              | |
|             // Reset form | |
|             form.reset(); | |
|              | |
|             // Refresh the page after a short delay | |
|             setTimeout(() => { | |
|                 location.reload(); | |
|             }, 1500); | |
|         } else { | |
|             // Error | |
|             showAlert('danger', result.error || 'Failed to create bucket'); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error creating bucket:', error); | |
|         showAlert('danger', 'Network error occurred while creating bucket'); | |
|     } | |
| } | |
| 
 | |
| // Validate bucket name input | |
| function validateBucketName(event) { | |
|     const input = event.target; | |
|     const value = input.value; | |
|     const isValid = /^[a-z0-9.-]+$/.test(value) && value.length >= 3 && value.length <= 63; | |
|      | |
|     if (value.length > 0 && !isValid) { | |
|         input.setCustomValidity('Bucket name must contain only lowercase letters, numbers, dots, and hyphens (3-63 characters)'); | |
|     } else { | |
|         input.setCustomValidity(''); | |
|     } | |
| } | |
| 
 | |
| // Confirm bucket deletion | |
| function confirmDeleteBucket(bucketName) { | |
|     bucketToDelete = bucketName; | |
|     document.getElementById('deleteBucketName').textContent = bucketName; | |
|      | |
|     const modal = new bootstrap.Modal(document.getElementById('deleteBucketModal')); | |
|     modal.show(); | |
| } | |
| 
 | |
| // Delete bucket | |
| async function deleteBucket() { | |
|     if (!bucketToDelete) { | |
|         return; | |
|     } | |
| 
 | |
|     try { | |
|         const response = await fetch(`/api/s3/buckets/${bucketToDelete}`, { | |
|             method: 'DELETE' | |
|         }); | |
| 
 | |
|         const result = await response.json(); | |
| 
 | |
|         if (response.ok) { | |
|             // Success | |
|             showAlert('success', `Bucket "${bucketToDelete}" deleted successfully!`); | |
|              | |
|             // Close modal | |
|             const modal = bootstrap.Modal.getInstance(document.getElementById('deleteBucketModal')); | |
|             modal.hide(); | |
|              | |
|             // Refresh the page after a short delay | |
|             setTimeout(() => { | |
|                 location.reload(); | |
|             }, 1500); | |
|         } else { | |
|             // Error | |
|             showAlert('danger', result.error || 'Failed to delete bucket'); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error deleting bucket:', error); | |
|         showAlert('danger', 'Network error occurred while deleting bucket'); | |
|     } | |
| 
 | |
|     bucketToDelete = ''; | |
| } | |
| 
 | |
| // Refresh buckets list | |
| function refreshBuckets() { | |
|     location.reload(); | |
| } | |
| 
 | |
| // Export bucket list | |
| function exportBucketList() { | |
|     // Get table data | |
|     const table = document.getElementById('bucketsTable'); | |
|     if (!table) return; | |
| 
 | |
|     const rows = Array.from(table.querySelectorAll('tbody tr')); | |
|     const data = rows.map(row => { | |
|         const cells = row.querySelectorAll('td'); | |
|         if (cells.length < 5) return null; // Skip empty state row | |
|          | |
|         return { | |
|             name: cells[0].textContent.trim(), | |
|             created: cells[1].textContent.trim(), | |
|             objects: cells[2].textContent.trim(), | |
|             size: cells[3].textContent.trim(), | |
|             quota: cells[4].textContent.trim() | |
|         }; | |
|     }).filter(item => item !== null); | |
| 
 | |
|     // Convert to CSV | |
|     const csv = [ | |
|         ['Name', 'Created', 'Objects', 'Size', 'Quota'].join(','), | |
|         ...data.map(row => [ | |
|             row.name, | |
|             row.created, | |
|             row.objects, | |
|             row.size, | |
|             row.quota | |
|         ].join(',')) | |
|     ].join('\n'); | |
| 
 | |
|     // Download CSV | |
|     const blob = new Blob([csv], { type: 'text/csv' }); | |
|     const url = window.URL.createObjectURL(blob); | |
|     const a = document.createElement('a'); | |
|     a.href = url; | |
|     a.download = `seaweedfs-buckets-${new Date().toISOString().split('T')[0]}.csv`; | |
|     document.body.appendChild(a); | |
|     a.click(); | |
|     document.body.removeChild(a); | |
|     window.URL.revokeObjectURL(url); | |
| } | |
| 
 | |
| // Show alert message | |
| function showAlert(type, message) { | |
|     // Remove existing alerts | |
|     const existingAlerts = document.querySelectorAll('.alert-floating'); | |
|     existingAlerts.forEach(alert => alert.remove()); | |
| 
 | |
|     // Create new alert | |
|     const alert = document.createElement('div'); | |
|     alert.className = `alert alert-${type} alert-dismissible fade show alert-floating`; | |
|     alert.style.cssText = ` | |
|         position: fixed; | |
|         top: 20px; | |
|         right: 20px; | |
|         z-index: 9999; | |
|         min-width: 300px; | |
|         box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
|     `; | |
|      | |
|     alert.innerHTML = ` | |
|         ${message} | |
|         <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> | |
|     `; | |
| 
 | |
|     document.body.appendChild(alert); | |
| 
 | |
|     // Auto-remove after 5 seconds | |
|     setTimeout(() => { | |
|         if (alert.parentNode) { | |
|             alert.remove(); | |
|         } | |
|     }, 5000); | |
| } | |
| 
 | |
| // Format date for display | |
| function formatDate(date) { | |
|     return new Date(date).toLocaleString(); | |
| } | |
| 
 | |
| // Copy text to clipboard | |
| function copyToClipboard(text) { | |
|     navigator.clipboard.writeText(text).then(() => { | |
|         showAlert('success', 'Copied to clipboard!'); | |
|     }).catch(err => { | |
|         console.error('Failed to copy text: ', err); | |
|         showAlert('danger', 'Failed to copy to clipboard'); | |
|     }); | |
| } | |
| 
 | |
| // Dashboard refresh functionality | |
| function refreshDashboard() { | |
|     location.reload(); | |
| } | |
| 
 | |
| // Cluster management functions | |
|  | |
| // Export volume servers data as CSV | |
| function exportVolumeServers() { | |
|     const table = document.getElementById('hostsTable'); | |
|     if (!table) { | |
|         showErrorMessage('No volume servers data to export'); | |
|         return; | |
|     } | |
|      | |
|     let csv = 'Server ID,Address,Data Center,Rack,Volumes,Capacity,Usage\n'; | |
|      | |
|     const rows = table.querySelectorAll('tbody tr'); | |
|     rows.forEach(row => { | |
|         const cells = row.querySelectorAll('td'); | |
|         if (cells.length >= 7) { | |
|             const rowData = [ | |
|                 cells[0].textContent.trim(), | |
|                 cells[1].textContent.trim(), | |
|                 cells[2].textContent.trim(), | |
|                 cells[3].textContent.trim(), | |
|                 cells[4].textContent.trim(), | |
|                 cells[5].textContent.trim(), | |
|                 cells[6].textContent.trim() | |
|             ]; | |
|             csv += rowData.join(',') + '\n'; | |
|         } | |
|     }); | |
|      | |
|     downloadCSV(csv, 'seaweedfs-volume-servers.csv'); | |
| } | |
| 
 | |
| // Export volumes data as CSV | |
| function exportVolumes() { | |
|     const table = document.getElementById('volumesTable'); | |
|     if (!table) { | |
|         showErrorMessage('No volumes data to export'); | |
|         return; | |
|     } | |
|      | |
|     // Get headers from the table (dynamically handles conditional columns) | |
|     const headerCells = table.querySelectorAll('thead th'); | |
|     const headers = []; | |
|     headerCells.forEach((cell, index) => { | |
|         // Skip the Actions column (last column) | |
|         if (index < headerCells.length - 1) { | |
|             headers.push(cell.textContent.trim()); | |
|         } | |
|     }); | |
|      | |
|     let csv = headers.join(',') + '\n'; | |
|      | |
|     const rows = table.querySelectorAll('tbody tr'); | |
|     rows.forEach(row => { | |
|         const cells = row.querySelectorAll('td'); | |
|         const rowData = []; | |
|         // Export all cells except the Actions column (last column) | |
|         for (let i = 0; i < cells.length - 1; i++) { | |
|             rowData.push(`"${cells[i].textContent.trim().replace(/"/g, '""')}"`); | |
|         } | |
|             csv += rowData.join(',') + '\n'; | |
|     }); | |
|      | |
|     downloadCSV(csv, 'seaweedfs-volumes.csv'); | |
| } | |
| 
 | |
| // Export collections data as CSV | |
| function exportCollections() { | |
|     const table = document.getElementById('collectionsTable'); | |
|     if (!table) { | |
|         showAlert('error', 'Collections table not found'); | |
|         return; | |
|     } | |
| 
 | |
|     const headers = ['Collection Name', 'Volumes', 'Files', 'Size', 'Disk Types']; | |
|     const rows = []; | |
| 
 | |
|     // Get table rows | |
|     const tableRows = table.querySelectorAll('tbody tr'); | |
|     tableRows.forEach(row => { | |
|         const cells = row.querySelectorAll('td'); | |
|         if (cells.length >= 5) { | |
|             rows.push([ | |
|                 cells[0].textContent.trim(), | |
|                 cells[1].textContent.trim(), | |
|                 cells[2].textContent.trim(), | |
|                 cells[3].textContent.trim(), | |
|                 formatDiskTypes(cells[4].textContent.trim()) | |
|             ]); | |
|         } | |
|     }); | |
| 
 | |
|     // Generate CSV | |
|     const csvContent = [headers, ...rows] | |
|         .map(row => row.map(cell => `"${cell}"`).join(',')) | |
|         .join('\n'); | |
| 
 | |
|     // Download | |
|     const filename = `seaweedfs-collections-${new Date().toISOString().split('T')[0]}.csv`; | |
|     downloadCSV(csvContent, filename); | |
| } | |
| 
 | |
| // Export Masters to CSV | |
| function exportMasters() { | |
|     const table = document.getElementById('mastersTable'); | |
|     if (!table) { | |
|         showAlert('error', 'Masters table not found'); | |
|         return; | |
|     } | |
| 
 | |
|     const headers = ['Address', 'Role', 'Suffrage']; | |
|     const rows = []; | |
| 
 | |
|     // Get table rows | |
|     const tableRows = table.querySelectorAll('tbody tr'); | |
|     tableRows.forEach(row => { | |
|         const cells = row.querySelectorAll('td'); | |
|         if (cells.length >= 3) { | |
|             rows.push([ | |
|                 cells[0].textContent.trim(), | |
|                 cells[1].textContent.trim(), | |
|                 cells[2].textContent.trim() | |
|             ]); | |
|         } | |
|     }); | |
| 
 | |
|     // Generate CSV | |
|     const csvContent = [headers, ...rows] | |
|         .map(row => row.map(cell => `"${cell}"`).join(',')) | |
|         .join('\n'); | |
| 
 | |
|     // Download | |
|     const filename = `seaweedfs-masters-${new Date().toISOString().split('T')[0]}.csv`; | |
|     downloadCSV(csvContent, filename); | |
| } | |
| 
 | |
| // Export Filers to CSV | |
| function exportFilers() { | |
|     const table = document.getElementById('filersTable'); | |
|     if (!table) { | |
|         showAlert('error', 'Filers table not found'); | |
|         return; | |
|     } | |
| 
 | |
|     const headers = ['Address', 'Version', 'Data Center', 'Rack', 'Created At']; | |
|     const rows = []; | |
| 
 | |
|     // Get table rows | |
|     const tableRows = table.querySelectorAll('tbody tr'); | |
|     tableRows.forEach(row => { | |
|         const cells = row.querySelectorAll('td'); | |
|         if (cells.length >= 5) { | |
|             rows.push([ | |
|                 cells[0].textContent.trim(), | |
|                 cells[1].textContent.trim(), | |
|                 cells[2].textContent.trim(), | |
|                 cells[3].textContent.trim(), | |
|                 cells[4].textContent.trim() | |
|             ]); | |
|         } | |
|     }); | |
| 
 | |
|     // Generate CSV | |
|     const csvContent = [headers, ...rows] | |
|         .map(row => row.map(cell => `"${cell}"`).join(',')) | |
|         .join('\n'); | |
| 
 | |
|     // Download | |
|     const filename = `seaweedfs-filers-${new Date().toISOString().split('T')[0]}.csv`; | |
|     downloadCSV(csvContent, filename); | |
| } | |
| 
 | |
| // Export Users to CSV | |
| function exportUsers() { | |
|     const table = document.getElementById('usersTable'); | |
|     if (!table) { | |
|         showAlert('error', 'Users table not found'); | |
|         return; | |
|     } | |
|      | |
|     const rows = table.querySelectorAll('tbody tr'); | |
|     if (rows.length === 0) { | |
|         showErrorMessage('No users to export'); | |
|         return; | |
|     } | |
|      | |
|     let csvContent = 'Username,Email,Access Key,Status,Created,Last Login\n'; | |
|      | |
|     rows.forEach(row => { | |
|         const cells = row.querySelectorAll('td'); | |
|         if (cells.length >= 6) { | |
|             const username = cells[0].textContent.trim(); | |
|             const email = cells[1].textContent.trim(); | |
|             const accessKey = cells[2].textContent.trim(); | |
|             const status = cells[3].textContent.trim(); | |
|             const created = cells[4].textContent.trim(); | |
|             const lastLogin = cells[5].textContent.trim(); | |
|              | |
|             csvContent += `"${username}","${email}","${accessKey}","${status}","${created}","${lastLogin}"\n`; | |
|         } | |
|     }); | |
|      | |
|     downloadCSV(csvContent, 'seaweedfs-users.csv'); | |
| } | |
| 
 | |
| // Confirm delete collection | |
| function confirmDeleteCollection(button) { | |
|     const collectionName = button.getAttribute('data-collection-name'); | |
|     document.getElementById('deleteCollectionName').textContent = collectionName; | |
|      | |
|     const modal = new bootstrap.Modal(document.getElementById('deleteCollectionModal')); | |
|     modal.show(); | |
|      | |
|     // Set up confirm button | |
|     document.getElementById('confirmDeleteCollection').onclick = function() { | |
|         deleteCollection(collectionName); | |
|     }; | |
| } | |
| 
 | |
| // Delete collection | |
| async function deleteCollection(collectionName) { | |
|     try { | |
|         const response = await fetch(`/api/collections/${collectionName}`, { | |
|             method: 'DELETE', | |
|             headers: { | |
|                 'Content-Type': 'application/json', | |
|             } | |
|         }); | |
|          | |
|         if (response.ok) { | |
|             showSuccessMessage(`Collection "${collectionName}" deleted successfully`); | |
|             // Hide modal | |
|             const modal = bootstrap.Modal.getInstance(document.getElementById('deleteCollectionModal')); | |
|             modal.hide(); | |
|             // Refresh page | |
|             setTimeout(() => { | |
|                 window.location.reload(); | |
|             }, 1000); | |
|         } else { | |
|             const error = await response.json(); | |
|             showErrorMessage(`Failed to delete collection: ${error.error || 'Unknown error'}`); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error deleting collection:', error); | |
|         showErrorMessage('Failed to delete collection. Please try again.'); | |
|     } | |
| } | |
| 
 | |
| 
 | |
| 
 | |
| // Download CSV utility function | |
| function downloadCSV(csvContent, filename) { | |
|     const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); | |
|     const link = document.createElement('a'); | |
|      | |
|     if (link.download !== undefined) { | |
|         const url = URL.createObjectURL(blob); | |
|         link.setAttribute('href', url); | |
|         link.setAttribute('download', filename); | |
|         link.style.visibility = 'hidden'; | |
|         document.body.appendChild(link); | |
|         link.click(); | |
|         document.body.removeChild(link); | |
|     } | |
| } | |
| 
 | |
| // File Browser Functions | |
|  | |
| // Toggle select all checkboxes | |
| function toggleSelectAll() { | |
|     const selectAll = document.getElementById('selectAll'); | |
|     const checkboxes = document.querySelectorAll('.file-checkbox'); | |
|      | |
|     checkboxes.forEach(checkbox => { | |
|         checkbox.checked = selectAll.checked; | |
|     }); | |
|      | |
|     updateDeleteSelectedButton(); | |
| } | |
| 
 | |
| // Update visibility of delete selected button based on selection | |
| function updateDeleteSelectedButton() { | |
|     const checkboxes = document.querySelectorAll('.file-checkbox:checked'); | |
|     const deleteBtn = document.getElementById('deleteSelectedBtn'); | |
|      | |
|     if (deleteBtn) { | |
|         if (checkboxes.length > 0) { | |
|             deleteBtn.style.display = 'inline-block'; | |
|             deleteBtn.innerHTML = `<i class="fas fa-trash me-1"></i>Delete Selected (${checkboxes.length})`; | |
|         } else { | |
|             deleteBtn.style.display = 'none'; | |
|         } | |
|     } | |
| } | |
| 
 | |
| // Update select all checkbox state based on individual selections | |
| function updateSelectAllCheckbox() { | |
|     const selectAll = document.getElementById('selectAll'); | |
|     const allCheckboxes = document.querySelectorAll('.file-checkbox'); | |
|     const checkedCheckboxes = document.querySelectorAll('.file-checkbox:checked'); | |
|      | |
|     if (selectAll && allCheckboxes.length > 0) { | |
|         if (checkedCheckboxes.length === 0) { | |
|             selectAll.checked = false; | |
|             selectAll.indeterminate = false; | |
|         } else if (checkedCheckboxes.length === allCheckboxes.length) { | |
|             selectAll.checked = true; | |
|             selectAll.indeterminate = false; | |
|         } else { | |
|             selectAll.checked = false; | |
|             selectAll.indeterminate = true; | |
|         } | |
|     } | |
| } | |
| 
 | |
| // Get selected file paths | |
| function getSelectedFilePaths() { | |
|     const checkboxes = document.querySelectorAll('.file-checkbox:checked'); | |
|     return Array.from(checkboxes).map(cb => cb.value); | |
| } | |
| 
 | |
| // Confirm delete selected files | |
| function confirmDeleteSelected() { | |
|     const selectedPaths = getSelectedFilePaths(); | |
|      | |
|     if (selectedPaths.length === 0) { | |
|         showAlert('warning', 'No files selected'); | |
|         return; | |
|     } | |
|      | |
|     const fileNames = selectedPaths.map(path => path.split('/').pop()).join(', '); | |
|     const message = selectedPaths.length === 1  | |
|         ? `Are you sure you want to delete "${fileNames}"?` | |
|         : `Are you sure you want to delete ${selectedPaths.length} selected items?\n\n${fileNames.substring(0, 200)}${fileNames.length > 200 ? '...' : ''}`; | |
|      | |
|     if (confirm(message)) { | |
|         deleteSelectedFiles(selectedPaths); | |
|     } | |
| } | |
| 
 | |
| // Delete multiple selected files | |
| async function deleteSelectedFiles(filePaths) { | |
|     if (!filePaths || filePaths.length === 0) { | |
|         showAlert('warning', 'No files selected'); | |
|         return; | |
|     } | |
|      | |
|     // Disable the delete button during operation | |
|     const deleteBtn = document.getElementById('deleteSelectedBtn'); | |
|     const originalText = deleteBtn.innerHTML; | |
|     deleteBtn.disabled = true; | |
|     deleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Deleting...'; | |
|      | |
|     try { | |
|         const response = await fetch('/api/files/delete-multiple', { | |
|             method: 'DELETE', | |
|             headers: { | |
|                 'Content-Type': 'application/json', | |
|             }, | |
|             body: JSON.stringify({ paths: filePaths }) | |
|         }); | |
| 
 | |
|         if (response.ok) { | |
|             const result = await response.json(); | |
|              | |
|             if (result.deleted > 0) { | |
|                 if (result.failed === 0) { | |
|                     showAlert('success', `Successfully deleted ${result.deleted} item(s)`); | |
|                 } else { | |
|                     showAlert('warning', `Deleted ${result.deleted} item(s), failed to delete ${result.failed} item(s)`); | |
|                     if (result.errors && result.errors.length > 0) { | |
|                         console.warn('Deletion errors:', result.errors); | |
|                     } | |
|                 } | |
|                  | |
|                 // Reload the page to update the file list | |
|                 setTimeout(() => { | |
|                     window.location.reload(); | |
|                 }, 1000); | |
|             } else { | |
|                 let errorMessage = result.message || 'Failed to delete all selected items'; | |
|                 if (result.errors && result.errors.length > 0) { | |
|                     errorMessage += ': ' + result.errors.join(', '); | |
|                 } | |
|                 showAlert('error', errorMessage); | |
|             } | |
|         } else { | |
|             const error = await response.json(); | |
|             showAlert('error', `Failed to delete files: ${error.error || 'Unknown error'}`); | |
|         } | |
|     } catch (error) { | |
|         console.error('Delete error:', error); | |
|         showAlert('error', 'Failed to delete files'); | |
|     } finally { | |
|         // Re-enable the button | |
|         deleteBtn.disabled = false; | |
|         deleteBtn.innerHTML = originalText; | |
|     } | |
| } | |
| 
 | |
| // Create new folder | |
| function createFolder() { | |
|     const modal = new bootstrap.Modal(document.getElementById('createFolderModal')); | |
|     modal.show(); | |
| } | |
| 
 | |
| // Upload file | |
| function uploadFile() { | |
|     const modal = new bootstrap.Modal(document.getElementById('uploadFileModal')); | |
|     modal.show(); | |
| } | |
| 
 | |
| // Submit create folder form | |
| async function submitCreateFolder() { | |
|     const folderName = document.getElementById('folderName').value.trim(); | |
|     const currentPath = document.getElementById('currentPath').value; | |
|      | |
|     if (!folderName) { | |
|         showErrorMessage('Please enter a folder name'); | |
|         return; | |
|     } | |
|      | |
|     // Validate folder name | |
|     if (folderName.includes('/') || folderName.includes('\\')) { | |
|         showErrorMessage('Folder names cannot contain / or \\ characters'); | |
|         return; | |
|     } | |
|      | |
|     // Additional validation for reserved names | |
|     const reservedNames = ['.', '..', 'CON', 'PRN', 'AUX', 'NUL']; | |
|     if (reservedNames.includes(folderName.toUpperCase())) { | |
|         showErrorMessage('This folder name is reserved and cannot be used'); | |
|         return; | |
|     } | |
|      | |
|     // Disable the button to prevent double submission | |
|     const submitButton = document.querySelector('#createFolderModal .btn-primary'); | |
|     const originalText = submitButton.innerHTML; | |
|     submitButton.disabled = true; | |
|     submitButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Creating...'; | |
|      | |
|     try { | |
|         const response = await fetch('/api/files/create-folder', { | |
|             method: 'POST', | |
|             headers: { | |
|                 'Content-Type': 'application/json', | |
|             }, | |
|             body: JSON.stringify({ | |
|                 path: currentPath, | |
|                 folder_name: folderName | |
|             }) | |
|         }); | |
| 
 | |
|         if (response.ok) { | |
|             showSuccessMessage(`Folder "${folderName}" created successfully`); | |
|             // Hide modal | |
|             const modal = bootstrap.Modal.getInstance(document.getElementById('createFolderModal')); | |
|             modal.hide(); | |
|             // Clear form | |
|             document.getElementById('folderName').value = ''; | |
|             // Refresh page | |
|             setTimeout(() => { | |
|                 window.location.reload(); | |
|             }, 1000); | |
|         } else { | |
|             const error = await response.json(); | |
|             showErrorMessage(`Failed to create folder: ${error.error || 'Unknown error'}`); | |
|         } | |
|     } catch (error) { | |
|         console.error('Create folder error:', error); | |
|         showErrorMessage('Failed to create folder. Please try again.'); | |
|     } finally { | |
|         // Re-enable the button | |
|         submitButton.disabled = false; | |
|         submitButton.innerHTML = originalText; | |
|     } | |
| } | |
| 
 | |
| // Submit upload file form | |
| async function submitUploadFile() { | |
|     const fileInput = document.getElementById('fileInput'); | |
|     const currentPath = document.getElementById('uploadPath').value; | |
|      | |
|     if (!fileInput.files || fileInput.files.length === 0) { | |
|         showErrorMessage('Please select at least one file to upload'); | |
|         return; | |
|     } | |
|      | |
|     const files = Array.from(fileInput.files); | |
|     const totalSize = files.reduce((sum, file) => sum + file.size, 0); | |
|      | |
|     // Validate total file size (limit to 500MB for admin interface) | |
|     const maxSize = 500 * 1024 * 1024; // 500MB total | |
|     if (totalSize > maxSize) { | |
|         showErrorMessage('Total file size exceeds 500MB limit. Please select fewer or smaller files.'); | |
|         return; | |
|     } | |
|      | |
|     // Individual file size validation removed - no limit per file | |
|      | |
|     const formData = new FormData(); | |
|     files.forEach(file => { | |
|         formData.append('files', file); | |
|     }); | |
|     formData.append('path', currentPath); | |
|      | |
|     // Show progress bar and disable button | |
|     const progressContainer = document.getElementById('uploadProgress'); | |
|     const progressBar = progressContainer.querySelector('.progress-bar'); | |
|     const uploadStatus = document.getElementById('uploadStatus'); | |
|     const submitButton = document.querySelector('#uploadFileModal .btn-primary'); | |
|     const originalText = submitButton.innerHTML; | |
|      | |
|     progressContainer.style.display = 'block'; | |
|     progressBar.style.width = '0%'; | |
|     progressBar.setAttribute('aria-valuenow', '0'); | |
|     progressBar.textContent = '0%'; | |
|     uploadStatus.textContent = `Uploading ${files.length} file(s)...`; | |
|     submitButton.disabled = true; | |
|     submitButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...'; | |
|      | |
|     try { | |
|         const xhr = new XMLHttpRequest(); | |
|          | |
|         // Handle progress | |
|         xhr.upload.addEventListener('progress', function(e) { | |
|             if (e.lengthComputable) { | |
|                 const percentComplete = Math.round((e.loaded / e.total) * 100); | |
|                 progressBar.style.width = percentComplete + '%'; | |
|                 progressBar.setAttribute('aria-valuenow', percentComplete); | |
|                 progressBar.textContent = percentComplete + '%'; | |
|                 uploadStatus.textContent = `Uploading ${files.length} file(s)... ${percentComplete}%`; | |
|             } | |
|         }); | |
|          | |
|         // Handle completion | |
|         xhr.addEventListener('load', function() { | |
|             if (xhr.status === 200) { | |
|                 try { | |
|                     const response = JSON.parse(xhr.responseText); | |
|                      | |
|                     if (response.uploaded > 0) { | |
|                         if (response.failed === 0) { | |
|                             showSuccessMessage(`Successfully uploaded ${response.uploaded} file(s)`); | |
|                         } else { | |
|                             showSuccessMessage(response.message); | |
|                             // Show details of failed uploads | |
|                             if (response.errors && response.errors.length > 0) { | |
|                                 console.warn('Upload errors:', response.errors); | |
|                             } | |
|                         } | |
|                          | |
|                         // Hide modal and refresh page | |
|                         const modal = bootstrap.Modal.getInstance(document.getElementById('uploadFileModal')); | |
|                         modal.hide(); | |
|                         setTimeout(() => { | |
|                             window.location.reload(); | |
|                         }, 1000); | |
|                     } else { | |
|                         let errorMessage = response.message || 'All file uploads failed'; | |
|                         if (response.errors && response.errors.length > 0) { | |
|                             errorMessage += ': ' + response.errors.join(', '); | |
|                         } | |
|                         showErrorMessage(errorMessage); | |
|                     } | |
|                 } catch (e) { | |
|                     showErrorMessage('Upload completed but response format was unexpected'); | |
|                 } | |
|                 progressContainer.style.display = 'none'; | |
|             } else { | |
|                 let errorMessage = 'Unknown error'; | |
|                 try { | |
|                     const error = JSON.parse(xhr.responseText); | |
|                     errorMessage = error.error || error.message || errorMessage; | |
|                 } catch (e) { | |
|                     errorMessage = `Server returned status ${xhr.status}`; | |
|                 } | |
|                 showErrorMessage(`Failed to upload files: ${errorMessage}`); | |
|                 progressContainer.style.display = 'none'; | |
|             } | |
|         }); | |
|          | |
|         // Handle errors | |
|         xhr.addEventListener('error', function() { | |
|             showErrorMessage('Failed to upload files. Please check your connection and try again.'); | |
|             progressContainer.style.display = 'none'; | |
|         }); | |
|          | |
|         // Handle abort | |
|         xhr.addEventListener('abort', function() { | |
|             showErrorMessage('File upload was cancelled.'); | |
|             progressContainer.style.display = 'none'; | |
|         }); | |
|          | |
|         // Send request | |
|         xhr.open('POST', '/api/files/upload'); | |
|         xhr.send(formData); | |
|          | |
|     } catch (error) { | |
|         console.error('Upload error:', error); | |
|         showErrorMessage('Failed to upload files. Please try again.'); | |
|         progressContainer.style.display = 'none'; | |
|     } finally { | |
|         // Re-enable the button | |
|         submitButton.disabled = false; | |
|         submitButton.innerHTML = originalText; | |
|     } | |
| } | |
| 
 | |
| // Export file list to CSV | |
| function exportFileList() { | |
|     const table = document.getElementById('fileTable'); | |
|     if (!table) { | |
|         showAlert('error', 'File table not found'); | |
|         return; | |
|     } | |
| 
 | |
|     const headers = ['Name', 'Size', 'Type', 'Modified', 'Permissions']; | |
|     const rows = []; | |
| 
 | |
|     // Get table rows | |
|     const tableRows = table.querySelectorAll('tbody tr'); | |
|     tableRows.forEach(row => { | |
|         const cells = row.querySelectorAll('td'); | |
|         if (cells.length >= 6) { | |
|             rows.push([ | |
|                 cells[1].textContent.trim(), // Name | |
|                 cells[2].textContent.trim(), // Size | |
|                 cells[3].textContent.trim(), // Type | |
|                 cells[4].textContent.trim(), // Modified | |
|                 cells[5].textContent.trim()  // Permissions | |
|             ]); | |
|         } | |
|     }); | |
| 
 | |
|     // Generate CSV | |
|     const csvContent = [headers, ...rows] | |
|         .map(row => row.map(cell => `"${cell}"`).join(',')) | |
|         .join('\n'); | |
| 
 | |
|     // Download | |
|     const filename = `seaweedfs-files-${new Date().toISOString().split('T')[0]}.csv`; | |
|     downloadCSV(csvContent, filename); | |
| } | |
| 
 | |
| // Download file | |
| function downloadFile(filePath) { | |
|     // Create download link using admin API | |
|     const downloadUrl = `/api/files/download?path=${encodeURIComponent(filePath)}`; | |
|     window.open(downloadUrl, '_blank'); | |
| } | |
| 
 | |
| // View file | |
| async function viewFile(filePath) { | |
|     try { | |
|         const response = await fetch(`/api/files/view?path=${encodeURIComponent(filePath)}`); | |
|          | |
|         if (!response.ok) { | |
|             const error = await response.json(); | |
|             showAlert('error', `Failed to view file: ${error.error || 'Unknown error'}`); | |
|             return; | |
|         } | |
|          | |
|         const data = await response.json(); | |
|         showFileViewer(data); | |
|          | |
|     } catch (error) { | |
|         console.error('View file error:', error); | |
|         showAlert('error', 'Failed to view file'); | |
|     } | |
| } | |
| 
 | |
| // Show file properties | |
| async function showProperties(filePath) { | |
|     try { | |
|         const response = await fetch(`/api/files/properties?path=${encodeURIComponent(filePath)}`); | |
|          | |
|         if (!response.ok) { | |
|             const error = await response.json(); | |
|             showAlert('error', `Failed to get file properties: ${error.error || 'Unknown error'}`); | |
|             return; | |
|         } | |
|          | |
|         const properties = await response.json(); | |
|         showPropertiesModal(properties); | |
|          | |
|     } catch (error) { | |
|         console.error('Properties error:', error); | |
|         showAlert('error', 'Failed to get file properties'); | |
|     } | |
| } | |
| 
 | |
| // Confirm delete file/folder | |
| function confirmDelete(filePath) { | |
|     if (confirm(`Are you sure you want to delete "${filePath}"?`)) { | |
|         deleteFile(filePath); | |
|     } | |
| } | |
| 
 | |
| // Delete file/folder | |
| async function deleteFile(filePath) { | |
|     try { | |
|         const response = await fetch('/api/files/delete', { | |
|             method: 'DELETE', | |
|             headers: { | |
|                 'Content-Type': 'application/json', | |
|             }, | |
|             body: JSON.stringify({ path: filePath }) | |
|         }); | |
| 
 | |
|         if (response.ok) { | |
|             showAlert('success', `Successfully deleted "${filePath}"`); | |
|             // Reload the page to update the file list | |
|             window.location.reload(); | |
|         } else { | |
|             const error = await response.json(); | |
|             showAlert('error', `Failed to delete file: ${error.error || 'Unknown error'}`); | |
|         } | |
|     } catch (error) { | |
|         console.error('Delete error:', error); | |
|         showAlert('error', 'Failed to delete file'); | |
|     } | |
| } | |
| 
 | |
| // Setup file manager specific event handlers | |
| function setupFileManagerEventHandlers() { | |
|     // Handle Enter key in folder name input | |
|     const folderNameInput = document.getElementById('folderName'); | |
|     if (folderNameInput) { | |
|         folderNameInput.addEventListener('keypress', function(e) { | |
|             if (e.key === 'Enter') { | |
|                 e.preventDefault(); | |
|                 submitCreateFolder(); | |
|             } | |
|         }); | |
|     } | |
|      | |
|     // Handle file selection change to show preview | |
|     const fileInput = document.getElementById('fileInput'); | |
|     if (fileInput) { | |
|         fileInput.addEventListener('change', function(e) { | |
|             updateFileListPreview(); | |
|         }); | |
|     } | |
|      | |
|     // Setup checkbox event listeners for file selection | |
|     const checkboxes = document.querySelectorAll('.file-checkbox'); | |
|     checkboxes.forEach(checkbox => { | |
|         checkbox.addEventListener('change', function() { | |
|             updateDeleteSelectedButton(); | |
|             updateSelectAllCheckbox(); | |
|         }); | |
|     }); | |
|      | |
|     // Setup drag and drop for file uploads | |
|     setupDragAndDrop(); | |
|      | |
|     // Clear form when modals are hidden | |
|     const createFolderModal = document.getElementById('createFolderModal'); | |
|     if (createFolderModal) { | |
|         createFolderModal.addEventListener('hidden.bs.modal', function() { | |
|             document.getElementById('folderName').value = ''; | |
|         }); | |
|     } | |
|      | |
|     const uploadFileModal = document.getElementById('uploadFileModal'); | |
|     if (uploadFileModal) { | |
|         uploadFileModal.addEventListener('hidden.bs.modal', function() { | |
|             const fileInput = document.getElementById('fileInput'); | |
|             const progressContainer = document.getElementById('uploadProgress'); | |
|             const fileListPreview = document.getElementById('fileListPreview'); | |
|             fileInput.value = ''; | |
|             progressContainer.style.display = 'none'; | |
|             fileListPreview.style.display = 'none'; | |
|         }); | |
|     } | |
| } | |
| 
 | |
| // Setup drag and drop functionality | |
| function setupDragAndDrop() { | |
|     const dropZone = document.querySelector('.card-body'); // Main file listing area | |
|     const uploadModal = document.getElementById('uploadFileModal'); | |
|      | |
|     if (!dropZone || !uploadModal) return; | |
|      | |
|     // Prevent default drag behaviors | |
|     ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
|         dropZone.addEventListener(eventName, preventDefaults, false); | |
|         document.body.addEventListener(eventName, preventDefaults, false); | |
|     }); | |
|      | |
|     // Highlight drop zone when item is dragged over it | |
|     ['dragenter', 'dragover'].forEach(eventName => { | |
|         dropZone.addEventListener(eventName, highlight, false); | |
|     }); | |
|      | |
|     ['dragleave', 'drop'].forEach(eventName => { | |
|         dropZone.addEventListener(eventName, unhighlight, false); | |
|     }); | |
|      | |
|     // Handle dropped files | |
|     dropZone.addEventListener('drop', handleDrop, false); | |
|      | |
|     function preventDefaults(e) { | |
|         e.preventDefault(); | |
|         e.stopPropagation(); | |
|     } | |
|      | |
|     function highlight(e) { | |
|         dropZone.classList.add('drag-over'); | |
|         // Add some visual feedback | |
|         if (!dropZone.querySelector('.drag-overlay')) { | |
|             const overlay = document.createElement('div'); | |
|             overlay.className = 'drag-overlay'; | |
|             overlay.innerHTML = ` | |
|                 <div class="text-center p-5"> | |
|                     <i class="fas fa-cloud-upload-alt fa-3x text-primary mb-3"></i> | |
|                     <h5>Drop files here to upload</h5> | |
|                     <p class="text-muted">Release to upload files to this directory</p> | |
|                 </div> | |
|             `; | |
|             overlay.style.cssText = ` | |
|                 position: absolute; | |
|                 top: 0; | |
|                 left: 0; | |
|                 right: 0; | |
|                 bottom: 0; | |
|                 background: rgba(255, 255, 255, 0.9); | |
|                 border: 2px dashed #007bff; | |
|                 border-radius: 0.375rem; | |
|                 z-index: 1000; | |
|                 display: flex; | |
|                 align-items: center; | |
|                 justify-content: center; | |
|             `; | |
|             dropZone.style.position = 'relative'; | |
|             dropZone.appendChild(overlay); | |
|         } | |
|     } | |
|      | |
|     function unhighlight(e) { | |
|         dropZone.classList.remove('drag-over'); | |
|         const overlay = dropZone.querySelector('.drag-overlay'); | |
|         if (overlay) { | |
|             overlay.remove(); | |
|         } | |
|     } | |
|      | |
|     function handleDrop(e) { | |
|         const dt = e.dataTransfer; | |
|         const files = dt.files; | |
|          | |
|         if (files.length > 0) { | |
|             // Open upload modal and set files | |
|             const fileInput = document.getElementById('fileInput'); | |
|             if (fileInput) { | |
|                 // Create a new FileList-like object | |
|                 const fileArray = Array.from(files); | |
|                  | |
|                 // Set files to input (this is a bit tricky with file inputs) | |
|                 const dataTransfer = new DataTransfer(); | |
|                 fileArray.forEach(file => dataTransfer.items.add(file)); | |
|                 fileInput.files = dataTransfer.files; | |
|                  | |
|                 // Update preview and show modal | |
|                 updateFileListPreview(); | |
|                 const modal = new bootstrap.Modal(uploadModal); | |
|                 modal.show(); | |
|             } | |
|         } | |
|     } | |
| } | |
| 
 | |
| // Update file list preview when files are selected | |
| function updateFileListPreview() { | |
|     const fileInput = document.getElementById('fileInput'); | |
|     const fileListPreview = document.getElementById('fileListPreview'); | |
|     const selectedFilesList = document.getElementById('selectedFilesList'); | |
|      | |
|     if (!fileInput.files || fileInput.files.length === 0) { | |
|         fileListPreview.style.display = 'none'; | |
|         return; | |
|     } | |
|      | |
|     const files = Array.from(fileInput.files); | |
|     const totalSize = files.reduce((sum, file) => sum + file.size, 0); | |
|      | |
|     let html = `<div class="d-flex justify-content-between align-items-center mb-2"> | |
|         <strong>${files.length} file(s) selected</strong> | |
|         <small class="text-muted">Total: ${formatBytes(totalSize)}</small> | |
|     </div>`; | |
|      | |
|     files.forEach((file, index) => { | |
|         const fileIcon = getFileIconByName(file.name); | |
|         html += `<div class="d-flex justify-content-between align-items-center py-1 ${index > 0 ? 'border-top' : ''}"> | |
|             <div class="d-flex align-items-center"> | |
|                 <i class="fas ${fileIcon} me-2 text-muted"></i> | |
|                 <span class="text-truncate" style="max-width: 200px;" title="${file.name}">${file.name}</span> | |
|             </div> | |
|             <small class="text-muted">${formatBytes(file.size)}</small> | |
|         </div>`; | |
|     }); | |
|      | |
|     selectedFilesList.innerHTML = html; | |
|     fileListPreview.style.display = 'block'; | |
| } | |
| 
 | |
| // Get file icon based on file name/extension | |
| function getFileIconByName(fileName) { | |
|     const ext = fileName.split('.').pop().toLowerCase(); | |
|      | |
|     switch (ext) { | |
|         case 'jpg': | |
|         case 'jpeg': | |
|         case 'png': | |
|         case 'gif': | |
|         case 'bmp': | |
|         case 'svg': | |
|             return 'fa-image'; | |
|         case 'mp4': | |
|         case 'avi': | |
|         case 'mov': | |
|         case 'wmv': | |
|         case 'flv': | |
|             return 'fa-video'; | |
|         case 'mp3': | |
|         case 'wav': | |
|         case 'flac': | |
|         case 'aac': | |
|             return 'fa-music'; | |
|         case 'pdf': | |
|             return 'fa-file-pdf'; | |
|         case 'doc': | |
|         case 'docx': | |
|             return 'fa-file-word'; | |
|         case 'xls': | |
|         case 'xlsx': | |
|             return 'fa-file-excel'; | |
|         case 'ppt': | |
|         case 'pptx': | |
|             return 'fa-file-powerpoint'; | |
|         case 'txt': | |
|         case 'md': | |
|             return 'fa-file-text'; | |
|         case 'zip': | |
|         case 'rar': | |
|         case '7z': | |
|         case 'tar': | |
|         case 'gz': | |
|             return 'fa-file-archive'; | |
|         case 'js': | |
|         case 'ts': | |
|         case 'html': | |
|         case 'css': | |
|         case 'json': | |
|         case 'xml': | |
|             return 'fa-file-code'; | |
|         default: | |
|             return 'fa-file'; | |
|     } | |
| } | |
| 
 | |
| // Quota Management Functions | |
|  | |
| // Show quota management modal | |
| function showQuotaModal(bucketName, currentQuotaMB, quotaEnabled) { | |
|     document.getElementById('quotaBucketName').value = bucketName; | |
|     document.getElementById('quotaEnabled').checked = quotaEnabled; | |
|      | |
|     // Convert quota to appropriate unit and set values | |
|     const quotaBytes = currentQuotaMB * 1024 * 1024; // Convert MB to bytes | |
|     const { size, unit } = convertBytesToBestUnit(quotaBytes); | |
|      | |
|     document.getElementById('quotaSizeMB').value = size; | |
|     document.getElementById('quotaUnitMB').value = unit; | |
|      | |
|     // Show/hide quota size settings based on enabled state | |
|     const quotaSizeSettings = document.getElementById('quotaSizeSettings'); | |
|     if (quotaEnabled) { | |
|         quotaSizeSettings.style.display = 'block'; | |
|     } else { | |
|         quotaSizeSettings.style.display = 'none'; | |
|     } | |
|      | |
|     const modal = new bootstrap.Modal(document.getElementById('manageQuotaModal')); | |
|     modal.show(); | |
| } | |
| 
 | |
| // Convert bytes to the best unit (TB, GB, or MB) | |
| function convertBytesToBestUnit(bytes) { | |
|     if (bytes === 0) { | |
|         return { size: 0, unit: 'MB' }; | |
|     } | |
|      | |
|     // Check if it's a clean TB value | |
|     if (bytes >= 1024 * 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024 * 1024) === 0) { | |
|         return { size: bytes / (1024 * 1024 * 1024 * 1024), unit: 'TB' }; | |
|     } | |
|      | |
|     // Check if it's a clean GB value | |
|     if (bytes >= 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024) === 0) { | |
|         return { size: bytes / (1024 * 1024 * 1024), unit: 'GB' }; | |
|     } | |
|      | |
|     // Default to MB | |
|     return { size: bytes / (1024 * 1024), unit: 'MB' }; | |
| } | |
| 
 | |
| // Handle quota update form submission | |
| async function handleUpdateQuota(event) { | |
|     event.preventDefault(); | |
|      | |
|     const form = event.target; | |
|     const formData = new FormData(form); | |
|     const bucketName = document.getElementById('quotaBucketName').value; | |
|      | |
|     const quotaData = { | |
|         quota_enabled: formData.get('quota_enabled') === 'on', | |
|         quota_size: parseInt(formData.get('quota_size')) || 0, | |
|         quota_unit: formData.get('quota_unit') || 'MB' | |
|     }; | |
| 
 | |
|     try { | |
|         const response = await fetch(`/api/s3/buckets/${bucketName}/quota`, { | |
|             method: 'PUT', | |
|             headers: { | |
|                 'Content-Type': 'application/json', | |
|             }, | |
|             body: JSON.stringify(quotaData) | |
|         }); | |
| 
 | |
|         const result = await response.json(); | |
| 
 | |
|         if (response.ok) { | |
|             // Success | |
|             showAlert('success', `Quota for bucket "${bucketName}" updated successfully!`); | |
|              | |
|             // Close modal | |
|             const modal = bootstrap.Modal.getInstance(document.getElementById('manageQuotaModal')); | |
|             modal.hide(); | |
|              | |
|             // Refresh the page after a short delay | |
|             setTimeout(() => { | |
|                 location.reload(); | |
|             }, 1500); | |
|         } else { | |
|             // Error | |
|             showAlert('danger', result.error || 'Failed to update bucket quota'); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error updating bucket quota:', error); | |
|         showAlert('danger', 'Network error occurred while updating bucket quota'); | |
|     } | |
| } | |
| 
 | |
| // Show file viewer modal | |
| function showFileViewer(data) { | |
|     const file = data.file; | |
|     const content = data.content || ''; | |
|     const viewable = data.viewable !== false; | |
|      | |
|     // Create modal HTML | |
|     const modalHtml = ` | |
|         <div class="modal fade" id="fileViewerModal" tabindex="-1" aria-labelledby="fileViewerModalLabel" aria-hidden="true"> | |
|             <div class="modal-dialog modal-xl"> | |
|                 <div class="modal-content"> | |
|                     <div class="modal-header"> | |
|                         <h5 class="modal-title" id="fileViewerModalLabel"> | |
|                             <i class="fas fa-eye me-2"></i>File Viewer: ${file.name} | |
|                         </h5> | |
|                         <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
|                     </div> | |
|                     <div class="modal-body"> | |
|                         ${viewable ? createFileViewerContent(file, content) : createNonViewableContent(data.reason || 'File cannot be viewed')} | |
|                     </div> | |
|                     <div class="modal-footer"> | |
|                         <button type="button" class="btn btn-primary" onclick="downloadFile('${file.full_path}')"> | |
|                             <i class="fas fa-download me-1"></i>Download | |
|                         </button> | |
|                         <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
|     `; | |
|      | |
|     // Remove existing modal if any | |
|     const existingModal = document.getElementById('fileViewerModal'); | |
|     if (existingModal) { | |
|         existingModal.remove(); | |
|     } | |
|      | |
|     // Add modal to DOM | |
|     document.body.insertAdjacentHTML('beforeend', modalHtml); | |
|      | |
|     // Show modal | |
|     const modal = new bootstrap.Modal(document.getElementById('fileViewerModal')); | |
|     modal.show(); | |
|      | |
|     // Clean up when modal is hidden | |
|     document.getElementById('fileViewerModal').addEventListener('hidden.bs.modal', function () { | |
|         this.remove(); | |
|     }); | |
| } | |
| 
 | |
| // Create file viewer content based on file type | |
| function createFileViewerContent(file, content) { | |
|     if (file.mime.startsWith('image/')) { | |
|         return ` | |
|             <div class="text-center"> | |
|                 <img src="/api/files/download?path=${encodeURIComponent(file.full_path)}"  | |
|                      class="img-fluid" alt="${file.name}" style="max-height: 500px;"> | |
|             </div> | |
|         `; | |
|     } else if (file.mime.startsWith('text/') || file.mime === 'application/json' || file.mime === 'application/javascript') { | |
|         const language = getLanguageFromMime(file.mime, file.name); | |
|         return ` | |
|             <div class="mb-3"> | |
|                 <small class="text-muted"> | |
|                     <i class="fas fa-info-circle me-1"></i> | |
|                     Size: ${formatBytes(file.size)} | Type: ${file.mime} | |
|                 </small> | |
|             </div> | |
|             <pre><code class="language-${language}" style="max-height: 400px; overflow-y: auto;">${escapeHtml(content)}</code></pre> | |
|         `; | |
|     } else if (file.mime === 'application/pdf') { | |
|         return ` | |
|             <div class="text-center"> | |
|                 <embed src="/api/files/download?path=${encodeURIComponent(file.full_path)}"  | |
|                        type="application/pdf" width="100%" height="500px"> | |
|             </div> | |
|         `; | |
|     } else { | |
|         return createNonViewableContent('This file type cannot be previewed in the browser.'); | |
|     } | |
| } | |
| 
 | |
| // Create non-viewable content message | |
| function createNonViewableContent(reason) { | |
|     return ` | |
|         <div class="text-center py-5"> | |
|             <i class="fas fa-file fa-3x text-muted mb-3"></i> | |
|             <h5 class="text-muted">Cannot preview file</h5> | |
|             <p class="text-muted">${reason}</p> | |
|         </div> | |
|     `; | |
| } | |
| 
 | |
| // Get language for syntax highlighting | |
| function getLanguageFromMime(mime, filename) { | |
|     // First check MIME type | |
|     switch (mime) { | |
|         case 'application/json': return 'json'; | |
|         case 'application/javascript': return 'javascript'; | |
|         case 'text/html': return 'html'; | |
|         case 'text/css': return 'css'; | |
|         case 'application/xml': return 'xml'; | |
|         case 'text/typescript': return 'typescript'; | |
|         case 'text/x-python': return 'python'; | |
|         case 'text/x-go': return 'go'; | |
|         case 'text/x-java': return 'java'; | |
|         case 'text/x-c': return 'c'; | |
|         case 'text/x-c++': return 'cpp'; | |
|         case 'text/x-c-header': return 'c'; | |
|         case 'text/x-shellscript': return 'bash'; | |
|         case 'text/x-php': return 'php'; | |
|         case 'text/x-ruby': return 'ruby'; | |
|         case 'text/x-perl': return 'perl'; | |
|         case 'text/x-rust': return 'rust'; | |
|         case 'text/x-swift': return 'swift'; | |
|         case 'text/x-kotlin': return 'kotlin'; | |
|         case 'text/x-scala': return 'scala'; | |
|         case 'text/x-dockerfile': return 'dockerfile'; | |
|         case 'text/yaml': return 'yaml'; | |
|         case 'text/csv': return 'csv'; | |
|         case 'text/sql': return 'sql'; | |
|         case 'text/markdown': return 'markdown'; | |
|     } | |
|      | |
|     // Fallback to file extension | |
|     const ext = filename.split('.').pop().toLowerCase(); | |
|     switch (ext) { | |
|         case 'js': case 'mjs': return 'javascript'; | |
|         case 'ts': return 'typescript'; | |
|         case 'py': return 'python'; | |
|         case 'go': return 'go'; | |
|         case 'java': return 'java'; | |
|         case 'cpp': case 'cc': case 'cxx': case 'c++': return 'cpp'; | |
|         case 'c': return 'c'; | |
|         case 'h': case 'hpp': return 'c'; | |
|         case 'sh': case 'bash': case 'zsh': case 'fish': return 'bash'; | |
|         case 'php': return 'php'; | |
|         case 'rb': return 'ruby'; | |
|         case 'pl': return 'perl'; | |
|         case 'rs': return 'rust'; | |
|         case 'swift': return 'swift'; | |
|         case 'kt': return 'kotlin'; | |
|         case 'scala': return 'scala'; | |
|         case 'yml': case 'yaml': return 'yaml'; | |
|         case 'md': case 'markdown': return 'markdown'; | |
|         case 'sql': return 'sql'; | |
|         case 'csv': return 'csv'; | |
|         case 'dockerfile': return 'dockerfile'; | |
|         case 'gitignore': case 'gitattributes': return 'text'; | |
|         case 'env': return 'bash'; | |
|         case 'cfg': case 'conf': case 'ini': case 'properties': return 'ini'; | |
|         default: return 'text'; | |
|     } | |
| } | |
| 
 | |
| // Show properties modal | |
| function showPropertiesModal(properties) { | |
|     // Create modal HTML | |
|     const modalHtml = ` | |
|         <div class="modal fade" id="propertiesModal" tabindex="-1" aria-labelledby="propertiesModalLabel" aria-hidden="true"> | |
|             <div class="modal-dialog modal-lg"> | |
|                 <div class="modal-content"> | |
|                     <div class="modal-header"> | |
|                         <h5 class="modal-title" id="propertiesModalLabel"> | |
|                             <i class="fas fa-info me-2"></i>Properties: ${properties.name} | |
|                         </h5> | |
|                         <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
|                     </div> | |
|                     <div class="modal-body"> | |
|                         ${createPropertiesContent(properties)} | |
|                     </div> | |
|                     <div class="modal-footer"> | |
|                         <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
|     `; | |
|      | |
|     // Remove existing modal if any | |
|     const existingModal = document.getElementById('propertiesModal'); | |
|     if (existingModal) { | |
|         existingModal.remove(); | |
|     } | |
|      | |
|     // Add modal to DOM | |
|     document.body.insertAdjacentHTML('beforeend', modalHtml); | |
|      | |
|     // Show modal | |
|     const modal = new bootstrap.Modal(document.getElementById('propertiesModal')); | |
|     modal.show(); | |
|      | |
|     // Clean up when modal is hidden | |
|     document.getElementById('propertiesModal').addEventListener('hidden.bs.modal', function () { | |
|         this.remove(); | |
|     }); | |
| } | |
| 
 | |
| // Create properties content | |
| function createPropertiesContent(properties) { | |
|     let html = ` | |
|         <div class="row"> | |
|             <div class="col-md-6"> | |
|                 <h6 class="text-primary"><i class="fas fa-file me-1"></i>Basic Information</h6> | |
|                 <table class="table table-sm"> | |
|                     <tr><td><strong>Name:</strong></td><td>${properties.name}</td></tr> | |
|                     <tr><td><strong>Full Path:</strong></td><td><code>${properties.full_path}</code></td></tr> | |
|                     <tr><td><strong>Type:</strong></td><td>${properties.is_directory ? 'Directory' : 'File'}</td></tr> | |
|     `; | |
|      | |
|     if (!properties.is_directory) { | |
|         html += ` | |
|                     <tr><td><strong>Size:</strong></td><td>${properties.size_formatted || formatBytes(properties.size || 0)}</td></tr> | |
|                     <tr><td><strong>MIME Type:</strong></td><td>${properties.mime_type || 'Unknown'}</td></tr> | |
|         `; | |
|     } | |
|      | |
|     html += ` | |
|                 </table> | |
|             </div> | |
|             <div class="col-md-6"> | |
|                 <h6 class="text-primary"><i class="fas fa-clock me-1"></i>Timestamps</h6> | |
|                 <table class="table table-sm"> | |
|     `; | |
|      | |
|     if (properties.modified_time) { | |
|         html += `<tr><td><strong>Modified:</strong></td><td>${properties.modified_time}</td></tr>`; | |
|     } | |
|     if (properties.created_time) { | |
|         html += `<tr><td><strong>Created:</strong></td><td>${properties.created_time}</td></tr>`; | |
|     } | |
|      | |
|     html += ` | |
|                 </table> | |
|                  | |
|                 <h6 class="text-primary"><i class="fas fa-shield-alt me-1"></i>Permissions</h6> | |
|                 <table class="table table-sm"> | |
|                     <tr><td><strong>Mode:</strong></td><td><code>${properties.file_mode_formatted || properties.file_mode}</code></td></tr> | |
|                     <tr><td><strong>UID:</strong></td><td>${properties.uid || 'N/A'}</td></tr> | |
|                     <tr><td><strong>GID:</strong></td><td>${properties.gid || 'N/A'}</td></tr> | |
|                 </table> | |
|             </div> | |
|         </div> | |
|     `; | |
|      | |
|     // Add TTL information if available | |
|     if (properties.ttl_seconds && properties.ttl_seconds > 0) { | |
|         html += ` | |
|             <div class="row mt-3"> | |
|                 <div class="col-12"> | |
|                     <h6 class="text-primary"><i class="fas fa-hourglass-half me-1"></i>TTL (Time To Live)</h6> | |
|                     <table class="table table-sm"> | |
|                         <tr><td><strong>TTL:</strong></td><td>${properties.ttl_formatted || properties.ttl_seconds + ' seconds'}</td></tr> | |
|                     </table> | |
|                 </div> | |
|             </div> | |
|         `; | |
|     } | |
|      | |
|     // Add chunk information if available | |
|     if (properties.chunks && properties.chunks.length > 0) { | |
|         html += ` | |
|             <div class="row mt-3"> | |
|                 <div class="col-12"> | |
|                     <h6 class="text-primary"><i class="fas fa-puzzle-piece me-1"></i>Chunks (${properties.chunk_count})</h6> | |
|                     <div class="table-responsive" style="max-height: 200px; overflow-y: auto;"> | |
|                         <table class="table table-sm"> | |
|                             <thead> | |
|                                 <tr> | |
|                                     <th>File ID</th> | |
|                                     <th>Offset</th> | |
|                                     <th>Size</th> | |
|                                     <th>ETag</th> | |
|                                 </tr> | |
|                             </thead> | |
|                             <tbody> | |
|         `; | |
|          | |
|         properties.chunks.forEach(chunk => { | |
|             html += ` | |
|                                 <tr> | |
|                                     <td><code class="small">${chunk.file_id}</code></td> | |
|                                     <td>${formatBytes(chunk.offset)}</td> | |
|                                     <td>${formatBytes(chunk.size)}</td> | |
|                                     <td><code class="small">${chunk.e_tag || 'N/A'}</code></td> | |
|                                 </tr> | |
|             `; | |
|         }); | |
|          | |
|         html += ` | |
|                             </tbody> | |
|                         </table> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         `; | |
|     } | |
|      | |
|     // Add extended attributes if available | |
|     if (properties.extended && Object.keys(properties.extended).length > 0) { | |
|         html += ` | |
|             <div class="row mt-3"> | |
|                 <div class="col-12"> | |
|                     <h6 class="text-primary"><i class="fas fa-tags me-1"></i>Extended Attributes</h6> | |
|                     <table class="table table-sm"> | |
|         `; | |
|          | |
|         Object.entries(properties.extended).forEach(([key, value]) => { | |
|             html += `<tr><td><strong>${key}:</strong></td><td>${value}</td></tr>`; | |
|         }); | |
|          | |
|         html += ` | |
|                     </table> | |
|                 </div> | |
|             </div> | |
|         `; | |
|     } | |
|      | |
|     return html; | |
| } | |
| 
 | |
| // Utility function to escape HTML | |
| function escapeHtml(text) { | |
|     var map = { | |
|         '&': '&', | |
|         '<': '<', | |
|         '>': '>', | |
|         '"': '"', | |
|         "'": ''' | |
|     }; | |
|     return text.replace(/[&<>"']/g, function(m) { return map[m]; }); | |
| } | |
| 
 | |
| // ============================================================================ | |
| // USER MANAGEMENT FUNCTIONS | |
| // ============================================================================ | |
|  | |
| // Global variables for user management | |
| let currentEditingUser = ''; | |
| let currentAccessKeysUser = ''; | |
| 
 | |
| // User Management Functions | |
|  | |
| async function handleCreateUser() { | |
|     const form = document.getElementById('createUserForm'); | |
|     const formData = new FormData(form); | |
|      | |
|     // Get selected actions | |
|     const actionsSelect = document.getElementById('actions'); | |
|     const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value); | |
|      | |
|     const userData = { | |
|         username: formData.get('username'), | |
|         email: formData.get('email'), | |
|         actions: selectedActions, | |
|         generate_key: formData.get('generateKey') === 'on' | |
|     }; | |
|      | |
|     try { | |
|         const response = await fetch('/api/users', { | |
|             method: 'POST', | |
|             headers: { | |
|                 'Content-Type': 'application/json', | |
|             }, | |
|             body: JSON.stringify(userData) | |
|         }); | |
|          | |
|         if (response.ok) { | |
|             const result = await response.json(); | |
|             showSuccessMessage('User created successfully'); | |
|              | |
|             // Show the created access key if generated | |
|             if (result.user && result.user.access_key) { | |
|                 showNewAccessKeyModal(result.user); | |
|             } | |
|              | |
|             // Close modal and refresh page | |
|             const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal')); | |
|             modal.hide(); | |
|             form.reset(); | |
|             setTimeout(() => window.location.reload(), 1000); | |
|         } else { | |
|             const error = await response.json(); | |
|             showErrorMessage('Failed to create user: ' + (error.error || 'Unknown error')); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error creating user:', error); | |
|         showErrorMessage('Failed to create user: ' + error.message); | |
|     } | |
| } | |
| 
 | |
| async function editUser(username) { | |
|     currentEditingUser = username; | |
|      | |
|     try { | |
|         const response = await fetch(`/api/users/${username}`); | |
|         if (response.ok) { | |
|             const user = await response.json(); | |
|              | |
|             // Populate edit form | |
|             document.getElementById('editUsername').value = username; | |
|                 document.getElementById('editEmail').value = user.email || ''; | |
|              | |
|             // Set selected actions | |
|             const actionsSelect = document.getElementById('editActions'); | |
|             Array.from(actionsSelect.options).forEach(option => { | |
|                 option.selected = user.actions && user.actions.includes(option.value); | |
|             }); | |
|              | |
|             // Show modal | |
|             const modal = new bootstrap.Modal(document.getElementById('editUserModal')); | |
|             modal.show(); | |
|         } else { | |
|             showErrorMessage('Failed to load user details'); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error loading user:', error); | |
|         showErrorMessage('Failed to load user details'); | |
|     } | |
| } | |
| 
 | |
| async function handleUpdateUser() { | |
|     const form = document.getElementById('editUserForm'); | |
|     const formData = new FormData(form); | |
|      | |
|     // Get selected actions | |
|     const actionsSelect = document.getElementById('editActions'); | |
|     const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value); | |
|      | |
|     const userData = { | |
|         email: formData.get('email'), | |
|         actions: selectedActions | |
|     }; | |
|      | |
|     try { | |
|         const response = await fetch(`/api/users/${currentEditingUser}`, { | |
|             method: 'PUT', | |
|             headers: { | |
|                 'Content-Type': 'application/json', | |
|             }, | |
|             body: JSON.stringify(userData) | |
|         }); | |
|          | |
|         if (response.ok) { | |
|             showSuccessMessage('User updated successfully'); | |
|              | |
|             // Close modal and refresh page | |
|             const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal')); | |
|             modal.hide(); | |
|             setTimeout(() => window.location.reload(), 1000); | |
|         } else { | |
|             const error = await response.json(); | |
|             showErrorMessage('Failed to update user: ' + (error.error || 'Unknown error')); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error updating user:', error); | |
|         showErrorMessage('Failed to update user: ' + error.message); | |
|     } | |
| } | |
| 
 | |
| function confirmDeleteUser(username) { | |
|     confirmAction( | |
|         `Are you sure you want to delete user "${username}"? This action cannot be undone.`, | |
|         () => deleteUserConfirmed(username) | |
|     ); | |
| } | |
| 
 | |
| function deleteUser(username) { | |
|     confirmDeleteUser(username); | |
| } | |
| 
 | |
| async function deleteUserConfirmed(username) { | |
|     try { | |
|         const response = await fetch(`/api/users/${username}`, { | |
|             method: 'DELETE' | |
|         }); | |
|          | |
|         if (response.ok) { | |
|             showSuccessMessage('User deleted successfully'); | |
|             setTimeout(() => window.location.reload(), 1000); | |
|         } else { | |
|             const error = await response.json(); | |
|             showErrorMessage('Failed to delete user: ' + (error.error || 'Unknown error')); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error deleting user:', error); | |
|         showErrorMessage('Failed to delete user: ' + error.message); | |
|     } | |
| } | |
| 
 | |
| async function showUserDetails(username) { | |
|     try { | |
|         const response = await fetch(`/api/users/${username}`); | |
|         if (response.ok) { | |
|             const user = await response.json(); | |
|              | |
|             const content = createUserDetailsContent(user); | |
|             document.getElementById('userDetailsContent').innerHTML = content; | |
|              | |
|             const modal = new bootstrap.Modal(document.getElementById('userDetailsModal')); | |
|             modal.show(); | |
|         } else { | |
|             showErrorMessage('Failed to load user details'); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error loading user details:', error); | |
|         showErrorMessage('Failed to load user details'); | |
|     } | |
| } | |
| 
 | |
| function createUserDetailsContent(user) { | |
|     return ` | |
|         <div class="row"> | |
|             <div class="col-md-6"> | |
|                 <h6 class="text-muted">Basic Information</h6> | |
|                 <table class="table table-sm"> | |
|                     <tr> | |
|                         <td><strong>Username:</strong></td> | |
|                         <td>${escapeHtml(user.username)}</td> | |
|                     </tr> | |
|                     <tr> | |
|                         <td><strong>Email:</strong></td> | |
|                         <td>${escapeHtml(user.email || 'Not set')}</td> | |
|                     </tr> | |
|                 </table> | |
|             </div> | |
|             <div class="col-md-6"> | |
|                 <h6 class="text-muted">Permissions</h6> | |
|                 <div class="mb-3"> | |
|                     ${user.actions && user.actions.length > 0 ?  | |
|                         user.actions.map(action => `<span class="badge bg-info me-1">${action}</span>`).join('') : | |
|                         '<span class="text-muted">No permissions assigned</span>' | |
|                     } | |
|                 </div> | |
|                  | |
|                 <h6 class="text-muted">Access Keys</h6> | |
|                 ${user.access_keys && user.access_keys.length > 0 ?  | |
|                     createAccessKeysTable(user.access_keys) : | |
|                     '<p class="text-muted">No access keys</p>' | |
|                 } | |
|             </div> | |
|         </div> | |
|     `; | |
| } | |
| 
 | |
| function createAccessKeysTable(accessKeys) { | |
|     return ` | |
|         <div class="table-responsive"> | |
|             <table class="table table-sm"> | |
|                 <thead> | |
|                     <tr> | |
|                         <th>Access Key</th> | |
|                         <th>Created</th> | |
|                     </tr> | |
|                 </thead> | |
|                 <tbody> | |
|                     ${accessKeys.map(key => ` | |
|                         <tr> | |
|                             <td><code>${key.access_key}</code></td> | |
|                             <td>${new Date(key.created_at).toLocaleDateString()}</td> | |
|                         </tr> | |
|                     `).join('')} | |
|                 </tbody> | |
|             </table> | |
|         </div> | |
|     `; | |
| } | |
| 
 | |
| async function manageAccessKeys(username) { | |
|     currentAccessKeysUser = username; | |
|     document.getElementById('accessKeysUsername').textContent = username; | |
|      | |
|     await loadAccessKeys(username); | |
|      | |
|     const modal = new bootstrap.Modal(document.getElementById('accessKeysModal')); | |
|     modal.show(); | |
| } | |
| 
 | |
| async function loadAccessKeys(username) { | |
|     try { | |
|         const response = await fetch(`/api/users/${username}`); | |
|         if (response.ok) { | |
|             const user = await response.json(); | |
|              | |
|             const content = createAccessKeysManagementContent(user.access_keys || []); | |
|             document.getElementById('accessKeysContent').innerHTML = content; | |
|         } else { | |
|             document.getElementById('accessKeysContent').innerHTML = '<p class="text-muted">Failed to load access keys</p>'; | |
|         } | |
|     } catch (error) { | |
|         console.error('Error loading access keys:', error); | |
|         document.getElementById('accessKeysContent').innerHTML = '<p class="text-muted">Error loading access keys</p>'; | |
|     } | |
| } | |
| 
 | |
| function createAccessKeysManagementContent(accessKeys) { | |
|     if (accessKeys.length === 0) { | |
|         return '<p class="text-muted">No access keys found. Create one to get started.</p>'; | |
|     } | |
|      | |
|     return ` | |
|         <div class="table-responsive"> | |
|             <table class="table table-hover"> | |
|                 <thead> | |
|                     <tr> | |
|                         <th>Access Key</th> | |
|                         <th>Secret Key</th> | |
|                         <th>Created</th> | |
|                         <th>Actions</th> | |
|                     </tr> | |
|                 </thead> | |
|                 <tbody> | |
|                     ${accessKeys.map(key => ` | |
|                         <tr> | |
|                             <td> | |
|                                 <code>${key.access_key}</code> | |
|                                 <button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('${key.access_key}')"> | |
|                                     <i class="fas fa-copy"></i> | |
|                                 </button> | |
|                             </td> | |
|                             <td> | |
|                                 <code class="text-muted">••••••••••••••••</code> | |
|                                 <button class="btn btn-sm btn-outline-secondary ms-2" onclick="showSecretKey('${key.access_key}', '${key.secret_key}')"> | |
|                                     <i class="fas fa-eye"></i> | |
|                                 </button> | |
|                             </td> | |
|                             <td>${new Date(key.created_at).toLocaleDateString()}</td> | |
|                             <td> | |
|                                 <button class="btn btn-sm btn-outline-danger" onclick="confirmDeleteAccessKey('${key.access_key}')"> | |
|                                     <i class="fas fa-trash"></i> | |
|                                 </button> | |
|                             </td> | |
|                         </tr> | |
|                     `).join('')} | |
|                 </tbody> | |
|             </table> | |
|         </div> | |
|     `; | |
| } | |
| 
 | |
| async function createAccessKey() { | |
|     if (!currentAccessKeysUser) { | |
|         showErrorMessage('No user selected'); | |
|         return; | |
|     } | |
|      | |
|     try { | |
|         const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys`, { | |
|             method: 'POST', | |
|             headers: { | |
|                 'Content-Type': 'application/json', | |
|             } | |
|         }); | |
|          | |
|         if (response.ok) { | |
|             const result = await response.json(); | |
|             showSuccessMessage('Access key created successfully'); | |
|              | |
|             // Show the new access key | |
|             showNewAccessKeyModal(result.access_key); | |
|              | |
|             // Reload access keys | |
|             await loadAccessKeys(currentAccessKeysUser); | |
|         } else { | |
|             const error = await response.json(); | |
|             showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error')); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error creating access key:', error); | |
|         showErrorMessage('Failed to create access key: ' + error.message); | |
|     } | |
| } | |
| 
 | |
| function confirmDeleteAccessKey(accessKeyId) { | |
|     confirmAction( | |
|         `Are you sure you want to delete access key "${accessKeyId}"? This action cannot be undone.`, | |
|         () => deleteAccessKeyConfirmed(accessKeyId) | |
|     ); | |
| } | |
| 
 | |
| async function deleteAccessKeyConfirmed(accessKeyId) { | |
|     try { | |
|         const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys/${accessKeyId}`, { | |
|             method: 'DELETE' | |
|         }); | |
|          | |
|         if (response.ok) { | |
|             showSuccessMessage('Access key deleted successfully'); | |
|              | |
|             // Reload access keys | |
|             await loadAccessKeys(currentAccessKeysUser); | |
|         } else { | |
|             const error = await response.json(); | |
|             showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error')); | |
|         } | |
|     } catch (error) { | |
|         console.error('Error deleting access key:', error); | |
|         showErrorMessage('Failed to delete access key: ' + error.message); | |
|     } | |
| } | |
| 
 | |
| function showSecretKey(accessKey, secretKey) { | |
|     const content = ` | |
|         <div class="alert alert-info"> | |
|             <i class="fas fa-info-circle me-2"></i> | |
|             <strong>Access Key Details:</strong> These credentials provide access to your object storage. Keep them secure and don't share them. | |
|         </div> | |
|         <div class="mb-3"> | |
|             <label class="form-label"><strong>Access Key:</strong></label> | |
|             <div class="input-group"> | |
|                 <input type="text" class="form-control" value="${accessKey}" readonly> | |
|                 <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKey}')"> | |
|                     <i class="fas fa-copy"></i> | |
|                 </button> | |
|             </div> | |
|         </div> | |
|         <div class="mb-3"> | |
|             <label class="form-label"><strong>Secret Key:</strong></label> | |
|             <div class="input-group"> | |
|                 <input type="text" class="form-control" value="${secretKey}" readonly> | |
|                 <button class="btn btn-outline-secondary" onclick="copyToClipboard('${secretKey}')"> | |
|                     <i class="fas fa-copy"></i> | |
|                 </button> | |
|             </div> | |
|         </div> | |
|     `; | |
|      | |
|     showModal('Access Key Details', content); | |
| } | |
| 
 | |
| function showNewAccessKeyModal(accessKeyData) { | |
|     const content = ` | |
|         <div class="alert alert-success"> | |
|             <i class="fas fa-check-circle me-2"></i> | |
|             <strong>Success!</strong> Your new access key has been created. | |
|         </div> | |
|         <div class="alert alert-info"> | |
|             <i class="fas fa-info-circle me-2"></i> | |
|             <strong>Important:</strong> These credentials provide access to your object storage. Keep them secure and don't share them. You can view them again through the user management interface if needed. | |
|         </div> | |
|         <div class="mb-3"> | |
|             <label class="form-label"><strong>Access Key:</strong></label> | |
|             <div class="input-group"> | |
|                 <input type="text" class="form-control" value="${accessKeyData.access_key}" readonly> | |
|                 <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKeyData.access_key}')"> | |
|                     <i class="fas fa-copy"></i> | |
|                 </button> | |
|             </div> | |
|         </div> | |
|         <div class="mb-3"> | |
|             <label class="form-label"><strong>Secret Key:</strong></label> | |
|             <div class="input-group"> | |
|                 <input type="text" class="form-control" value="${accessKeyData.secret_key}" readonly> | |
|                 <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKeyData.secret_key}')"> | |
|                     <i class="fas fa-copy"></i> | |
|                 </button> | |
|             </div> | |
|         </div> | |
|     `; | |
|      | |
|     showModal('New Access Key Created', content); | |
| } | |
| 
 | |
| function showModal(title, content) { | |
|     // Create a dynamic modal | |
|     const modalId = 'dynamicModal_' + Date.now(); | |
|     const modalHtml = ` | |
|         <div class="modal fade" id="${modalId}" tabindex="-1" role="dialog"> | |
|             <div class="modal-dialog" role="document"> | |
|                 <div class="modal-content"> | |
|                     <div class="modal-header"> | |
|                         <h5 class="modal-title">${title}</h5> | |
|                         <button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
|                     </div> | |
|                     <div class="modal-body"> | |
|                         ${content} | |
|                     </div> | |
|                     <div class="modal-footer"> | |
|                         <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
|     `; | |
|      | |
|     // Add modal to body | |
|     document.body.insertAdjacentHTML('beforeend', modalHtml); | |
|      | |
|     // Show modal | |
|     const modal = new bootstrap.Modal(document.getElementById(modalId)); | |
|     modal.show(); | |
|      | |
|     // Remove modal from DOM when hidden | |
|     document.getElementById(modalId).addEventListener('hidden.bs.modal', function() { | |
|         this.remove(); | |
|     }); | |
| } | |
| 
 | |
| 
 | |
| 
 | |
|  
 |