10 changed files with 2361 additions and 12 deletions
-
395weed/admin/dash/ec_shard_management.go
-
74weed/admin/dash/types.go
-
4weed/admin/handlers/admin_handlers.go
-
86weed/admin/handlers/cluster_handlers.go
-
424weed/admin/view/app/cluster_ec_shards.templ
-
706weed/admin/view/app/cluster_ec_shards_templ.go
-
223weed/admin/view/app/ec_volume_details.templ
-
432weed/admin/view/app/ec_volume_details_templ.go
-
5weed/admin/view/layout/layout.templ
-
24weed/admin/view/layout/layout_templ.go
@ -0,0 +1,395 @@ |
|||
package dash |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"sort" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb" |
|||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding" |
|||
) |
|||
|
|||
// GetClusterEcShards retrieves cluster EC shards data with pagination, sorting, and filtering
|
|||
func (s *AdminServer) GetClusterEcShards(page int, pageSize int, sortBy string, sortOrder string, collection string) (*ClusterEcShardsData, error) { |
|||
// Set defaults
|
|||
if page < 1 { |
|||
page = 1 |
|||
} |
|||
if pageSize < 1 || pageSize > 1000 { |
|||
pageSize = 100 |
|||
} |
|||
if sortBy == "" { |
|||
sortBy = "volume_id" |
|||
} |
|||
if sortOrder == "" { |
|||
sortOrder = "asc" |
|||
} |
|||
|
|||
var ecShards []EcShardWithInfo |
|||
shardsPerVolume := make(map[uint32]int) |
|||
volumesWithAllShards := 0 |
|||
volumesWithMissingShards := 0 |
|||
|
|||
// Get detailed EC shard information via gRPC
|
|||
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { |
|||
resp, err := client.VolumeList(context.Background(), &master_pb.VolumeListRequest{}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
if resp.TopologyInfo != nil { |
|||
for _, dc := range resp.TopologyInfo.DataCenterInfos { |
|||
for _, rack := range dc.RackInfos { |
|||
for _, node := range rack.DataNodeInfos { |
|||
for _, diskInfo := range node.DiskInfos { |
|||
// Process EC shard information
|
|||
for _, ecShardInfo := range diskInfo.EcShardInfos { |
|||
// Count shards per volume
|
|||
shardsPerVolume[ecShardInfo.Id] += getShardCount(ecShardInfo.EcIndexBits) |
|||
|
|||
// Create individual shard entries for each shard this server has
|
|||
shardBits := ecShardInfo.EcIndexBits |
|||
for shardId := 0; shardId < erasure_coding.TotalShardsCount; shardId++ { |
|||
if (shardBits & (1 << uint(shardId))) != 0 { |
|||
ecShard := EcShardWithInfo{ |
|||
VolumeID: ecShardInfo.Id, |
|||
ShardID: uint32(shardId), |
|||
Collection: ecShardInfo.Collection, |
|||
Size: 0, // EC shards don't have individual size in the API response
|
|||
Server: node.Id, |
|||
DataCenter: dc.Id, |
|||
Rack: rack.Id, |
|||
DiskType: diskInfo.Type, |
|||
ModifiedTime: 0, // Not available in current API
|
|||
EcIndexBits: ecShardInfo.EcIndexBits, |
|||
ShardCount: getShardCount(ecShardInfo.EcIndexBits), |
|||
} |
|||
ecShards = append(ecShards, ecShard) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
}) |
|||
|
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
// Calculate completeness statistics
|
|||
for volumeId, shardCount := range shardsPerVolume { |
|||
if shardCount == erasure_coding.TotalShardsCount { |
|||
volumesWithAllShards++ |
|||
} else { |
|||
volumesWithMissingShards++ |
|||
} |
|||
|
|||
// Update completeness info for each shard
|
|||
for i := range ecShards { |
|||
if ecShards[i].VolumeID == volumeId { |
|||
ecShards[i].IsComplete = (shardCount == erasure_coding.TotalShardsCount) |
|||
if !ecShards[i].IsComplete { |
|||
// Calculate missing shards
|
|||
ecShards[i].MissingShards = getMissingShards(ecShards[i].EcIndexBits) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Filter by collection if specified
|
|||
if collection != "" { |
|||
var filteredShards []EcShardWithInfo |
|||
for _, shard := range ecShards { |
|||
if shard.Collection == collection { |
|||
filteredShards = append(filteredShards, shard) |
|||
} |
|||
} |
|||
ecShards = filteredShards |
|||
} |
|||
|
|||
// Sort the results
|
|||
sortEcShards(ecShards, sortBy, sortOrder) |
|||
|
|||
// Calculate statistics for conditional display
|
|||
dataCenters := make(map[string]bool) |
|||
racks := make(map[string]bool) |
|||
collections := make(map[string]bool) |
|||
|
|||
for _, shard := range ecShards { |
|||
dataCenters[shard.DataCenter] = true |
|||
racks[shard.Rack] = true |
|||
if shard.Collection != "" { |
|||
collections[shard.Collection] = true |
|||
} |
|||
} |
|||
|
|||
// Pagination
|
|||
totalShards := len(ecShards) |
|||
totalPages := (totalShards + pageSize - 1) / pageSize |
|||
startIndex := (page - 1) * pageSize |
|||
endIndex := startIndex + pageSize |
|||
if endIndex > totalShards { |
|||
endIndex = totalShards |
|||
} |
|||
|
|||
if startIndex >= totalShards { |
|||
startIndex = 0 |
|||
endIndex = 0 |
|||
} |
|||
|
|||
paginatedShards := ecShards[startIndex:endIndex] |
|||
|
|||
// Build response
|
|||
data := &ClusterEcShardsData{ |
|||
EcShards: paginatedShards, |
|||
TotalShards: totalShards, |
|||
TotalVolumes: len(shardsPerVolume), |
|||
LastUpdated: time.Now(), |
|||
|
|||
// Pagination
|
|||
CurrentPage: page, |
|||
TotalPages: totalPages, |
|||
PageSize: pageSize, |
|||
|
|||
// Sorting
|
|||
SortBy: sortBy, |
|||
SortOrder: sortOrder, |
|||
|
|||
// Statistics
|
|||
DataCenterCount: len(dataCenters), |
|||
RackCount: len(racks), |
|||
CollectionCount: len(collections), |
|||
|
|||
// Conditional display flags
|
|||
ShowDataCenterColumn: len(dataCenters) > 1, |
|||
ShowRackColumn: len(racks) > 1, |
|||
ShowCollectionColumn: len(collections) > 1, |
|||
|
|||
// Filtering
|
|||
FilterCollection: collection, |
|||
|
|||
// EC specific statistics
|
|||
ShardsPerVolume: shardsPerVolume, |
|||
VolumesWithAllShards: volumesWithAllShards, |
|||
VolumesWithMissingShards: volumesWithMissingShards, |
|||
} |
|||
|
|||
// Set single values when only one exists
|
|||
if len(dataCenters) == 1 { |
|||
for dc := range dataCenters { |
|||
data.SingleDataCenter = dc |
|||
break |
|||
} |
|||
} |
|||
if len(racks) == 1 { |
|||
for rack := range racks { |
|||
data.SingleRack = rack |
|||
break |
|||
} |
|||
} |
|||
if len(collections) == 1 { |
|||
for col := range collections { |
|||
data.SingleCollection = col |
|||
break |
|||
} |
|||
} |
|||
|
|||
return data, nil |
|||
} |
|||
|
|||
// getShardCount returns the number of shards represented by the bitmap
|
|||
func getShardCount(ecIndexBits uint32) int { |
|||
count := 0 |
|||
for i := 0; i < erasure_coding.TotalShardsCount; i++ { |
|||
if (ecIndexBits & (1 << uint(i))) != 0 { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
// getMissingShards returns a slice of missing shard IDs for a volume
|
|||
func getMissingShards(ecIndexBits uint32) []int { |
|||
var missing []int |
|||
for i := 0; i < erasure_coding.TotalShardsCount; i++ { |
|||
if (ecIndexBits & (1 << uint(i))) == 0 { |
|||
missing = append(missing, i) |
|||
} |
|||
} |
|||
return missing |
|||
} |
|||
|
|||
// sortEcShards sorts EC shards based on the specified field and order
|
|||
func sortEcShards(shards []EcShardWithInfo, sortBy string, sortOrder string) { |
|||
sort.Slice(shards, func(i, j int) bool { |
|||
var less bool |
|||
switch sortBy { |
|||
case "volume_id": |
|||
if shards[i].VolumeID == shards[j].VolumeID { |
|||
less = shards[i].ShardID < shards[j].ShardID |
|||
} else { |
|||
less = shards[i].VolumeID < shards[j].VolumeID |
|||
} |
|||
case "shard_id": |
|||
if shards[i].ShardID == shards[j].ShardID { |
|||
less = shards[i].VolumeID < shards[j].VolumeID |
|||
} else { |
|||
less = shards[i].ShardID < shards[j].ShardID |
|||
} |
|||
case "collection": |
|||
if shards[i].Collection == shards[j].Collection { |
|||
less = shards[i].VolumeID < shards[j].VolumeID |
|||
} else { |
|||
less = shards[i].Collection < shards[j].Collection |
|||
} |
|||
case "server": |
|||
if shards[i].Server == shards[j].Server { |
|||
less = shards[i].VolumeID < shards[j].VolumeID |
|||
} else { |
|||
less = shards[i].Server < shards[j].Server |
|||
} |
|||
case "datacenter": |
|||
if shards[i].DataCenter == shards[j].DataCenter { |
|||
less = shards[i].VolumeID < shards[j].VolumeID |
|||
} else { |
|||
less = shards[i].DataCenter < shards[j].DataCenter |
|||
} |
|||
case "rack": |
|||
if shards[i].Rack == shards[j].Rack { |
|||
less = shards[i].VolumeID < shards[j].VolumeID |
|||
} else { |
|||
less = shards[i].Rack < shards[j].Rack |
|||
} |
|||
default: |
|||
less = shards[i].VolumeID < shards[j].VolumeID |
|||
} |
|||
|
|||
if sortOrder == "desc" { |
|||
return !less |
|||
} |
|||
return less |
|||
}) |
|||
} |
|||
|
|||
// GetEcVolumeDetails retrieves detailed information about a specific EC volume
|
|||
func (s *AdminServer) GetEcVolumeDetails(volumeID uint32) (*EcVolumeDetailsData, error) { |
|||
var shards []EcShardWithInfo |
|||
var collection string |
|||
dataCenters := make(map[string]bool) |
|||
servers := make(map[string]bool) |
|||
|
|||
// Get detailed EC shard information for the specific volume via gRPC
|
|||
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { |
|||
resp, err := client.VolumeList(context.Background(), &master_pb.VolumeListRequest{}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
if resp.TopologyInfo != nil { |
|||
for _, dc := range resp.TopologyInfo.DataCenterInfos { |
|||
for _, rack := range dc.RackInfos { |
|||
for _, node := range rack.DataNodeInfos { |
|||
for _, diskInfo := range node.DiskInfos { |
|||
// Process EC shard information for this specific volume
|
|||
for _, ecShardInfo := range diskInfo.EcShardInfos { |
|||
if ecShardInfo.Id == volumeID { |
|||
collection = ecShardInfo.Collection |
|||
dataCenters[dc.Id] = true |
|||
servers[node.Id] = true |
|||
|
|||
// Create individual shard entries for each shard this server has
|
|||
shardBits := ecShardInfo.EcIndexBits |
|||
for shardId := 0; shardId < erasure_coding.TotalShardsCount; shardId++ { |
|||
if (shardBits & (1 << uint(shardId))) != 0 { |
|||
ecShard := EcShardWithInfo{ |
|||
VolumeID: ecShardInfo.Id, |
|||
ShardID: uint32(shardId), |
|||
Collection: ecShardInfo.Collection, |
|||
Size: 0, // EC shards don't have individual size in the API response
|
|||
Server: node.Id, |
|||
DataCenter: dc.Id, |
|||
Rack: rack.Id, |
|||
DiskType: diskInfo.Type, |
|||
ModifiedTime: 0, // Not available in current API
|
|||
EcIndexBits: ecShardInfo.EcIndexBits, |
|||
ShardCount: getShardCount(ecShardInfo.EcIndexBits), |
|||
} |
|||
shards = append(shards, ecShard) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
}) |
|||
|
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
if len(shards) == 0 { |
|||
return nil, fmt.Errorf("EC volume %d not found", volumeID) |
|||
} |
|||
|
|||
// Calculate completeness
|
|||
totalShards := len(shards) |
|||
isComplete := (totalShards == erasure_coding.TotalShardsCount) |
|||
|
|||
// Calculate missing shards
|
|||
var missingShards []int |
|||
foundShards := make(map[int]bool) |
|||
for _, shard := range shards { |
|||
foundShards[int(shard.ShardID)] = true |
|||
} |
|||
for i := 0; i < erasure_coding.TotalShardsCount; i++ { |
|||
if !foundShards[i] { |
|||
missingShards = append(missingShards, i) |
|||
} |
|||
} |
|||
|
|||
// Update completeness info for each shard
|
|||
for i := range shards { |
|||
shards[i].IsComplete = isComplete |
|||
shards[i].MissingShards = missingShards |
|||
} |
|||
|
|||
// Sort shards by ID
|
|||
sort.Slice(shards, func(i, j int) bool { |
|||
return shards[i].ShardID < shards[j].ShardID |
|||
}) |
|||
|
|||
// Convert maps to slices
|
|||
var dcList []string |
|||
for dc := range dataCenters { |
|||
dcList = append(dcList, dc) |
|||
} |
|||
var serverList []string |
|||
for server := range servers { |
|||
serverList = append(serverList, server) |
|||
} |
|||
|
|||
data := &EcVolumeDetailsData{ |
|||
VolumeID: volumeID, |
|||
Collection: collection, |
|||
Shards: shards, |
|||
TotalShards: totalShards, |
|||
IsComplete: isComplete, |
|||
MissingShards: missingShards, |
|||
DataCenters: dcList, |
|||
Servers: serverList, |
|||
LastUpdated: time.Now(), |
|||
} |
|||
|
|||
return data, nil |
|||
} |
|||
@ -0,0 +1,424 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
templ ClusterEcShards(data dash.ClusterEcShardsData) { |
|||
<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-th-large me-2"></i>EC Shards |
|||
</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/ec-shards" 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="exportEcShards()"> |
|||
<i class="fas fa-download me-1"></i>Export |
|||
</button> |
|||
</div> |
|||
</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 Shards</h6> |
|||
<h4 class="mb-0">{fmt.Sprintf("%d", data.TotalShards)}</h4> |
|||
</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-info"> |
|||
<div class="card-body"> |
|||
<div class="d-flex justify-content-between"> |
|||
<div> |
|||
<h6 class="card-title">EC Volumes</h6> |
|||
<h4 class="mb-0">{fmt.Sprintf("%d", data.TotalVolumes)}</h4> |
|||
</div> |
|||
<div class="align-self-center"> |
|||
<i class="fas fa-database 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.VolumesWithAllShards)}</h4> |
|||
<small>All 14 shards</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.VolumesWithMissingShards)}</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> |
|||
|
|||
<!-- Shards Table --> |
|||
<div class="table-responsive"> |
|||
<table class="table table-striped table-hover" id="ecShardsTable"> |
|||
<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> |
|||
<th> |
|||
<a href="#" onclick="sortBy('shard_id')" class="text-dark text-decoration-none"> |
|||
Shard ID |
|||
if data.SortBy == "shard_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('server')" class="text-dark text-decoration-none"> |
|||
Server |
|||
if data.SortBy == "server" { |
|||
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> |
|||
<a href="#" onclick="sortBy('datacenter')" class="text-dark text-decoration-none"> |
|||
Data Center |
|||
if data.SortBy == "datacenter" { |
|||
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.ShowRackColumn { |
|||
<th> |
|||
<a href="#" onclick="sortBy('rack')" class="text-dark text-decoration-none"> |
|||
Rack |
|||
if data.SortBy == "rack" { |
|||
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">Status</th> |
|||
<th class="text-dark">Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, shard := range data.EcShards { |
|||
<tr> |
|||
<td> |
|||
<span class="fw-bold">{fmt.Sprintf("%d", shard.VolumeID)}</span> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-secondary">{fmt.Sprintf("%02d", shard.ShardID)}</span> |
|||
</td> |
|||
if data.ShowCollectionColumn { |
|||
<td> |
|||
if shard.Collection != "" { |
|||
<a href="/cluster/ec-shards?collection={shard.Collection}" class="text-decoration-none"> |
|||
<span class="badge bg-info">{shard.Collection}</span> |
|||
</a> |
|||
} else { |
|||
<span class="text-muted">default</span> |
|||
} |
|||
</td> |
|||
} |
|||
<td> |
|||
<code class="small">{shard.Server}</code> |
|||
</td> |
|||
if data.ShowDataCenterColumn { |
|||
<td> |
|||
<span class="badge bg-outline-primary">{shard.DataCenter}</span> |
|||
</td> |
|||
} |
|||
if data.ShowRackColumn { |
|||
<td> |
|||
<span class="badge bg-outline-secondary">{shard.Rack}</span> |
|||
</td> |
|||
} |
|||
<td> |
|||
if shard.IsComplete { |
|||
<span class="badge bg-success"> |
|||
<i class="fas fa-check me-1"></i>Complete |
|||
</span> |
|||
} else { |
|||
<span class="badge bg-warning"> |
|||
<i class="fas fa-exclamation-triangle me-1"></i> |
|||
Missing {fmt.Sprintf("%d", len(shard.MissingShards))} shards |
|||
</span> |
|||
} |
|||
</td> |
|||
<td> |
|||
<div class="btn-group" role="group"> |
|||
<button type="button" class="btn btn-sm btn-outline-primary" |
|||
onclick="showShardDetails(event)" |
|||
data-volume-id={ fmt.Sprintf("%d", shard.VolumeID) } |
|||
title="View EC volume details"> |
|||
<i class="fas fa-info-circle"></i> |
|||
</button> |
|||
if !shard.IsComplete { |
|||
<button type="button" class="btn btn-sm btn-outline-warning" |
|||
onclick="repairVolume(event)" |
|||
data-volume-id={ fmt.Sprintf("%d", shard.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 Shards pagination"> |
|||
<ul class="pagination justify-content-center"> |
|||
if data.CurrentPage > 1 { |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.CurrentPage-1) }> |
|||
<i class="fas fa-chevron-left"></i> |
|||
</a> |
|||
</li> |
|||
} |
|||
|
|||
<!-- First page --> |
|||
if data.CurrentPage > 3 { |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" onclick="goToPage(1)">1</a> |
|||
</li> |
|||
if data.CurrentPage > 4 { |
|||
<li class="page-item disabled"> |
|||
<span class="page-link">...</span> |
|||
</li> |
|||
} |
|||
} |
|||
|
|||
<!-- Current page and neighbors --> |
|||
if data.CurrentPage > 1 && data.CurrentPage-1 >= 1 { |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.CurrentPage-1) }>{fmt.Sprintf("%d", data.CurrentPage-1)}</a> |
|||
</li> |
|||
} |
|||
|
|||
<li class="page-item active"> |
|||
<span class="page-link">{fmt.Sprintf("%d", data.CurrentPage)}</span> |
|||
</li> |
|||
|
|||
if data.CurrentPage < data.TotalPages && data.CurrentPage+1 <= data.TotalPages { |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.CurrentPage+1) }>{fmt.Sprintf("%d", data.CurrentPage+1)}</a> |
|||
</li> |
|||
} |
|||
|
|||
<!-- Last page --> |
|||
if data.CurrentPage < data.TotalPages-2 { |
|||
if data.CurrentPage < data.TotalPages-3 { |
|||
<li class="page-item disabled"> |
|||
<span class="page-link">...</span> |
|||
</li> |
|||
} |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.TotalPages) }>{fmt.Sprintf("%d", data.TotalPages)}</a> |
|||
</li> |
|||
} |
|||
|
|||
if data.CurrentPage < data.TotalPages { |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.CurrentPage+1) }> |
|||
<i class="fas fa-chevron-right"></i> |
|||
</a> |
|||
</li> |
|||
} |
|||
</ul> |
|||
</nav> |
|||
} |
|||
|
|||
<!-- JavaScript --> |
|||
<script> |
|||
function sortBy(field) { |
|||
const currentSort = "{data.SortBy}"; |
|||
const currentOrder = "{data.SortOrder}"; |
|||
let newOrder = 'asc'; |
|||
|
|||
if (currentSort === field && currentOrder === 'asc') { |
|||
newOrder = 'desc'; |
|||
} |
|||
|
|||
updateUrl({ |
|||
sortBy: field, |
|||
sortOrder: newOrder, |
|||
page: 1 |
|||
}); |
|||
} |
|||
|
|||
function goToPage(event) { |
|||
// Get data from the link element (not any child elements) |
|||
const link = event.target.closest('a'); |
|||
const page = link.getAttribute('data-page'); |
|||
updateUrl({ page: page }); |
|||
} |
|||
|
|||
function changePageSize() { |
|||
const pageSize = document.getElementById('pageSizeSelect').value; |
|||
updateUrl({ pageSize: pageSize, page: 1 }); |
|||
} |
|||
|
|||
function updateUrl(params) { |
|||
const url = new URL(window.location); |
|||
Object.keys(params).forEach(key => { |
|||
if (params[key]) { |
|||
url.searchParams.set(key, params[key]); |
|||
} else { |
|||
url.searchParams.delete(key); |
|||
} |
|||
}); |
|||
window.location.href = url.toString(); |
|||
} |
|||
|
|||
function exportEcShards() { |
|||
const url = new URL('/api/cluster/ec-shards/export', window.location.origin); |
|||
const params = new URLSearchParams(window.location.search); |
|||
params.forEach((value, key) => { |
|||
url.searchParams.set(key, value); |
|||
}); |
|||
window.open(url.toString(), '_blank'); |
|||
} |
|||
|
|||
function showShardDetails(event) { |
|||
// Get data from the button element (not the icon inside it) |
|||
const button = event.target.closest('button'); |
|||
const volumeId = button.getAttribute('data-volume-id'); |
|||
|
|||
// Navigate to the EC volume details page |
|||
window.location.href = `/cluster/ec-volumes/${volumeId}`; |
|||
} |
|||
|
|||
function repairVolume(event) { |
|||
// Get data from the button element (not the icon inside it) |
|||
const button = event.target.closest('button'); |
|||
const volumeId = button.getAttribute('data-volume-id'); |
|||
if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) { |
|||
fetch(`/api/cluster/volumes/${volumeId}/repair`, { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json', |
|||
} |
|||
}) |
|||
.then(response => response.json()) |
|||
.then(data => { |
|||
if (data.success) { |
|||
alert('Repair initiated successfully'); |
|||
location.reload(); |
|||
} else { |
|||
alert('Failed to initiate repair: ' + data.error); |
|||
} |
|||
}) |
|||
.catch(error => { |
|||
alert('Error: ' + error.message); |
|||
}); |
|||
} |
|||
} |
|||
</script> |
|||
} |
|||
@ -0,0 +1,706 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.906
|
|||
package app |
|||
|
|||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|||
|
|||
import "github.com/a-h/templ" |
|||
import templruntime "github.com/a-h/templ/runtime" |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
func ClusterEcShards(data dash.ClusterEcShardsData) templ.Component { |
|||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { |
|||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context |
|||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { |
|||
return templ_7745c5c3_CtxErr |
|||
} |
|||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) |
|||
if !templ_7745c5c3_IsBuffer { |
|||
defer func() { |
|||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err == nil { |
|||
templ_7745c5c3_Err = templ_7745c5c3_BufErr |
|||
} |
|||
}() |
|||
} |
|||
ctx = templ.InitializeContext(ctx) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
ctx = templ.ClearChildren(ctx) |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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-th-large me-2\"></i>EC Shards</h1>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.FilterCollection != "" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<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: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.FilterCollection) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 17, Col: 92} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span> <a href=\"/cluster/ec-shards\" class=\"btn btn-sm btn-outline-secondary\"><i class=\"fas fa-times me-1\"></i>Clear Filter</a></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</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 templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.PageSize == 50 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " selected=\"selected\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, ">50 per page</option> <option value=\"100\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.PageSize == 100 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " selected=\"selected\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, ">100 per page</option> <option value=\"200\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.PageSize == 200 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " selected=\"selected\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, ">200 per page</option> <option value=\"500\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.PageSize == 500 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " selected=\"selected\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ">500 per page</option></select> <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" onclick=\"exportEcShards()\"><i class=\"fas fa-download me-1\"></i>Export</button></div></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 Shards</h6><h4 class=\"mb-0\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalShards)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 48, Col: 81} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</h4></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-info\"><div class=\"card-body\"><div class=\"d-flex justify-content-between\"><div><h6 class=\"card-title\">EC Volumes</h6><h4 class=\"mb-0\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 string |
|||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalVolumes)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 63, Col: 82} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</h4></div><div class=\"align-self-center\"><i class=\"fas fa-database 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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.VolumesWithAllShards)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 78, Col: 90} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</h4><small>All 14 shards</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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.VolumesWithMissingShards)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 94, Col: 94} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</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><!-- Shards Table --><div class=\"table-responsive\"><table class=\"table table-striped table-hover\" id=\"ecShardsTable\"><thead><tr><th><a href=\"#\" onclick=\"sortBy('volume_id')\" class=\"text-dark text-decoration-none\">Volume ID ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.SortBy == "volume_id" { |
|||
if data.SortOrder == "asc" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<i class=\"fas fa-sort-up ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<i class=\"fas fa-sort-down ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<i class=\"fas fa-sort ms-1 text-muted\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</a></th><th><a href=\"#\" onclick=\"sortBy('shard_id')\" class=\"text-dark text-decoration-none\">Shard ID ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.SortBy == "shard_id" { |
|||
if data.SortOrder == "asc" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<i class=\"fas fa-sort-up ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<i class=\"fas fa-sort-down ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<i class=\"fas fa-sort ms-1 text-muted\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</a></th>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.ShowCollectionColumn { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<th><a href=\"#\" onclick=\"sortBy('collection')\" class=\"text-dark text-decoration-none\">Collection ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.SortBy == "collection" { |
|||
if data.SortOrder == "asc" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<i class=\"fas fa-sort-up ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<i class=\"fas fa-sort-down ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<i class=\"fas fa-sort ms-1 text-muted\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</a></th>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<th><a href=\"#\" onclick=\"sortBy('server')\" class=\"text-dark text-decoration-none\">Server ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.SortBy == "server" { |
|||
if data.SortOrder == "asc" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<i class=\"fas fa-sort-up ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<i class=\"fas fa-sort-down ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<i class=\"fas fa-sort ms-1 text-muted\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</a></th>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.ShowDataCenterColumn { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<th><a href=\"#\" onclick=\"sortBy('datacenter')\" class=\"text-dark text-decoration-none\">Data Center ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.SortBy == "datacenter" { |
|||
if data.SortOrder == "asc" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<i class=\"fas fa-sort-up ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<i class=\"fas fa-sort-down ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<i class=\"fas fa-sort ms-1 text-muted\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</a></th>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
if data.ShowRackColumn { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<th><a href=\"#\" onclick=\"sortBy('rack')\" class=\"text-dark text-decoration-none\">Rack ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.SortBy == "rack" { |
|||
if data.SortOrder == "asc" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<i class=\"fas fa-sort-up ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<i class=\"fas fa-sort-down ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<i class=\"fas fa-sort ms-1 text-muted\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a></th>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<th class=\"text-dark\">Status</th><th class=\"text-dark\">Actions</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, shard := range data.EcShards { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<tr><td><span class=\"fw-bold\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 string |
|||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", shard.VolumeID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 209, Col: 84} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</span></td><td><span class=\"badge bg-secondary\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 string |
|||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d", shard.ShardID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 212, Col: 96} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span></td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.ShowCollectionColumn { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if shard.Collection != "" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<a href=\"/cluster/ec-shards?collection={shard.Collection}\" class=\"text-decoration-none\"><span class=\"badge bg-info\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var9 string |
|||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(shard.Collection) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 218, Col: 85} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span></a>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<span class=\"text-muted\">default</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<td><code class=\"small\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var10 string |
|||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(shard.Server) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 226, Col: 61} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</code></td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.ShowDataCenterColumn { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<td><span class=\"badge bg-outline-primary\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 string |
|||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(shard.DataCenter) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 230, Col: 88} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span></td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
if data.ShowRackColumn { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<td><span class=\"badge bg-outline-secondary\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var12 string |
|||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(shard.Rack) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 235, Col: 84} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span></td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if shard.IsComplete { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<span class=\"badge bg-success\"><i class=\"fas fa-check me-1\"></i>Complete</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<span class=\"badge bg-warning\"><i class=\"fas fa-exclamation-triangle me-1\"></i> Missing ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var13 string |
|||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(shard.MissingShards))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 246, Col: 88} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " shards</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</td><td><div class=\"btn-group\" role=\"group\"><button type=\"button\" class=\"btn btn-sm btn-outline-primary\" onclick=\"showShardDetails(event)\" data-volume-id=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var14 string |
|||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", shard.VolumeID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 254, Col: 90} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" title=\"View EC volume details\"><i class=\"fas fa-info-circle\"></i></button> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if !shard.IsComplete { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<button type=\"button\" class=\"btn btn-sm btn-outline-warning\" onclick=\"repairVolume(event)\" data-volume-id=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var15 string |
|||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", shard.VolumeID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 261, Col: 94} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\" title=\"Repair missing shards\"><i class=\"fas fa-wrench\"></i></button>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</div></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</tbody></table></div><!-- Pagination -->") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.TotalPages > 1 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<nav aria-label=\"EC Shards pagination\"><ul class=\"pagination justify-content-center\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.CurrentPage > 1 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<li class=\"page-item\"><a class=\"page-link\" href=\"#\" onclick=\"goToPage(event)\" data-page=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var16 string |
|||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage-1)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 280, Col: 129} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\"><i class=\"fas fa-chevron-left\"></i></a></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<!-- First page -->") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.CurrentPage > 3 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "<li class=\"page-item\"><a class=\"page-link\" href=\"#\" onclick=\"goToPage(1)\">1</a></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.CurrentPage > 4 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<li class=\"page-item disabled\"><span class=\"page-link\">...</span></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<!-- Current page and neighbors -->") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.CurrentPage > 1 && data.CurrentPage-1 >= 1 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<li class=\"page-item\"><a class=\"page-link\" href=\"#\" onclick=\"goToPage(event)\" data-page=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var17 string |
|||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage-1)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 301, Col: 129} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var18 string |
|||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage-1)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 301, Col: 170} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</a></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<li class=\"page-item active\"><span class=\"page-link\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var19 string |
|||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 306, Col: 80} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</span></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.CurrentPage < data.TotalPages && data.CurrentPage+1 <= data.TotalPages { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "<li class=\"page-item\"><a class=\"page-link\" href=\"#\" onclick=\"goToPage(event)\" data-page=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var20 string |
|||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage+1)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 311, Col: 129} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var21 string |
|||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage+1)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 311, Col: 170} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</a></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<!-- Last page -->") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.CurrentPage < data.TotalPages-2 { |
|||
if data.CurrentPage < data.TotalPages-3 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<li class=\"page-item disabled\"><span class=\"page-link\">...</span></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, " <li class=\"page-item\"><a class=\"page-link\" href=\"#\" onclick=\"goToPage(event)\" data-page=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var22 string |
|||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalPages)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 323, Col: 126} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var23 string |
|||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalPages)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 323, Col: 164} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</a></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
if data.CurrentPage < data.TotalPages { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<li class=\"page-item\"><a class=\"page-link\" href=\"#\" onclick=\"goToPage(event)\" data-page=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var24 string |
|||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage+1)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 329, Col: 129} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "\"><i class=\"fas fa-chevron-right\"></i></a></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</ul></nav>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<!-- JavaScript --><script>\n function sortBy(field) {\n const currentSort = \"{data.SortBy}\";\n const currentOrder = \"{data.SortOrder}\";\n let newOrder = 'asc';\n \n if (currentSort === field && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n updateUrl({\n sortBy: field,\n sortOrder: newOrder,\n page: 1\n });\n }\n\n function goToPage(event) {\n // Get data from the link element (not any child elements)\n const link = event.target.closest('a');\n const page = link.getAttribute('data-page');\n updateUrl({ page: page });\n }\n\n function changePageSize() {\n const pageSize = document.getElementById('pageSizeSelect').value;\n updateUrl({ pageSize: pageSize, page: 1 });\n }\n\n function updateUrl(params) {\n const url = new URL(window.location);\n Object.keys(params).forEach(key => {\n if (params[key]) {\n url.searchParams.set(key, params[key]);\n } else {\n url.searchParams.delete(key);\n }\n });\n window.location.href = url.toString();\n }\n\n function exportEcShards() {\n const url = new URL('/api/cluster/ec-shards/export', window.location.origin);\n const params = new URLSearchParams(window.location.search);\n params.forEach((value, key) => {\n url.searchParams.set(key, value);\n });\n window.open(url.toString(), '_blank');\n }\n\n function showShardDetails(event) {\n // Get data from the button element (not the icon inside it)\n const button = event.target.closest('button');\n const volumeId = button.getAttribute('data-volume-id');\n \n // Navigate to the EC volume details page\n window.location.href = `/cluster/ec-volumes/${volumeId}`;\n }\n\n function repairVolume(event) {\n // Get data from the button element (not the icon inside it)\n const button = event.target.closest('button');\n const volumeId = button.getAttribute('data-volume-id');\n if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) {\n fetch(`/api/cluster/volumes/${volumeId}/repair`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n }\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Repair initiated successfully');\n location.reload();\n } else {\n alert('Failed to initiate repair: ' + data.error);\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n }\n }\n </script>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
|||
@ -0,0 +1,223 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
templ EcVolumeDetails(data dash.EcVolumeDetailsData) { |
|||
<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-th-large me-2"></i>EC Volume Details |
|||
</h1> |
|||
<nav aria-label="breadcrumb"> |
|||
<ol class="breadcrumb"> |
|||
<li class="breadcrumb-item"><a href="/admin" class="text-decoration-none">Dashboard</a></li> |
|||
<li class="breadcrumb-item"><a href="/cluster/ec-shards" class="text-decoration-none">EC Shards</a></li> |
|||
<li class="breadcrumb-item active" aria-current="page">Volume {fmt.Sprintf("%d", data.VolumeID)}</li> |
|||
</ol> |
|||
</nav> |
|||
</div> |
|||
<div class="btn-toolbar mb-2 mb-md-0"> |
|||
<div class="btn-group me-2"> |
|||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="history.back()"> |
|||
<i class="fas fa-arrow-left me-1"></i>Back |
|||
</button> |
|||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="window.location.reload()"> |
|||
<i class="fas fa-refresh me-1"></i>Refresh |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- EC Volume Summary --> |
|||
<div class="row mb-4"> |
|||
<div class="col-md-6"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h5 class="card-title mb-0"> |
|||
<i class="fas fa-info-circle me-2"></i>Volume Information |
|||
</h5> |
|||
</div> |
|||
<div class="card-body"> |
|||
<table class="table table-borderless"> |
|||
<tr> |
|||
<td><strong>Volume ID:</strong></td> |
|||
<td>{fmt.Sprintf("%d", data.VolumeID)}</td> |
|||
</tr> |
|||
<tr> |
|||
<td><strong>Collection:</strong></td> |
|||
<td> |
|||
if data.Collection != "" { |
|||
<span class="badge bg-info">{data.Collection}</span> |
|||
} else { |
|||
<span class="text-muted">default</span> |
|||
} |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td><strong>Status:</strong></td> |
|||
<td> |
|||
if data.IsComplete { |
|||
<span class="badge bg-success"> |
|||
<i class="fas fa-check me-1"></i>Complete ({data.TotalShards}/14 shards) |
|||
</span> |
|||
} else { |
|||
<span class="badge bg-warning"> |
|||
<i class="fas fa-exclamation-triangle me-1"></i>Incomplete ({data.TotalShards}/14 shards) |
|||
</span> |
|||
} |
|||
</td> |
|||
</tr> |
|||
if !data.IsComplete { |
|||
<tr> |
|||
<td><strong>Missing Shards:</strong></td> |
|||
<td> |
|||
for i, shardID := range data.MissingShards { |
|||
if i > 0 { |
|||
<span>, </span> |
|||
} |
|||
<span class="badge bg-danger">{fmt.Sprintf("%02d", shardID)}</span> |
|||
} |
|||
</td> |
|||
</tr> |
|||
} |
|||
<tr> |
|||
<td><strong>Data Centers:</strong></td> |
|||
<td> |
|||
for i, dc := range data.DataCenters { |
|||
if i > 0 { |
|||
<span>, </span> |
|||
} |
|||
<span class="badge bg-primary">{dc}</span> |
|||
} |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td><strong>Servers:</strong></td> |
|||
<td> |
|||
<span class="text-muted">{fmt.Sprintf("%d servers", len(data.Servers))}</span> |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td><strong>Last Updated:</strong></td> |
|||
<td> |
|||
<span class="text-muted">{data.LastUpdated.Format("2006-01-02 15:04:05")}</span> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-md-6"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h5 class="card-title mb-0"> |
|||
<i class="fas fa-chart-pie me-2"></i>Shard Distribution |
|||
</h5> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="row text-center"> |
|||
<div class="col-4"> |
|||
<div class="border rounded p-3"> |
|||
<h3 class="text-primary mb-1">{fmt.Sprintf("%d", data.TotalShards)}</h3> |
|||
<small class="text-muted">Total Shards</small> |
|||
</div> |
|||
</div> |
|||
<div class="col-4"> |
|||
<div class="border rounded p-3"> |
|||
<h3 class="text-success mb-1">{fmt.Sprintf("%d", len(data.DataCenters))}</h3> |
|||
<small class="text-muted">Data Centers</small> |
|||
</div> |
|||
</div> |
|||
<div class="col-4"> |
|||
<div class="border rounded p-3"> |
|||
<h3 class="text-info mb-1">{fmt.Sprintf("%d", len(data.Servers))}</h3> |
|||
<small class="text-muted">Servers</small> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Shard Distribution Visualization --> |
|||
<div class="mt-3"> |
|||
<h6>Present Shards:</h6> |
|||
<div class="d-flex flex-wrap gap-1"> |
|||
for _, shard := range data.Shards { |
|||
<span class="badge bg-success me-1 mb-1">{fmt.Sprintf("%02d", shard.ShardID)}</span> |
|||
} |
|||
</div> |
|||
if len(data.MissingShards) > 0 { |
|||
<h6 class="mt-2">Missing Shards:</h6> |
|||
<div class="d-flex flex-wrap gap-1"> |
|||
for _, shardID := range data.MissingShards { |
|||
<span class="badge bg-secondary me-1 mb-1">{fmt.Sprintf("%02d", shardID)}</span> |
|||
} |
|||
</div> |
|||
} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Shard Details Table --> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h5 class="card-title mb-0"> |
|||
<i class="fas fa-list me-2"></i>Shard Details |
|||
</h5> |
|||
</div> |
|||
<div class="card-body"> |
|||
if len(data.Shards) > 0 { |
|||
<div class="table-responsive"> |
|||
<table class="table table-striped table-hover"> |
|||
<thead> |
|||
<tr> |
|||
<th>Shard ID</th> |
|||
<th>Server</th> |
|||
<th>Data Center</th> |
|||
<th>Rack</th> |
|||
<th>Disk Type</th> |
|||
<th>Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, shard := range data.Shards { |
|||
<tr> |
|||
<td> |
|||
<span class="badge bg-primary">{fmt.Sprintf("%02d", shard.ShardID)}</span> |
|||
</td> |
|||
<td> |
|||
<code class="small text-dark">{shard.Server}</code> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-primary text-white">{shard.DataCenter}</span> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-secondary text-white">{shard.Rack}</span> |
|||
</td> |
|||
<td> |
|||
<span class="text-dark">{shard.DiskType}</span> |
|||
</td> |
|||
<td> |
|||
<a href={ templ.SafeURL(fmt.Sprintf("http://%s/ui/index.html", shard.Server)) } target="_blank" class="btn btn-sm btn-primary"> |
|||
<i class="fas fa-external-link-alt me-1"></i>Volume Server |
|||
</a> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
} else { |
|||
<div class="text-center py-4"> |
|||
<i class="fas fa-exclamation-triangle fa-3x text-warning mb-3"></i> |
|||
<h5>No EC shards found</h5> |
|||
<p class="text-muted">This volume may not be EC encoded yet.</p> |
|||
</div> |
|||
} |
|||
</div> |
|||
</div> |
|||
} |
|||
@ -0,0 +1,432 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.906
|
|||
package app |
|||
|
|||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|||
|
|||
import "github.com/a-h/templ" |
|||
import templruntime "github.com/a-h/templ/runtime" |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
func EcVolumeDetails(data dash.EcVolumeDetailsData) templ.Component { |
|||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { |
|||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context |
|||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { |
|||
return templ_7745c5c3_CtxErr |
|||
} |
|||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) |
|||
if !templ_7745c5c3_IsBuffer { |
|||
defer func() { |
|||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err == nil { |
|||
templ_7745c5c3_Err = templ_7745c5c3_BufErr |
|||
} |
|||
}() |
|||
} |
|||
ctx = templ.InitializeContext(ctx) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
ctx = templ.ClearChildren(ctx) |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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-th-large me-2\"></i>EC Volume Details</h1><nav aria-label=\"breadcrumb\"><ol class=\"breadcrumb\"><li class=\"breadcrumb-item\"><a href=\"/admin\" class=\"text-decoration-none\">Dashboard</a></li><li class=\"breadcrumb-item\"><a href=\"/cluster/ec-shards\" class=\"text-decoration-none\">EC Shards</a></li><li class=\"breadcrumb-item active\" aria-current=\"page\">Volume ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.VolumeID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 18, Col: 115} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</li></ol></nav></div><div class=\"btn-toolbar mb-2 mb-md-0\"><div class=\"btn-group me-2\"><button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" onclick=\"history.back()\"><i class=\"fas fa-arrow-left me-1\"></i>Back</button> <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" onclick=\"window.location.reload()\"><i class=\"fas fa-refresh me-1\"></i>Refresh</button></div></div></div><!-- EC Volume Summary --><div class=\"row mb-4\"><div class=\"col-md-6\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"card-title mb-0\"><i class=\"fas fa-info-circle me-2\"></i>Volume Information</h5></div><div class=\"card-body\"><table class=\"table table-borderless\"><tr><td><strong>Volume ID:</strong></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.VolumeID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 47, Col: 65} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</td></tr><tr><td><strong>Collection:</strong></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.Collection != "" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"badge bg-info\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 string |
|||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Collection) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 53, Col: 80} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"text-muted\">default</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</td></tr><tr><td><strong>Status:</strong></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.IsComplete { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"badge bg-success\"><i class=\"fas fa-check me-1\"></i>Complete (") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.TotalShards) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 64, Col: 100} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "/14 shards)</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"badge bg-warning\"><i class=\"fas fa-exclamation-triangle me-1\"></i>Incomplete (") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.TotalShards) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 68, Col: 117} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "/14 shards)</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if !data.IsComplete { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<tr><td><strong>Missing Shards:</strong></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for i, shardID := range data.MissingShards { |
|||
if i > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span>, </span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " <span class=\"badge bg-danger\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 string |
|||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d", shardID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 81, Col: 99} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<tr><td><strong>Data Centers:</strong></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for i, dc := range data.DataCenters { |
|||
if i > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span>, </span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " <span class=\"badge bg-primary\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 string |
|||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(dc) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 93, Col: 70} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</td></tr><tr><td><strong>Servers:</strong></td><td><span class=\"text-muted\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var9 string |
|||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d servers", len(data.Servers))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 100, Col: 102} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</span></td></tr><tr><td><strong>Last Updated:</strong></td><td><span class=\"text-muted\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var10 string |
|||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 106, Col: 104} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span></td></tr></table></div></div></div><div class=\"col-md-6\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"card-title mb-0\"><i class=\"fas fa-chart-pie me-2\"></i>Shard Distribution</h5></div><div class=\"card-body\"><div class=\"row text-center\"><div class=\"col-4\"><div class=\"border rounded p-3\"><h3 class=\"text-primary mb-1\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 string |
|||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalShards)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 125, Col: 98} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</h3><small class=\"text-muted\">Total Shards</small></div></div><div class=\"col-4\"><div class=\"border rounded p-3\"><h3 class=\"text-success mb-1\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var12 string |
|||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(data.DataCenters))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 131, Col: 103} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</h3><small class=\"text-muted\">Data Centers</small></div></div><div class=\"col-4\"><div class=\"border rounded p-3\"><h3 class=\"text-info mb-1\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var13 string |
|||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(data.Servers))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 137, Col: 96} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</h3><small class=\"text-muted\">Servers</small></div></div></div><!-- Shard Distribution Visualization --><div class=\"mt-3\"><h6>Present Shards:</h6><div class=\"d-flex flex-wrap gap-1\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, shard := range data.Shards { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<span class=\"badge bg-success me-1 mb-1\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var14 string |
|||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d", shard.ShardID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 148, Col: 108} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if len(data.MissingShards) > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<h6 class=\"mt-2\">Missing Shards:</h6><div class=\"d-flex flex-wrap gap-1\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, shardID := range data.MissingShards { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span class=\"badge bg-secondary me-1 mb-1\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var15 string |
|||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d", shardID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 155, Col: 108} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div></div></div></div></div><!-- Shard Details Table --><div class=\"card\"><div class=\"card-header\"><h5 class=\"card-title mb-0\"><i class=\"fas fa-list me-2\"></i>Shard Details</h5></div><div class=\"card-body\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if len(data.Shards) > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div class=\"table-responsive\"><table class=\"table table-striped table-hover\"><thead><tr><th>Shard ID</th><th>Server</th><th>Data Center</th><th>Rack</th><th>Disk Type</th><th>Actions</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, shard := range data.Shards { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<tr><td><span class=\"badge bg-primary\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var16 string |
|||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d", shard.ShardID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 190, Col: 110} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</span></td><td><code class=\"small text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var17 string |
|||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(shard.Server) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 193, Col: 87} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</code></td><td><span class=\"badge bg-primary text-white\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var18 string |
|||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(shard.DataCenter) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 196, Col: 103} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</span></td><td><span class=\"badge bg-secondary text-white\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var19 string |
|||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(shard.Rack) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 199, Col: 99} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span></td><td><span class=\"text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var20 string |
|||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(shard.DiskType) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 202, Col: 83} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</span></td><td><a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var21 templ.SafeURL |
|||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("http://%s/ui/index.html", shard.Server))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/ec_volume_details.templ`, Line: 205, Col: 121} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" target=\"_blank\" class=\"btn btn-sm btn-primary\"><i class=\"fas fa-external-link-alt me-1\"></i>Volume Server</a></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</tbody></table></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div class=\"text-center py-4\"><i class=\"fas fa-exclamation-triangle fa-3x text-warning mb-3\"></i><h5>No EC shards found</h5><p class=\"text-muted\">This volume may not be EC encoded yet.</p></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</div></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue