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.
		
		
		
		
		
			
		
			
				
					
					
						
							776 lines
						
					
					
						
							30 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							776 lines
						
					
					
						
							30 KiB
						
					
					
				| package app | |
| 
 | |
| import ( | |
| 	"fmt" | |
| 	"strings" | |
| 	"github.com/seaweedfs/seaweedfs/weed/admin/dash" | |
| 	"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding" | |
| ) | |
| 
 | |
| templ ClusterEcVolumes(data dash.ClusterEcVolumesData) { | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
|     <title>EC Volumes - SeaweedFS</title> | |
|     <meta charset="utf-8"> | |
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | |
|     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> | |
|     <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| </head> | |
| <body> | |
|     <div class="container-fluid"> | |
|         <div class="row"> | |
|             <div class="col-12"> | |
|                 <h2 class="mb-4"> | |
|                     <i class="fas fa-database me-2"></i>EC Volumes | |
|                     <small class="text-muted">({fmt.Sprintf("%d", data.TotalVolumes)} volumes)</small> | |
|                 </h2> | |
|             </div> | |
|         </div> | |
| 
 | |
|         <!-- Statistics Cards --> | |
|         <div class="row mb-4"> | |
|             <div class="col-md-3"> | |
|                 <div class="card text-bg-primary"> | |
|                     <div class="card-body"> | |
|                         <div class="d-flex justify-content-between"> | |
|                             <div> | |
|                                 <h6 class="card-title">Total Volumes</h6> | |
|                                 <h4 class="mb-0">{fmt.Sprintf("%d", data.TotalVolumes)}</h4> | |
|                                 <small>EC encoded volumes</small> | |
|                             </div> | |
|                             <div class="align-self-center"> | |
|                                 <i class="fas fa-cubes fa-2x"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|             <div class="col-md-3"> | |
|                 <div class="card text-bg-info"> | |
|                     <div class="card-body"> | |
|                         <div class="d-flex justify-content-between"> | |
|                             <div> | |
|                                 <h6 class="card-title">Total Shards</h6> | |
|                                 <h4 class="mb-0">{fmt.Sprintf("%d", data.TotalShards)}</h4> | |
|                                 <small>Distributed shards</small> | |
|                             </div> | |
|                             <div class="align-self-center"> | |
|                                 <i class="fas fa-puzzle-piece fa-2x"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|             <div class="col-md-3"> | |
|                 <div class="card text-bg-success"> | |
|                     <div class="card-body"> | |
|                         <div class="d-flex justify-content-between"> | |
|                             <div> | |
|                                 <h6 class="card-title">Complete Volumes</h6> | |
|                                 <h4 class="mb-0">{fmt.Sprintf("%d", data.CompleteVolumes)}</h4> | |
|                                 <small>All shards present</small> | |
|                             </div> | |
|                             <div class="align-self-center"> | |
|                                 <i class="fas fa-check-circle fa-2x"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|             <div class="col-md-3"> | |
|                 <div class="card text-bg-warning"> | |
|                     <div class="card-body"> | |
|                         <div class="d-flex justify-content-between"> | |
|                             <div> | |
|                                 <h6 class="card-title">Incomplete Volumes</h6> | |
|                                 <h4 class="mb-0">{fmt.Sprintf("%d", data.IncompleteVolumes)}</h4> | |
|                                 <small>Missing shards</small> | |
|                             </div> | |
|                             <div class="align-self-center"> | |
|                                 <i class="fas fa-exclamation-triangle fa-2x"></i> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
| 
 | |
|         <!-- EC Storage Information Note --> | |
|         <div class="alert alert-info mb-4" role="alert"> | |
|             <i class="fas fa-info-circle me-2"></i> | |
|             <strong>EC Storage Note:</strong>  | |
|             EC volumes use erasure coding ({ fmt.Sprintf("%d+%d", erasure_coding.DataShardsCount, erasure_coding.ParityShardsCount) }) which stores data across { fmt.Sprintf("%d", erasure_coding.TotalShardsCount) } shards with redundancy.  | |
|             Physical storage is approximately { fmt.Sprintf("%.1fx", float64(erasure_coding.TotalShardsCount)/float64(erasure_coding.DataShardsCount)) } the original logical data size due to { fmt.Sprintf("%d", erasure_coding.ParityShardsCount) } parity shards. | |
|         </div> | |
| 
 | |
|         <!-- Volumes Table --> | |
|         <div class="d-flex justify-content-between align-items-center mb-3"> | |
|             <div class="d-flex align-items-center"> | |
|                 <span class="me-3"> | |
|                     Showing {fmt.Sprintf("%d", (data.Page-1)*data.PageSize + 1)} to {fmt.Sprintf("%d", func() int { | |
|                         end := data.Page * data.PageSize | |
|                         if end > data.TotalVolumes { | |
|                             return data.TotalVolumes | |
|                         } | |
|                         return end | |
|                     }())} of {fmt.Sprintf("%d", data.TotalVolumes)} volumes | |
|                 </span> | |
|                  | |
|                 <div class="d-flex align-items-center"> | |
|                     <label for="pageSize" class="form-label me-2 mb-0">Show:</label> | |
|                     <select id="pageSize" class="form-select form-select-sm" style="width: auto;" onchange="changePageSize(this.value)"> | |
|                         <option value="5" if data.PageSize == 5 { selected }>5</option> | |
|                         <option value="10" if data.PageSize == 10 { selected }>10</option> | |
|                         <option value="25" if data.PageSize == 25 { selected }>25</option> | |
|                         <option value="50" if data.PageSize == 50 { selected }>50</option> | |
|                         <option value="100" if data.PageSize == 100 { selected }>100</option> | |
|                     </select> | |
|                     <span class="ms-2">per page</span> | |
|                 </div> | |
|             </div> | |
|              | |
|             if data.Collection != "" { | |
|                 <div> | |
|                     if data.Collection == "default" { | |
|                         <span class="badge bg-secondary text-white">Collection: default</span> | |
|                     } else { | |
|                         <span class="badge bg-info text-white">Collection: {data.Collection}</span> | |
|                     } | |
|                     <a href="/cluster/ec-shards" class="btn btn-sm btn-outline-secondary ms-2">Clear Filter</a> | |
|                 </div> | |
|             } | |
|         </div> | |
| 
 | |
|         <div class="table-responsive"> | |
|             <table class="table table-striped table-hover" id="ecVolumesTable"> | |
|                 <thead> | |
|                     <tr> | |
|                         <th> | |
|                             <a href="#" onclick="sortBy('volume_id')" class="text-dark text-decoration-none"> | |
|                                 Volume ID | |
|                                 if data.SortBy == "volume_id" { | |
|                                     if data.SortOrder == "asc" { | |
|                                         <i class="fas fa-sort-up ms-1"></i> | |
|                                     } else { | |
|                                         <i class="fas fa-sort-down ms-1"></i> | |
|                                     } | |
|                                 } else { | |
|                                     <i class="fas fa-sort ms-1 text-muted"></i> | |
|                                 } | |
|                             </a> | |
|                         </th> | |
|                         if data.ShowCollectionColumn { | |
|                             <th> | |
|                                 <a href="#" onclick="sortBy('collection')" class="text-dark text-decoration-none"> | |
|                                     Collection | |
|                                     if data.SortBy == "collection" { | |
|                                         if data.SortOrder == "asc" { | |
|                                             <i class="fas fa-sort-up ms-1"></i> | |
|                                         } else { | |
|                                             <i class="fas fa-sort-down ms-1"></i> | |
|                                         } | |
|                                     } else { | |
|                                         <i class="fas fa-sort ms-1 text-muted"></i> | |
|                                     } | |
|                                 </a> | |
|                             </th> | |
|                         } | |
|                         <th> | |
|                             <a href="#" onclick="sortBy('total_shards')" class="text-dark text-decoration-none"> | |
|                                 Shard Count | |
|                                 if data.SortBy == "total_shards" { | |
|                                     if data.SortOrder == "asc" { | |
|                                         <i class="fas fa-sort-up ms-1"></i> | |
|                                     } else { | |
|                                         <i class="fas fa-sort-down ms-1"></i> | |
|                                     } | |
|                                 } else { | |
|                                     <i class="fas fa-sort ms-1 text-muted"></i> | |
|                                 } | |
|                             </a> | |
|                         </th> | |
|                         <th class="text-dark">Shard Size</th> | |
|                         <th class="text-dark">Shard Locations</th> | |
|                         <th> | |
|                             <a href="#" onclick="sortBy('completeness')" class="text-dark text-decoration-none"> | |
|                                 Status | |
|                                 if data.SortBy == "completeness" { | |
|                                     if data.SortOrder == "asc" { | |
|                                         <i class="fas fa-sort-up ms-1"></i> | |
|                                     } else { | |
|                                         <i class="fas fa-sort-down ms-1"></i> | |
|                                     } | |
|                                 } else { | |
|                                     <i class="fas fa-sort ms-1 text-muted"></i> | |
|                                 } | |
|                             </a> | |
|                         </th> | |
|                         if data.ShowDataCenterColumn { | |
|                             <th class="text-dark">Data Centers</th> | |
|                         } | |
|                         <th class="text-dark">Actions</th> | |
|                     </tr> | |
|                 </thead> | |
|                 <tbody> | |
|                     for _, volume := range data.EcVolumes { | |
|                         <tr> | |
|                             <td> | |
|                                 <strong>{fmt.Sprintf("%d", volume.VolumeID)}</strong> | |
|                             </td> | |
|                             if data.ShowCollectionColumn { | |
|                                 <td> | |
|                                     if volume.Collection != "" { | |
|                                         <a href="/cluster/ec-shards?collection={volume.Collection}" class="text-decoration-none"> | |
|                                             <span class="badge bg-info text-white">{volume.Collection}</span> | |
|                                         </a> | |
|                                     } else { | |
|                                         <a href="/cluster/ec-shards?collection=default" class="text-decoration-none"> | |
|                                             <span class="badge bg-secondary text-white">default</span> | |
|                                         </a> | |
|                                     } | |
|                                 </td> | |
|                             } | |
|                             <td> | |
|                                 <span class="badge bg-primary">{fmt.Sprintf("%d/14", volume.TotalShards)}</span> | |
|                             </td> | |
|                             <td> | |
|                                 @displayShardSizes(volume.ShardSizes) | |
|                             </td> | |
|                             <td> | |
|                                 @displayVolumeDistribution(volume) | |
|                             </td> | |
|                             <td> | |
|                                 @displayEcVolumeStatus(volume) | |
|                             </td> | |
|                             if data.ShowDataCenterColumn { | |
|                                 <td> | |
|                                     for i, dc := range volume.DataCenters { | |
|                                         if i > 0 { | |
|                                             <span>, </span> | |
|                                         } | |
|                                         <span class="badge bg-primary text-white">{dc}</span> | |
|                                     } | |
|                                 </td> | |
|                             } | |
|                             <td> | |
|                                 <div class="btn-group" role="group"> | |
|                                     <button type="button" class="btn btn-sm btn-outline-primary"  | |
|                                             onclick="showVolumeDetails(event)"  | |
|                                             data-volume-id={ fmt.Sprintf("%d", volume.VolumeID) } | |
|                                             title="View EC volume details"> | |
|                                         <i class="fas fa-info-circle"></i> | |
|                                     </button> | |
|                                     if !volume.IsComplete { | |
|                                         <button type="button" class="btn btn-sm btn-outline-warning"  | |
|                                                 onclick="repairVolume(event)"  | |
|                                                 data-volume-id={ fmt.Sprintf("%d", volume.VolumeID) } | |
|                                                 title="Repair missing shards"> | |
|                                             <i class="fas fa-wrench"></i> | |
|                                         </button> | |
|                                     } | |
|                                 </div> | |
|                             </td> | |
|                         </tr> | |
|                     } | |
|                 </tbody> | |
|             </table> | |
|         </div> | |
| 
 | |
|         <!-- Pagination --> | |
|         if data.TotalPages > 1 { | |
|             <nav aria-label="EC Volumes pagination"> | |
|                 <ul class="pagination justify-content-center"> | |
|                     if data.Page > 1 { | |
|                         <li class="page-item"> | |
|                             <a class="page-link" href="#" onclick="goToPage(event)" data-page="1">First</a> | |
|                         </li> | |
|                         <li class="page-item"> | |
|                             <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.Page-1) }>Previous</a> | |
|                         </li> | |
|                     } | |
|                      | |
|                     for i := 1; i <= data.TotalPages; i++ { | |
|                         if i == data.Page { | |
|                             <li class="page-item active"> | |
|                                 <span class="page-link">{fmt.Sprintf("%d", i)}</span> | |
|                             </li> | |
|                         } else if i <= 3 || i > data.TotalPages-3 || (i >= data.Page-2 && i <= data.Page+2) { | |
|                             <li class="page-item"> | |
|                                 <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", i) }>{fmt.Sprintf("%d", i)}</a> | |
|                             </li> | |
|                         } else if i == 4 && data.Page > 6 { | |
|                             <li class="page-item disabled"> | |
|                                 <span class="page-link">...</span> | |
|                             </li> | |
|                         } else if i == data.TotalPages-3 && data.Page < data.TotalPages-5 { | |
|                             <li class="page-item disabled"> | |
|                                 <span class="page-link">...</span> | |
|                             </li> | |
|                         } | |
|                     } | |
|                      | |
|                     if data.Page < data.TotalPages { | |
|                         <li class="page-item"> | |
|                             <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.Page+1) }>Next</a> | |
|                         </li> | |
|                         <li class="page-item"> | |
|                             <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.TotalPages) }>Last</a> | |
|                         </li> | |
|                     } | |
|                 </ul> | |
|             </nav> | |
|         } | |
|     </div> | |
| 
 | |
