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.
		
		
		
		
		
			
		
			
				
					
					
						
							400 lines
						
					
					
						
							18 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							400 lines
						
					
					
						
							18 KiB
						
					
					
				| package app | |
| 
 | |
| import ( | |
|     "fmt" | |
|     "github.com/seaweedfs/seaweedfs/weed/admin/dash" | |
| ) | |
| 
 | |
| templ ClusterCollections(data dash.ClusterCollectionsData) { | |
|     <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> | |
|         <h1 class="h2"> | |
|             <i class="fas fa-layer-group me-2"></i>Cluster Collections | |
|         </h1> | |
|         <div class="btn-toolbar mb-2 mb-md-0"> | |
|             <div class="btn-group me-2"> | |
|                 <button type="button" class="btn btn-sm btn-outline-primary" onclick="exportCollections()"> | |
|                     <i class="fas fa-download me-1"></i>Export | |
|                 </button> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <div id="collections-content"> | |
|         <!-- Summary Cards --> | |
|         <div class="row mb-4"> | |
|             <div class="col-xl-3 col-md-6 mb-4"> | |
|                 <div class="card border-left-primary shadow h-100 py-2"> | |
|                     <div class="card-body"> | |
|                         <div class="row no-gutters align-items-center"> | |
|                             <div class="col mr-2"> | |
|                                 <div class="text-xs font-weight-bold text-primary text-uppercase mb-1"> | |
|                                     Total Collections | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     {fmt.Sprintf("%d", data.TotalCollections)} | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-layer-group fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
| 
 | |
|             <div class="col-xl-3 col-md-6 mb-4"> | |
|                 <div class="card border-left-info shadow h-100 py-2"> | |
|                     <div class="card-body"> | |
|                         <div class="row no-gutters align-items-center"> | |
|                             <div class="col mr-2"> | |
|                                 <div class="text-xs font-weight-bold text-info text-uppercase mb-1"> | |
|                                     Total Volumes | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     {fmt.Sprintf("%d", data.TotalVolumes)} | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-database fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
| 
 | |
|             <div class="col-xl-3 col-md-6 mb-4"> | |
|                 <div class="card border-left-warning shadow h-100 py-2"> | |
|                     <div class="card-body"> | |
|                         <div class="row no-gutters align-items-center"> | |
|                             <div class="col mr-2"> | |
|                                 <div class="text-xs font-weight-bold text-warning text-uppercase mb-1"> | |
|                                     Total Files | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     {fmt.Sprintf("%d", data.TotalFiles)} | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-file fa-2x text-gray-300"></i> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
| 
 | |
|             <div class="col-xl-3 col-md-6 mb-4"> | |
|                 <div class="card border-left-secondary shadow h-100 py-2"> | |
|                     <div class="card-body"> | |
|                         <div class="row no-gutters align-items-center"> | |
|                             <div class="col mr-2"> | |
|                                 <div class="text-xs font-weight-bold text-secondary text-uppercase mb-1"> | |
|                                     Total Storage Size | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     {formatBytes(data.TotalSize)} | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-hdd fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
| 
 | |
|         <!-- Collections Table --> | |
|         <div class="card shadow mb-4"> | |
|             <div class="card-header py-3"> | |
|                 <h6 class="m-0 font-weight-bold text-primary"> | |
|                     <i class="fas fa-layer-group me-2"></i>Collection Details | |
|                 </h6> | |
|             </div> | |
|             <div class="card-body"> | |
|                 if len(data.Collections) > 0 { | |
|                     <div class="table-responsive"> | |
|                         <table class="table table-hover" id="collectionsTable"> | |
|                             <thead> | |
|                                 <tr> | |
|                                     <th>Collection Name</th> | |
|                                     <th>Volumes</th> | |
|                                     <th>Files</th> | |
|                                     <th>Size</th> | |
|                                     <th>Disk Types</th> | |
|                                     <th>Actions</th> | |
|                                 </tr> | |
|                             </thead> | |
|                             <tbody> | |
|                                 for _, collection := range data.Collections { | |
|                                     <tr> | |
|                                         <td> | |
|                                             <a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes?collection=%s", collection.Name))} class="text-decoration-none"> | |
|                                             <strong>{collection.Name}</strong> | |
|                                             </a> | |
|                                         </td> | |
|                                         <td> | |
|                                             <a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes?collection=%s", collection.Name))} class="text-decoration-none"> | |
|                                             <div class="d-flex align-items-center"> | |
|                                                 <i class="fas fa-database me-2 text-muted"></i> | |
|                                                 {fmt.Sprintf("%d", collection.VolumeCount)} | |
|                                             </div> | |
|                                             </a> | |
|                                         </td> | |
|                                         <td> | |
|                                             <div class="d-flex align-items-center"> | |
|                                                 <i class="fas fa-file me-2 text-muted"></i> | |
|                                                 {fmt.Sprintf("%d", collection.FileCount)} | |
|                                             </div> | |
|                                         </td> | |
|                                         <td> | |
|                                             <div class="d-flex align-items-center"> | |
|                                                 <i class="fas fa-hdd me-2 text-muted"></i> | |
|                                                 {formatBytes(collection.TotalSize)} | |
|                                             </div> | |
|                                         </td> | |
|                                         <td> | |
|                                             for i, diskType := range collection.DiskTypes { | |
|                                                 if i > 0 { | |
|                                                     <span class="me-1"></span> | |
|                                                 } | |
|                                                 <span class={fmt.Sprintf("badge bg-%s me-1", getDiskTypeColor(diskType))}>{diskType}</span> | |
|                                             } | |
|                                             if len(collection.DiskTypes) == 0 { | |
|                                                 <span class="text-muted">Unknown</span> | |
|                                             } | |
|                                         </td> | |
|                                         <td> | |
|                                             <button type="button"  | |
|                                                     class="btn btn-outline-primary btn-sm"  | |
|                                                     title="View Details" | |
|                                                     data-action="view-details" | |
|                                                     data-name={collection.Name} | |
|                                                     data-datacenter={collection.DataCenter} | |
|                                                     data-volume-count={fmt.Sprintf("%d", collection.VolumeCount)} | |
|                                                     data-file-count={fmt.Sprintf("%d", collection.FileCount)} | |
|                                                     data-total-size={fmt.Sprintf("%d", collection.TotalSize)} | |
|                                                     data-disk-types={formatDiskTypes(collection.DiskTypes)}> | |
|                                                 <i class="fas fa-eye"></i> | |
|                                             </button> | |
|                                         </td> | |
|                                     </tr> | |
|                                 } | |
|                             </tbody> | |
|                         </table> | |
|                     </div> | |
|                 } else { | |
|                     <div class="text-center py-5"> | |
|                         <i class="fas fa-layer-group fa-3x text-muted mb-3"></i> | |
|                         <h5 class="text-muted">No Collections Found</h5> | |
|                         <p class="text-muted">No collections are currently configured in the cluster.</p> | |
|                     </div> | |
|                 } | |
|             </div> | |
|         </div> | |
| 
 | |
|         <!-- Last Updated --> | |
|         <div class="row"> | |
|             <div class="col-12"> | |
|                 <small class="text-muted"> | |
|                     <i class="fas fa-clock me-1"></i> | |
|                     Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")} | |
|                 </small> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
|     <!-- JavaScript for cluster collections functionality --> | |
|     <script> | |
|     document.addEventListener('DOMContentLoaded', function() { | |
|         // Handle collection action buttons | |
|         document.addEventListener('click', function(e) { | |
|             const button = e.target.closest('[data-action]'); | |
|             if (!button) return; | |
|              | |
|             const action = button.getAttribute('data-action'); | |
|              | |
|             switch(action) { | |
|                 case 'view-details': | |
|                     const collectionData = { | |
|                         name: button.getAttribute('data-name'), | |
|                         datacenter: button.getAttribute('data-datacenter'), | |
|                         volumeCount: parseInt(button.getAttribute('data-volume-count')), | |
|                         fileCount: parseInt(button.getAttribute('data-file-count')), | |
|                         totalSize: parseInt(button.getAttribute('data-total-size')), | |
|                         diskTypes: button.getAttribute('data-disk-types') | |
|                     }; | |
|                     showCollectionDetails(collectionData); | |
|                     break; | |
|             } | |
|         }); | |
|     }); | |
|      | |
|     function showCollectionDetails(collection) { | |
|         const modalHtml = '<div class="modal fade" id="collectionDetailsModal" tabindex="-1">' + | |
|             '<div class="modal-dialog modal-lg">' + | |
|             '<div class="modal-content">' + | |
|             '<div class="modal-header">' + | |
|             '<h5 class="modal-title"><i class="fas fa-layer-group me-2"></i>Collection Details: ' + collection.name + '</h5>' + | |
|             '<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' + | |
|             '</div>' + | |
|             '<div class="modal-body">' + | |
|             '<div class="row">' + | |
|             '<div class="col-md-6">' + | |
|             '<h6 class="text-primary"><i class="fas fa-info-circle me-1"></i>Basic Information</h6>' + | |
|             '<table class="table table-sm">' + | |
|             '<tr><td><strong>Collection Name:</strong></td><td><code>' + collection.name + '</code></td></tr>' + | |
|             '<tr><td><strong>Data Center:</strong></td><td>' + | |
|             (collection.datacenter ? '<span class="badge bg-light text-dark">' + collection.datacenter + '</span>' : '<span class="text-muted">N/A</span>') + | |
|             '</td></tr>' + | |
|             '<tr><td><strong>Disk Types:</strong></td><td>' + | |
|             (collection.diskTypes ? collection.diskTypes.split(', ').map(type =>  | |
|                 '<span class="badge bg-' + getDiskTypeBadgeColor(type) + ' me-1">' + type + '</span>' | |
|             ).join('') : '<span class="text-muted">Unknown</span>') + | |
|             '</td></tr>' + | |
|             '</table>' + | |
|             '</div>' + | |
|             '<div class="col-md-6">' + | |
|             '<h6 class="text-primary"><i class="fas fa-chart-bar me-1"></i>Storage Statistics</h6>' + | |
|             '<table class="table table-sm">' + | |
|             '<tr><td><strong>Total Volumes:</strong></td><td>' + | |
|             '<div class="d-flex align-items-center">' + | |
|             '<i class="fas fa-database me-2 text-muted"></i>' + | |
|             '<span>' + collection.volumeCount.toLocaleString() + '</span>' + | |
|             '</div>' + | |
|             '</td></tr>' + | |
|             '<tr><td><strong>Total Files:</strong></td><td>' + | |
|             '<div class="d-flex align-items-center">' + | |
|             '<i class="fas fa-file me-2 text-muted"></i>' + | |
|             '<span>' + collection.fileCount.toLocaleString() + '</span>' + | |
|             '</div>' + | |
|             '</td></tr>' + | |
|             '<tr><td><strong>Total Size:</strong></td><td>' + | |
|             '<div class="d-flex align-items-center">' + | |
|             '<i class="fas fa-hdd me-2 text-muted"></i>' + | |
|             '<span>' + formatBytes(collection.totalSize) + '</span>' + | |
|             '</div>' + | |
|             '</td></tr>' + | |
|             '</table>' + | |
|             '</div>' + | |
|             '</div>' + | |
|             '<div class="row mt-3">' + | |
|             '<div class="col-12">' + | |
|             '<h6 class="text-primary"><i class="fas fa-link me-1"></i>Quick Actions</h6>' + | |
|             '<div class="d-grid gap-2 d-md-flex">' + | |
|             '<a href="/cluster/volumes?collection=' + encodeURIComponent(collection.name) + '" class="btn btn-outline-primary">' + | |
|             '<i class="fas fa-database me-1"></i>View Volumes' + | |
|             '</a>' + | |
|             '<a href="/files?collection=' + encodeURIComponent(collection.name) + '" class="btn btn-outline-info">' + | |
|             '<i class="fas fa-folder me-1"></i>Browse Files' + | |
|             '</a>' + | |
|             '</div>' + | |
|             '</div>' + | |
|             '</div>' + | |
|             '</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 present | |
|         const existingModal = document.getElementById('collectionDetailsModal'); | |
|         if (existingModal) { | |
|             existingModal.remove(); | |
|         } | |
|          | |
|         // Add modal to body and show | |
|         document.body.insertAdjacentHTML('beforeend', modalHtml); | |
|         const modal = new bootstrap.Modal(document.getElementById('collectionDetailsModal')); | |
|         modal.show(); | |
|          | |
|         // Remove modal when hidden | |
|         document.getElementById('collectionDetailsModal').addEventListener('hidden.bs.modal', function() { | |
|             this.remove(); | |
|         }); | |
|     } | |
|      | |
|     function getDiskTypeBadgeColor(diskType) { | |
|         switch(diskType.toLowerCase()) { | |
|             case 'ssd': | |
|                 return 'primary'; | |
|             case 'hdd': | |
|             case '': | |
|                 return 'secondary'; | |
|             default: | |
|                 return 'info'; | |
|         } | |
|     } | |
|      | |
|     function formatBytes(bytes) { | |
|         if (bytes === 0) return '0 Bytes'; | |
|         const k = 1024; | |
|         const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | |
|         const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
|         return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
|     } | |
|      | |
|     function exportCollections() { | |
|         // Simple CSV export of collections list | |
|         const rows = Array.from(document.querySelectorAll('#collectionsTable tbody tr')).map(row => { | |
|             const cells = row.querySelectorAll('td'); | |
|             if (cells.length > 1) { | |
|                 return { | |
|                     name: cells[0].textContent.trim(), | |
|                     volumes: cells[1].textContent.trim(), | |
|                     files: cells[2].textContent.trim(), | |
|                     size: cells[3].textContent.trim(), | |
|                     diskTypes: cells[4].textContent.trim() | |
|                 }; | |
|             } | |
|             return null; | |
|         }).filter(row => row !== null); | |
|          | |
|         const csvContent = "data:text/csv;charset=utf-8," +  | |
|             "Collection Name,Volumes,Files,Size,Disk Types\n" + | |
|             rows.map(r => '"' + r.name + '","' + r.volumes + '","' + r.files + '","' + r.size + '","' + r.diskTypes + '"').join("\n"); | |
|          | |
|         const encodedUri = encodeURI(csvContent); | |
|         const link = document.createElement("a"); | |
|         link.setAttribute("href", encodedUri); | |
|         link.setAttribute("download", "collections.csv"); | |
|         document.body.appendChild(link); | |
|         link.click(); | |
|         document.body.removeChild(link); | |
|     } | |
|     </script> | |
| } | |
| 
 | |
| func getDiskTypeColor(diskType string) string { | |
|     switch diskType { | |
|     case "ssd": | |
|         return "primary" | |
|     case "hdd", "": | |
|         return "secondary" | |
|     default: | |
|         return "info" | |
|     } | |
| } | |
| 
 | |
| func formatDiskTypes(diskTypes []string) string { | |
|     if len(diskTypes) == 0 { | |
|         return "Unknown" | |
|     } | |
|     if len(diskTypes) == 1 { | |
|         return diskTypes[0] | |
|     } | |
|     // For multiple disk types, join with comma | |
|     result := "" | |
|     for i, diskType := range diskTypes { | |
|         if i > 0 { | |
|             result += ", " | |
|         } | |
|         result += diskType | |
|     } | |
|     return result | |
| }  |