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.
		
		
		
		
		
			
		
			
				
					
					
						
							726 lines
						
					
					
						
							36 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							726 lines
						
					
					
						
							36 KiB
						
					
					
				| package app | |
| 
 | |
| import ( | |
|     "fmt" | |
|     "strings" | |
|     "github.com/seaweedfs/seaweedfs/weed/admin/dash" | |
| ) | |
| 
 | |
| templ ClusterVolumes(data dash.ClusterVolumesData) { | |
|     <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> | |
|         <div> | |
|             <h1 class="h2"> | |
|                 <i class="fas fa-database me-2"></i>Cluster Volumes | |
|             </h1> | |
|             if data.FilterCollection != "" { | |
|                 <div class="d-flex align-items-center mt-2"> | |
|                     <span class="badge bg-info me-2"> | |
|                         <i class="fas fa-filter me-1"></i>Collection: {data.FilterCollection} | |
|                     </span> | |
|                     <a href="/cluster/volumes" class="btn btn-sm btn-outline-secondary"> | |
|                         <i class="fas fa-times me-1"></i>Clear Filter | |
|                     </a> | |
|                 </div> | |
|             } | |
|         </div> | |
|         <div class="btn-toolbar mb-2 mb-md-0"> | |
|             <div class="btn-group me-2"> | |
|                 <select class="form-select form-select-sm me-2" id="pageSizeSelect" onchange="changePageSize()" style="width: auto;"> | |
|                     <option value="50" if data.PageSize == 50 { selected="selected" }>50 per page</option> | |
|                     <option value="100" if data.PageSize == 100 { selected="selected" }>100 per page</option> | |
|                     <option value="200" if data.PageSize == 200 { selected="selected" }>200 per page</option> | |
|                     <option value="500" if data.PageSize == 500 { selected="selected" }>500 per page</option> | |
|                 </select> | |
|                 <button type="button" class="btn btn-sm btn-outline-primary" onclick="exportVolumes()"> | |
|                     <i class="fas fa-download me-1"></i>Export | |
|                 </button> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <div id="volumes-content"> | |
|         <!-- Summary Cards --> | |
|         <div class="row mb-4"> | |
|             <div class="col-xl-2 col-md-4 col-sm-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 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-2 col-md-4 col-sm-6 mb-4"> | |
|                 <div class="card border-left-success 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-success text-uppercase mb-1"> | |
|                                     if data.CollectionCount == 1 { | |
|                                         Collection | |
|                                     } else { | |
|                                         Collections | |
|                                     } | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     if data.CollectionCount == 1 { | |
|                                         {data.SingleCollection} | |
|                                     } else { | |
|                                         {fmt.Sprintf("%d", data.CollectionCount)} | |
|                                     } | |
|                                 </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-2 col-md-4 col-sm-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"> | |
|                                     if data.DataCenterCount == 1 { | |
|                                         Data Center | |
|                                     } else { | |
|                                         Data Centers | |
|                                     } | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     if data.DataCenterCount == 1 { | |
|                                         {data.SingleDataCenter} | |
|                                     } else { | |
|                                         {fmt.Sprintf("%d", data.DataCenterCount)} | |
|                                     } | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-building fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
| 
 | |
|             <div class="col-xl-2 col-md-4 col-sm-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"> | |
|                                     if data.RackCount == 1 { | |
|                                         Rack | |
|                                     } else { | |
|                                         Racks | |
|                                     } | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     if data.RackCount == 1 { | |
|                                         {data.SingleRack} | |
|                                     } else { | |
|                                         {fmt.Sprintf("%d", data.RackCount)} | |
|                                     } | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-server fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
| 
 | |
|             <div class="col-xl-2 col-md-4 col-sm-6 mb-4"> | |
|                 <div class="card border-left-dark 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-dark text-uppercase mb-1"> | |
|                                     if data.DiskTypeCount == 1 { | |
|                                         Disk Type | |
|                                     } else { | |
|                                         Disk Types | |
|                                     } | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     if data.DiskTypeCount == 1 { | |
|                                         {data.SingleDiskType} | |
|                                     } else { | |
|                                         {strings.Join(data.AllDiskTypes, ", ")} | |
|                                     } | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-hdd fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
| 
 | |
|             <div class="col-xl-2 col-md-4 col-sm-6 mb-4"> | |
|                 <div class="card border-left-purple 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-purple text-uppercase mb-1"> | |
|                                     if data.VersionCount == 1 { | |
|                                         Version | |
|                                     } else { | |
|                                         Versions | |
|                                     } | |
|                                 </div> | |
|                                 <div class="h5 mb-0 font-weight-bold text-gray-800"> | |
|                                     if data.VersionCount == 1 { | |
|                                         {data.SingleVersion} | |
|                                     } else { | |
|                                         {strings.Join(data.AllVersions, ", ")} | |
|                                     } | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-auto"> | |
|                                 <i class="fas fa-code-branch fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
| 
 | |
|             <div class="col-xl-2 col-md-4 col-sm-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 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-chart-area fa-2x text-gray-300"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
| 
 | |
|         <!-- Volumes 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-database me-2"></i>Volume Details | |
|                 </h6> | |
|             </div> | |
|             <div class="card-body"> | |
|                 if len(data.Volumes) > 0 { | |
|                     <div class="table-responsive"> | |
|                         <table class="table table-hover" id="volumesTable"> | |
|                             <thead> | |
|                                 <tr> | |
|                                     <th> | |
|                                         <a href="#" onclick="sortTable('id')" class="text-decoration-none text-dark"> | |
|                                             Volume ID | |
|                                             @getSortIcon("id", data.SortBy, data.SortOrder) | |
|                                         </a> | |
|                                     </th> | |
|                                     <th> | |
|                                         <a href="#" onclick="sortTable('server')" class="text-decoration-none text-dark"> | |
|                                             Server | |
|                                             @getSortIcon("server", data.SortBy, data.SortOrder) | |
|                                         </a> | |
|                                     </th> | |
|                                     if data.ShowDataCenterColumn { | |
|                                         <th> | |
|                                             <a href="#" onclick="sortTable('datacenter')" class="text-decoration-none text-dark"> | |
|                                                 Data Center | |
|                                                 @getSortIcon("datacenter", data.SortBy, data.SortOrder) | |
|                                             </a> | |
|                                         </th> | |
|                                     } | |
|                                     if data.ShowRackColumn { | |
|                                         <th> | |
|                                             <a href="#" onclick="sortTable('rack')" class="text-decoration-none text-dark"> | |
|                                                 Rack | |
|                                                 @getSortIcon("rack", data.SortBy, data.SortOrder) | |
|                                             </a> | |
|                                         </th> | |
|                                     } | |
|                                     if data.ShowCollectionColumn { | |
|                                         <th> | |
|                                             <a href="#" onclick="sortTable('collection')" class="text-decoration-none text-dark"> | |
|                                                 Collection | |
|                                                 @getSortIcon("collection", data.SortBy, data.SortOrder) | |
|                                             </a> | |
|                                         </th> | |
|                                     } | |
|                                     <th> | |
|                                         <a href="#" onclick="sortTable('size')" class="text-decoration-none text-dark"> | |
|                                             Size | |
|                                             @getSortIcon("size", data.SortBy, data.SortOrder) | |
|                                         </a> | |
|                                     </th> | |
|                                     <th>Volume Utilization</th> | |
|                                     <th> | |
|                                         <a href="#" onclick="sortTable('filecount')" class="text-decoration-none text-dark"> | |
|                                             File Count | |
|                                             @getSortIcon("filecount", data.SortBy, data.SortOrder) | |
|                                         </a> | |
|                                     </th> | |
|                                     <th> | |
|                                         <a href="#" onclick="sortTable('replication')" class="text-decoration-none text-dark"> | |
|                                             Replication | |
|                                             @getSortIcon("replication", data.SortBy, data.SortOrder) | |
|                                         </a> | |
|                                     </th> | |
|                                     if data.ShowDiskTypeColumn { | |
|                                         <th> | |
|                                             <a href="#" onclick="sortTable('disktype')" class="text-decoration-none text-dark"> | |
|                                                 Disk Type | |
|                                                 @getSortIcon("disktype", data.SortBy, data.SortOrder) | |
|                                             </a> | |
|                                         </th> | |
|                                     } | |
|                                     if data.ShowVersionColumn { | |
|                                         <th> | |
|                                             <a href="#" onclick="sortTable('version')" class="text-decoration-none text-dark"> | |
|                                                 Version | |
|                                                 @getSortIcon("version", data.SortBy, data.SortOrder) | |
|                                             </a> | |
|                                         </th> | |
|                                     } | |
|                                     <th>Actions</th> | |
|                                 </tr> | |
|                             </thead> | |
|                             <tbody> | |
|                                 for _, volume := range data.Volumes { | |
|                                     <tr> | |
|                                         <td> | |
|                                             <code class="volume-id-link" style="cursor: pointer; text-decoration: underline; color: #0d6efd;"  | |
|                                                   data-volume-id={fmt.Sprintf("%d", volume.Id)} | |
|                                                   title="Click to view volume details"> | |
|                                                 {fmt.Sprintf("%d", volume.Id)} | |
|                                             </code> | |
|                                         </td> | |
|                                         <td> | |
|                                             <a href={templ.SafeURL(fmt.Sprintf("http://%s/ui/index.html", volume.Server))} target="_blank" class="text-decoration-none"> | |
|                                                 {volume.Server} | |
|                                                 <i class="fas fa-external-link-alt ms-1 text-muted"></i> | |
|                                             </a> | |
|                                         </td> | |
|                                         if data.ShowDataCenterColumn { | |
|                                             <td> | |
|                                                 <span class="badge bg-light text-dark">{volume.DataCenter}</span> | |
|                                             </td> | |
|                                         } | |
|                                         if data.ShowRackColumn { | |
|                                             <td> | |
|                                                 <span class="badge bg-light text-dark">{volume.Rack}</span> | |
|                                             </td> | |
|                                         } | |
|                                         if data.ShowCollectionColumn { | |
|                                             <td> | |
|                                                 if volume.Collection == "" { | |
|                                                     <a href={templ.SafeURL("/cluster/volumes?collection=default")} class="text-decoration-none"> | |
|                                                         <span class="badge bg-secondary">default</span> | |
|                                                     </a> | |
|                                                 } else { | |
|                                                     <a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes?collection=%s", volume.Collection))} class="text-decoration-none"> | |
|                                                         <span class="badge bg-secondary">{volume.Collection}</span> | |
|                                                     </a> | |
|                                                 } | |
|                                             </td> | |
|                                         } | |
|                                                                 <td>{formatBytes(int64(volume.Size))}</td> | |
|                         <td> | |
|                             <div class="d-flex align-items-center"> | |
|                                 <div class="progress me-2" style="width: 80px; height: 16px; background-color: #e9ecef;"> | |
|                                     <!-- Active data (green) --> | |
|                                     <div class="progress-bar bg-success" role="progressbar"  | |
|                                          style={fmt.Sprintf("width: %.1f%%",  | |
|                                              func() float64 { | |
|                                                  if volume.Size > 0 { | |
|                                                      activePct := float64(volume.Size - volume.DeletedByteCount) / float64(volume.Size) * 100 | |
|                                                      if data.VolumeSizeLimit > 0 { | |
|                                                          return activePct * float64(volume.Size) / float64(data.VolumeSizeLimit) * 100 | |
|                                                      } | |
|                                                      return activePct | |
|                                                  } | |
|                                                  return 0 | |
|                                              }())} | |
|                                          title={fmt.Sprintf("Active: %s", formatBytes(int64(volume.Size - volume.DeletedByteCount)))}> | |
|                                     </div> | |
|                                     <!-- Garbage data (red) --> | |
|                                     <div class="progress-bar bg-danger" role="progressbar"  | |
|                                          style={fmt.Sprintf("width: %.1f%%",  | |
|                                              func() float64 { | |
|                                                  if volume.Size > 0 && volume.DeletedByteCount > 0 { | |
|                                                      garbagePct := float64(volume.DeletedByteCount) / float64(volume.Size) * 100 | |
|                                                      if data.VolumeSizeLimit > 0 { | |
|                                                          return garbagePct * float64(volume.Size) / float64(data.VolumeSizeLimit) * 100 | |
|                                                      } | |
|                                                      return garbagePct | |
|                                                  } | |
|                                                  return 0 | |
|                                              }())} | |
|                                          title={fmt.Sprintf("Garbage: %s", formatBytes(int64(volume.DeletedByteCount)))}> | |
|                                     </div> | |
|                                 </div> | |
|                                 <small class="text-muted"> | |
|                                     {func() string { | |
|                                         if data.VolumeSizeLimit > 0 { | |
|                                             return fmt.Sprintf("%.0f%%", float64(volume.Size)/float64(data.VolumeSizeLimit)*100) | |
|                                         } | |
|                                         return "N/A" | |
|                                     }()} | |
|                                 </small> | |
|                             </div> | |
|                         </td> | |
|                         <td>{fmt.Sprintf("%d", volume.FileCount)}</td> | |
|                         <td> | |
|                             <span class="badge bg-info">{fmt.Sprintf("%03d", volume.ReplicaPlacement)}</span> | |
|                         </td> | |
|                                         if data.ShowDiskTypeColumn { | |
|                                             <td> | |
|                                                 <span class="badge bg-primary">{volume.DiskType}</span> | |
|                                             </td> | |
|                                         } | |
|                                         if data.ShowVersionColumn { | |
|                                             <td> | |
|                                                 <span class="badge bg-dark">{fmt.Sprintf("v%d", volume.Version)}</span> | |
|                                             </td> | |
|                                         } | |
|                                         <td> | |
|                                             <div class="btn-group btn-group-sm"> | |
|                                                 <button type="button" class="btn btn-outline-primary btn-sm view-details-btn"  | |
|                                                         title="View Details" data-volume-id={fmt.Sprintf("%d", volume.Id)}> | |
|                                                     <i class="fas fa-eye"></i> | |
|                                                 </button> | |
|                                                 <button type="button" class="btn btn-outline-secondary btn-sm vacuum-btn"  | |
|                                                         title="Vacuum" | |
|                                                         data-volume-id={fmt.Sprintf("%d", volume.Id)} | |
|                                                         data-server={volume.Server}> | |
|                                                     <i class="fas fa-compress-alt"></i> | |
|                                                 </button> | |
|                                             </div> | |
|                                         </td> | |
|                                     </tr> | |
|                                 } | |
|                             </tbody> | |
|                         </table> | |
|                     </div> | |
|                      | |
|                     <!-- Volume Summary --> | |
|                     <div class="d-flex justify-content-between align-items-center mt-3"> | |
|                         <div> | |
|                             <small class="text-muted"> | |
|                                 Showing {fmt.Sprintf("%d", (data.CurrentPage-1)*data.PageSize + 1)} to {fmt.Sprintf("%d", minInt(data.CurrentPage*data.PageSize, data.TotalVolumes))} of {fmt.Sprintf("%d", data.TotalVolumes)} volumes | |
|                             </small> | |
|                         </div> | |
|                         if data.TotalPages > 1 { | |
|                             <div> | |
|                                 <small class="text-muted"> | |
|                                     Page {fmt.Sprintf("%d", data.CurrentPage)} of {fmt.Sprintf("%d", data.TotalPages)} | |
|                                 </small> | |
|                             </div> | |
|                         } | |
|                     </div> | |
|                      | |
|                     <!-- Pagination Controls --> | |
|                     if data.TotalPages > 1 { | |
|                         <div class="d-flex justify-content-center mt-3"> | |
|                             <nav aria-label="Volumes pagination"> | |
|                                 <ul class="pagination pagination-sm mb-0"> | |
|                                     <!-- Previous Button --> | |
|                                     if data.CurrentPage > 1 { | |
|                                         <li class="page-item"> | |
|                                             <a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", data.CurrentPage-1)}> | |
|                                                 <i class="fas fa-chevron-left"></i> | |
|                                             </a> | |
|                                         </li> | |
|                                     } else { | |
|                                         <li class="page-item disabled"> | |
|                                             <span class="page-link"> | |
|                                                 <i class="fas fa-chevron-left"></i> | |
|                                             </span> | |
|                                         </li> | |
|                                     } | |
|                                      | |
|                                     <!-- Page Numbers --> | |
|                                     for i := maxInt(1, data.CurrentPage-2); i <= minInt(data.TotalPages, data.CurrentPage+2); i++ { | |
|                                         if i == data.CurrentPage { | |
|                                             <li class="page-item active"> | |
|                                                 <span class="page-link">{fmt.Sprintf("%d", i)}</span> | |
|                                             </li> | |
|                                         } else { | |
|                                             <li class="page-item"> | |
|                                                 <a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", i)}>{fmt.Sprintf("%d", i)}</a> | |
|                                             </li> | |
|                                         } | |
|                                     } | |
|                                      | |
|                                     <!-- Next Button --> | |
|                                     if data.CurrentPage < data.TotalPages { | |
|                                         <li class="page-item"> | |
|                                             <a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", data.CurrentPage+1)}> | |
|                                                 <i class="fas fa-chevron-right"></i> | |
|                                             </a> | |
|                                         </li> | |
|                                     } else { | |
|                                         <li class="page-item disabled"> | |
|                                             <span class="page-link"> | |
|                                                 <i class="fas fa-chevron-right"></i> | |
|                                             </span> | |
|                                         </li> | |
|                                     } | |
|                                 </ul> | |
|                             </nav> | |
|                         </div> | |
|                     } | |
|                 } else { | |
|                     <div class="text-center py-5"> | |
|                         <i class="fas fa-database fa-3x text-muted mb-3"></i> | |
|                         <h5 class="text-muted">No Volumes Found</h5> | |
|                         <p class="text-muted">No volumes are currently available 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 pagination and sorting --> | |
|     <script> | |
|         // Initialize pagination links when page loads | |
|         document.addEventListener('DOMContentLoaded', function() { | |
|             // Add click handlers to pagination links | |
|             document.querySelectorAll('.pagination-link').forEach(link => { | |
|                 link.addEventListener('click', function(e) { | |
|                     e.preventDefault(); | |
|                     const page = this.getAttribute('data-page'); | |
|                     goToPage(page); | |
|                 }); | |
|             }); | |
|              | |
|             // Add click handlers to view details buttons | |
|             document.querySelectorAll('.view-details-btn').forEach(button => { | |
|                 button.addEventListener('click', function(e) { | |
|                     e.preventDefault(); | |
|                     const volumeId = this.getAttribute('data-volume-id'); | |
|                     viewVolumeDetails(volumeId); | |
|                 }); | |
|             }); | |
| 
 | |
|             // Add click handlers to volume ID links | |
|             document.querySelectorAll('.volume-id-link').forEach(link => { | |
|                 link.addEventListener('click', function(e) { | |
|                     e.preventDefault(); | |
|                     const volumeId = this.getAttribute('data-volume-id'); | |
|                     viewVolumeDetails(volumeId); | |
|                 }); | |
|             }); | |
| 
 | |
|             // Add click handlers to vacuum buttons | |
|             document.querySelectorAll('.vacuum-btn').forEach(button => { | |
|                 button.addEventListener('click', function(e) { | |
|                     e.preventDefault(); | |
|                     const volumeId = this.getAttribute('data-volume-id'); | |
|                     const server = this.getAttribute('data-server'); | |
|                     performVacuum(volumeId, server, this); | |
|                 }); | |
|             }); | |
|         }); | |
|          | |
|         function goToPage(page) { | |
|             const url = new URL(window.location); | |
|             url.searchParams.set('page', page); | |
|             window.location.href = url.toString(); | |
|         } | |
|          | |
|         function changePageSize() { | |
|             const pageSize = document.getElementById('pageSizeSelect').value; | |
|             const url = new URL(window.location); | |
|             url.searchParams.set('pageSize', pageSize); | |
|             url.searchParams.set('page', '1'); // Reset to first page | |
|             window.location.href = url.toString(); | |
|         } | |
|          | |
|         function sortTable(column) { | |
|             const url = new URL(window.location); | |
|             const currentSort = url.searchParams.get('sortBy'); | |
|             const currentOrder = url.searchParams.get('sortOrder') || 'asc'; | |
|              | |
|             let newOrder = 'asc'; | |
|             if (currentSort === column && currentOrder === 'asc') { | |
|                 newOrder = 'desc'; | |
|             } | |
|              | |
|             url.searchParams.set('sortBy', column); | |
|             url.searchParams.set('sortOrder', newOrder); | |
|             url.searchParams.set('page', '1'); // Reset to first page | |
|             window.location.href = url.toString(); | |
|         } | |
|          | |
|         function exportVolumes() { | |
|             // TODO: Implement volume export functionality | |
|             alert('Export functionality to be implemented'); | |
|         } | |
|          | |
|         function viewVolumeDetails(volumeId) { | |
|             // Get the server from the current row - works for both buttons and volume ID links | |
|             const clickedElement = event.target; | |
|             const row = clickedElement.closest('tr'); | |
|             const serverCell = row.querySelector('td:nth-child(2) a'); | |
|             const server = serverCell ? serverCell.textContent.trim() : 'unknown'; | |
|              | |
|             window.location.href = `/cluster/volumes/${volumeId}/${encodeURIComponent(server)}`; | |
|         } | |
| 
 | |
|         function performVacuum(volumeId, server, button) { | |
|             // Disable button and show loading state | |
|             const originalHTML = button.innerHTML; | |
|             button.disabled = true; | |
|             button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; | |
| 
 | |
|             // Send vacuum request | |
|             fetch(`/api/volumes/${volumeId}/${encodeURIComponent(server)}/vacuum`, { | |
|                 method: 'POST', | |
|                 headers: { | |
|                     'Content-Type': 'application/json', | |
|                 } | |
|             }) | |
|             .then(response => response.json()) | |
|             .then(data => { | |
|                 if (data.error) { | |
|                     showMessage(data.error, 'error'); | |
|                 } else { | |
|                     showMessage(data.message || 'Volume vacuum started successfully', 'success'); | |
|                     // Optionally refresh the page after a delay to show updated vacuum status | |
|                     setTimeout(() => { | |
|                         window.location.reload(); | |
|                     }, 2000); | |
|                 } | |
|             }) | |
|             .catch(error => { | |
|                 console.error('Error:', error); | |
|                 showMessage('Failed to start vacuum operation', 'error'); | |
|             }) | |
|             .finally(() => { | |
|                 // Re-enable button | |
|                 button.disabled = false; | |
|                 button.innerHTML = originalHTML; | |
|             }); | |
|         } | |
| 
 | |
|         function showMessage(message, type) { | |
|             // Create toast notification | |
|             const toast = document.createElement('div'); | |
|             toast.className = `alert alert-${type === 'error' ? 'danger' : 'success'} alert-dismissible fade show position-fixed`; | |
|             toast.style.top = '20px'; | |
|             toast.style.right = '20px'; | |
|             toast.style.zIndex = '9999'; | |
|             toast.style.minWidth = '300px'; | |
|              | |
|             toast.innerHTML = ` | |
|                 ${message} | |
|                 <button type="button" class="btn-close" data-bs-dismiss="alert"></button> | |
|             `; | |
|              | |
|             document.body.appendChild(toast); | |
|              | |
|             // Auto-remove after 5 seconds | |
|             setTimeout(() => { | |
|                 if (toast.parentNode) { | |
|                     toast.parentNode.removeChild(toast); | |
|                 } | |
|             }, 5000); | |
|         } | |
|     </script> | |
| } | |
| 
 | |
| func countActiveVolumes(volumes []dash.VolumeWithTopology) int { | |
| 	// Since we removed status tracking, consider all volumes as active | |
| 	return len(volumes) | |
| } | |
| 
 | |
| func countUniqueDataCenters(volumes []dash.VolumeWithTopology) int { | |
|     dcMap := make(map[string]bool) | |
|     for _, volume := range volumes { | |
|         dcMap[volume.DataCenter] = true | |
|     } | |
|     return len(dcMap) | |
| } | |
| 
 | |
| func countUniqueRacks(volumes []dash.VolumeWithTopology) int { | |
|     rackMap := make(map[string]bool) | |
|     for _, volume := range volumes { | |
|         if volume.Rack != "" { | |
|             rackMap[volume.Rack] = true | |
|         } | |
|     } | |
|     return len(rackMap) | |
| } | |
| 
 | |
| func countUniqueDiskTypes(volumes []dash.VolumeWithTopology) int { | |
|     diskTypeMap := make(map[string]bool) | |
|     for _, volume := range volumes { | |
|         diskType := volume.DiskType | |
|         if diskType == "" { | |
|             diskType = "hdd" | |
|         } | |
|         diskTypeMap[diskType] = true | |
|     } | |
|     return len(diskTypeMap) | |
| } | |
| 
 | |
| 
 | |
| 
 | |
| templ getSortIcon(column, currentSort, currentOrder string) { | |
|     if column != currentSort { | |
|         <i class="fas fa-sort text-muted ms-1"></i> | |
|     } else if currentOrder == "asc" { | |
|         <i class="fas fa-sort-up text-primary ms-1"></i> | |
|     } else { | |
|         <i class="fas fa-sort-down text-primary ms-1"></i> | |
|     } | |
| } | |
| 
 | |
| func minInt(a, b int) int { | |
|     if a < b { | |
|         return a | |
|     } | |
|     return b | |
| } | |
| 
 | |
| func maxInt(a, b int) int { | |
|     if a > b { | |
|         return a | |
|     } | |
|     return b | |
| }  |