|     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> | |
|     <script> | |
|         // Sorting functionality | |
|         function sortBy(field) { | |
|             const currentSort = new URLSearchParams(window.location.search).get('sort_by'); | |
|             const currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc'; | |
|              | |
|             let newOrder = 'asc'; | |
|             if (currentSort === field && currentOrder === 'asc') { | |
|                 newOrder = 'desc'; | |
|             } | |
|              | |
|             const url = new URL(window.location); | |
|             url.searchParams.set('sort_by', field); | |
|             url.searchParams.set('sort_order', newOrder); | |
|             url.searchParams.set('page', '1'); // Reset to first page | |
|             window.location.href = url.toString(); | |
|         } | |
| 
 | |
|         // Pagination functionality | |
|         function goToPage(event) { | |
|             event.preventDefault(); | |
|             const page = event.target.closest('a').getAttribute('data-page'); | |
|             const url = new URL(window.location); | |
|             url.searchParams.set('page', page); | |
|             window.location.href = url.toString(); | |
|         } | |
| 
 | |
|         // Page size functionality | |
|         function changePageSize(newPageSize) { | |
|             const url = new URL(window.location); | |
|             url.searchParams.set('page_size', newPageSize); | |
|             url.searchParams.set('page', '1'); // Reset to first page when changing page size | |
|             window.location.href = url.toString(); | |
|         } | |
| 
 | |
