diff options
Diffstat (limited to 'weed/admin/view/app/cluster_ec_volumes.templ')
| -rw-r--r-- | weed/admin/view/app/cluster_ec_volumes.templ | 775 |
1 files changed, 775 insertions, 0 deletions
diff --git a/weed/admin/view/app/cluster_ec_volumes.templ b/weed/admin/view/app/cluster_ec_volumes.templ new file mode 100644 index 000000000..aafa621aa --- /dev/null +++ b/weed/admin/view/app/cluster_ec_volumes.templ @@ -0,0 +1,775 @@ +package app + +import ( + "fmt" + "strings" + "github.com/seaweedfs/seaweedfs/weed/admin/dash" +) + +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 (10+4) which stores data across 14 shards with redundancy. + Physical storage is approximately 1.4x the original logical data size due to 4 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) <= 4 { + // 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) > 10 { + <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) > 6 { + <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) > 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)) +}
\ No newline at end of file |