|         // Volume details | |
|         function showVolumeDetails(event) { | |
|             const volumeId = event.target.closest('button').getAttribute('data-volume-id'); | |
|             window.location.href = `/cluster/ec-volumes/${volumeId}`; | |
|         } | |
| 
 | |
|         // Repair volume | |
|         function repairVolume(event) { | |
|             const volumeId = event.target.closest('button').getAttribute('data-volume-id'); | |
|             if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) { | |
|                 // TODO: Implement repair functionality | |
|                 alert('Repair functionality will be implemented soon.'); | |
|             } | |
|         } | |
|     </script> | |
| </body> | |
| </html> | |
| } | |
| 
 | |
| // displayShardLocationsHTML renders shard locations as proper HTML | |
| templ displayShardLocationsHTML(shardLocations map[int]string) { | |
| 	if len(shardLocations) == 0 { | |
| 		<span class="text-muted">No shards</span> | |
| 	} else { | |
| 		for i, serverInfo := range groupShardsByServer(shardLocations) { | |
| 			if i > 0 { | |
| 				<br/> | |
| 			} | |
| 			<strong> | |
| 				<a href={ templ.URL("/cluster/volume-servers/" + serverInfo.Server) } class="text-primary text-decoration-none"> | |
| 					{ serverInfo.Server } | |
| 				</a>: | |
| 			</strong> { serverInfo.ShardRanges } | |
| 		} | |
| 	} | |
| } | |
| 
 | |
| // displayShardSizes renders shard sizes in a compact format | |
| templ displayShardSizes(shardSizes map[int]int64) { | |
| 	if len(shardSizes) == 0 { | |
| 		<span class="text-muted">-</span> | |
| 	} else { | |
| 		@renderShardSizesContent(shardSizes) | |
| 	} | |
| } | |
| 
 | |
| // renderShardSizesContent renders the content of shard sizes | |
| templ renderShardSizesContent(shardSizes map[int]int64) { | |
| 	if areAllShardSizesSame(shardSizes) { | |
| 		// All shards have the same size, show just the common size | |
| 		<span class="text-success">{getCommonShardSize(shardSizes)}</span> | |
| 	} else { | |
| 		// Shards have different sizes, show individual sizes | |
| 		<div class="shard-sizes" style="max-width: 300px;"> | |
| 			{ formatIndividualShardSizes(shardSizes) } | |
| 		</div> | |
| 	} | |
| } | |
| 
 | |
| // ServerShardInfo represents server and its shard ranges with sizes | |
| type ServerShardInfo struct { | |
| 	Server      string | |
| 	ShardRanges string | |
| } | |
| 
 | |
| // groupShardsByServer groups shards by server and formats ranges | |
| func groupShardsByServer(shardLocations map[int]string) []ServerShardInfo { | |
| 	if len(shardLocations) == 0 { | |
| 		return []ServerShardInfo{} | |
| 	} | |
| 
 | |
| 	// Group shards by server | |
| 	serverShards := make(map[string][]int) | |
| 	for shardId, server := range shardLocations { | |
| 		serverShards[server] = append(serverShards[server], shardId) | |
| 	} | |
| 
 | |
| 	var serverInfos []ServerShardInfo | |
| 	for server, shards := range serverShards { | |
| 		// Sort shards for each server | |
| 		for i := 0; i < len(shards); i++ { | |
| 			for j := i + 1; j < len(shards); j++ { | |
| 				if shards[i] > shards[j] { | |
| 					shards[i], shards[j] = shards[j], shards[i] | |
| 				} | |
| 			} | |
| 		} | |
| 		 | |
| 		// Format shard ranges compactly | |
| 		shardRanges := formatShardRanges(shards) | |
| 		serverInfos = append(serverInfos, ServerShardInfo{ | |
| 			Server:      server, | |
| 			ShardRanges: shardRanges, | |
| 		}) | |
| 	} | |
| 
 | |
| 	// Sort by server name | |
| 	for i := 0; i < len(serverInfos); i++ { | |
| 		for j := i + 1; j < len(serverInfos); j++ { | |
| 			if serverInfos[i].Server > serverInfos[j].Server { | |
| 				serverInfos[i], serverInfos[j] = serverInfos[j], serverInfos[i] | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	return serverInfos | |
| } | |
| 
 | |
| // groupShardsByServerWithSizes groups shards by server and formats ranges with sizes | |
| func groupShardsByServerWithSizes(shardLocations map[int]string, shardSizes map[int]int64) []ServerShardInfo { | |
| 	if len(shardLocations) == 0 { | |
| 		return []ServerShardInfo{} | |
| 	} | |
| 
 | |
| 	// Group shards by server | |
| 	serverShards := make(map[string][]int) | |
| 	for shardId, server := range shardLocations { | |
| 		serverShards[server] = append(serverShards[server], shardId) | |
| 	} | |
| 
 | |
| 	var serverInfos []ServerShardInfo | |
| 	for server, shards := range serverShards { | |
| 		// Sort shards for each server | |
| 		for i := 0; i < len(shards); i++ { | |
| 			for j := i + 1; j < len(shards); j++ { | |
| 				if shards[i] > shards[j] { | |
| 					shards[i], shards[j] = shards[j], shards[i] | |
| 				} | |
| 			} | |
| 		} | |
| 		 | |
| 		// Format shard ranges compactly with sizes | |
| 		shardRanges := formatShardRangesWithSizes(shards, shardSizes) | |
| 		serverInfos = append(serverInfos, ServerShardInfo{ | |
| 			Server:      server, | |
| 			ShardRanges: shardRanges, | |
| 		}) | |
| 	} | |
| 
 | |
| 	// Sort by server name | |
| 	for i := 0; i < len(serverInfos); i++ { | |
| 		for j := i + 1; j < len(serverInfos); j++ { | |
| 			if serverInfos[i].Server > serverInfos[j].Server { | |
| 				serverInfos[i], serverInfos[j] = serverInfos[j], serverInfos[i] | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	return serverInfos | |
| } | |
| 
 | |
| // Helper function to format shard ranges compactly (e.g., "0-3,7,9-11") | |
| func formatShardRanges(shards []int) string { | |
| 	if len(shards) == 0 { | |
| 		return "" | |
| 	} | |
| 
 | |
| 	var ranges []string | |
| 	start := shards[0] | |
| 	end := shards[0] | |
| 
 | |
| 	for i := 1; i < len(shards); i++ { | |
| 		if shards[i] == end+1 { | |
| 			end = shards[i] | |
| 		} else { | |
| 			if start == end { | |
| 				ranges = append(ranges, fmt.Sprintf("%d", start)) | |
| 			} else { | |
| 				ranges = append(ranges, fmt.Sprintf("%d-%d", start, end)) | |
| 			} | |
| 			start = shards[i] | |
| 			end = shards[i] | |
| 		} | |
| 	} | |
| 
 | |
| 	// Add the last range | |
| 	if start == end { | |
| 		ranges = append(ranges, fmt.Sprintf("%d", start)) | |
| 	} else { | |
| 		ranges = append(ranges, fmt.Sprintf("%d-%d", start, end)) | |
| 	} | |
| 
 | |
| 	return strings.Join(ranges, ",") | |
| } | |
| 
 | |
| // Helper function to format shard ranges with sizes (e.g., "0(1.2MB),1-3(2.5MB),7(800KB)") | |
| func formatShardRangesWithSizes(shards []int, shardSizes map[int]int64) string { | |
| 	if len(shards) == 0 { | |
| 		return "" | |
| 	} | |
| 
 | |
| 	var ranges []string | |
| 	start := shards[0] | |
| 	end := shards[0] | |
| 	var totalSize int64 | |
| 
 | |
| 	for i := 1; i < len(shards); i++ { | |
| 		if shards[i] == end+1 { | |
| 			end = shards[i] | |
| 			totalSize += shardSizes[shards[i]] | |
| 		} else { | |
| 			// Add current range with size | |
| 			if start == end { | |
| 				size := shardSizes[start] | |
| 				if size > 0 { | |
| 					ranges = append(ranges, fmt.Sprintf("%d(%s)", start, bytesToHumanReadable(size))) | |
| 				} else { | |
| 					ranges = append(ranges, fmt.Sprintf("%d", start)) | |
| 				} | |
| 			} else { | |
| 				// Calculate total size for the range | |
| 				rangeSize := shardSizes[start] | |
| 				for j := start + 1; j <= end; j++ { | |
| 					rangeSize += shardSizes[j] | |
| 				} | |
| 				if rangeSize > 0 { | |
| 					ranges = append(ranges, fmt.Sprintf("%d-%d(%s)", start, end, bytesToHumanReadable(rangeSize))) | |
| 				} else { | |
| 					ranges = append(ranges, fmt.Sprintf("%d-%d", start, end)) | |
| 				} | |
| 			} | |
| 			start = shards[i] | |
| 			end = shards[i] | |
| 			totalSize = shardSizes[shards[i]] | |
| 		} | |
| 	} | |
| 
 | |
| 	// Add the last range | |
| 	if start == end { | |
| 		size := shardSizes[start] | |
| 		if size > 0 { | |
| 			ranges = append(ranges, fmt.Sprintf("%d(%s)", start, bytesToHumanReadable(size))) | |
| 		} else { | |
| 			ranges = append(ranges, fmt.Sprintf("%d", start)) | |
| 		} | |
| 	} else { | |
| 		// Calculate total size for the range | |
| 		rangeSize := shardSizes[start] | |
| 		for j := start + 1; j <= end; j++ { | |
| 			rangeSize += shardSizes[j] | |
| 		} | |
| 		if rangeSize > 0 { | |
| 			ranges = append(ranges, fmt.Sprintf("%d-%d(%s)", start, end, bytesToHumanReadable(rangeSize))) | |
| 		} else { | |
| 			ranges = append(ranges, fmt.Sprintf("%d-%d", start, end)) | |
| 		} | |
| 	} | |
| 
 | |
| 	return strings.Join(ranges, ",") | |
| } | |
| 
 | |
| // Helper function to convert bytes to human readable format | |
| func bytesToHumanReadable(bytes int64) string { | |
| 	const unit = 1024 | |
| 	if bytes < unit { | |
| 		return fmt.Sprintf("%dB", bytes) | |
| 	} | |
| 	div, exp := int64(unit), 0 | |
| 	for n := bytes / unit; n >= unit; n /= unit { | |
| 		div *= unit | |
| 		exp++ | |
| 	} | |
| 	return fmt.Sprintf("%.1f%cB", float64(bytes)/float64(div), "KMGTPE"[exp]) | |
| } | |
| 
 | |
| // Helper function to format missing shards | |
| func formatMissingShards(missingShards []int) string { | |
| 	if len(missingShards) == 0 { | |
| 		return "" | |
| 	} | |
| 	 | |
| 	var shardStrs []string | |
| 	for _, shard := range missingShards { | |
| 		shardStrs = append(shardStrs, fmt.Sprintf("%d", shard)) | |
| 	} | |
| 	 | |
| 	return strings.Join(shardStrs, ", ") | |
| } | |
| 
 | |
| // Helper function to check if all shard sizes are the same | |
| func areAllShardSizesSame(shardSizes map[int]int64) bool { | |
| 	if len(shardSizes) <= 1 { | |
| 		return true | |
| 	} | |
| 	 | |
| 	var firstSize int64 = -1 | |
| 	for _, size := range shardSizes { | |
| 		if firstSize == -1 { | |
| 			firstSize = size | |
| 		} else if size != firstSize { | |
| 			return false | |
| 		} | |
| 	} | |
| 	return true | |
| } | |
| 
 | |
| // Helper function to get the common shard size (when all shards are the same size) | |
| func getCommonShardSize(shardSizes map[int]int64) string { | |
| 	for _, size := range shardSizes { | |
| 		return bytesToHumanReadable(size) | |
| 	} | |
| 	return "-" | |
| } | |
| 
 | |
| // Helper function to format individual shard sizes | |
| func formatIndividualShardSizes(shardSizes map[int]int64) string { | |
| 	if len(shardSizes) == 0 { | |
| 		return "" | |
| 	} | |
| 	 | |
| 	// Group shards by size for more compact display | |
| 	sizeGroups := make(map[int64][]int) | |
| 	for shardId, size := range shardSizes { | |
| 		sizeGroups[size] = append(sizeGroups[size], shardId) | |
| 	} | |
| 	 | |
| 	// If there are only 1-2 different sizes, show them grouped | |
| 	if len(sizeGroups) <= 3 { | |
| 		var groupStrs []string | |
| 		for size, shardIds := range sizeGroups { | |
| 			// Sort shard IDs | |
| 			for i := 0; i < len(shardIds); i++ { | |
| 				for j := i + 1; j < len(shardIds); j++ { | |
| 					if shardIds[i] > shardIds[j] { | |
| 						shardIds[i], shardIds[j] = shardIds[j], shardIds[i] | |
| 					} | |
| 				} | |
| 			} | |
| 			 | |
| 			var idRanges []string | |
| 			if len(shardIds) <= erasure_coding.ParityShardsCount { | |
| 				// Show individual IDs if few shards | |
| 				for _, id := range shardIds { | |
| 					idRanges = append(idRanges, fmt.Sprintf("%d", id)) | |
| 				} | |
| 			} else { | |
| 				// Show count if many shards | |
| 				idRanges = append(idRanges, fmt.Sprintf("%d shards", len(shardIds))) | |
| 			} | |
| 			groupStrs = append(groupStrs, fmt.Sprintf("%s: %s", strings.Join(idRanges, ","), bytesToHumanReadable(size))) | |
| 		} | |
| 		return strings.Join(groupStrs, " | ") | |
| 	} | |
| 	 | |
| 	// If too many different sizes, show summary | |
| 	return fmt.Sprintf("%d different sizes", len(sizeGroups)) | |
| } | |
| 
 | |
| // displayVolumeDistribution shows the distribution summary for a volume | |
| templ displayVolumeDistribution(volume dash.EcVolumeWithShards) { | |
|     <div class="small"> | |
|         <i class="fas fa-sitemap me-1"></i> | |
|         { calculateVolumeDistributionSummary(volume) } | |
|     </div> | |
| } | |
| 
 | |
| // displayEcVolumeStatus shows an improved status display for EC volumes | |
| templ displayEcVolumeStatus(volume dash.EcVolumeWithShards) { | |
|     if volume.IsComplete { | |
|         <span class="badge bg-success"><i class="fas fa-check me-1"></i>Complete</span> | |
|     } else { | |
|         if len(volume.MissingShards) > erasure_coding.DataShardsCount { | |
|             <span class="badge bg-danger"><i class="fas fa-skull me-1"></i>Critical ({fmt.Sprintf("%d", len(volume.MissingShards))} missing)</span> | |
|         } else if len(volume.MissingShards) > (erasure_coding.DataShardsCount/2) { | |
|             <span class="badge bg-warning"><i class="fas fa-exclamation-triangle me-1"></i>Degraded ({fmt.Sprintf("%d", len(volume.MissingShards))} missing)</span> | |
|         } else if len(volume.MissingShards) > (erasure_coding.ParityShardsCount/2) { | |
|             <span class="badge bg-warning"><i class="fas fa-info-circle me-1"></i>Incomplete ({fmt.Sprintf("%d", len(volume.MissingShards))} missing)</span> | |
|         } else { | |
|             <span class="badge bg-info"><i class="fas fa-info-circle me-1"></i>Minor Issues ({fmt.Sprintf("%d", len(volume.MissingShards))} missing)</span> | |
|         } | |
|     } | |
| } | |
| 
 | |
| // calculateVolumeDistributionSummary calculates and formats the distribution summary for a volume | |
| func calculateVolumeDistributionSummary(volume dash.EcVolumeWithShards) string { | |
|     dataCenters := make(map[string]bool) | |
|     racks := make(map[string]bool) | |
|     servers := make(map[string]bool) | |
|      | |
|     // Count unique servers from shard locations | |
|     for _, server := range volume.ShardLocations { | |
|         servers[server] = true | |
|     } | |
|      | |
|     // Use the DataCenters field if available | |
|     for _, dc := range volume.DataCenters { | |
|         dataCenters[dc] = true | |
|     } | |
|      | |
|     // Use the Servers field if available | |
|     for _, server := range volume.Servers { | |
|         servers[server] = true | |
|     } | |
|      | |
|     // Use the Racks field if available | |
|     for _, rack := range volume.Racks { | |
|         racks[rack] = true | |
|     } | |
|      | |
|     // If we don't have rack information, estimate it from servers as fallback | |
|     rackCount := len(racks) | |
|     if rackCount == 0 { | |
|         // Fallback estimation - assume each server might be in a different rack | |
|         rackCount = len(servers) | |
|         if len(dataCenters) > 0 { | |
|             // More conservative estimate if we have DC info | |
|             rackCount = (len(servers) + len(dataCenters) - 1) / len(dataCenters) | |
|             if rackCount == 0 { | |
|                 rackCount = 1 | |
|             } | |
|         } | |
|     } | |
|      | |
|     return fmt.Sprintf("%d DCs, %d racks, %d servers", len(dataCenters), rackCount, len(servers)) | |
| }  |