admin: dashboard counts chunks, not files (#10598)

* admin: count each chunk once in the dashboard total

The dashboard summed file_count from every node's volume list, so a chunk
was counted once per replica and deleted chunks were never subtracted.
Reuse the collection aggregation, which dedupes replicas and EC shard
holders and nets out tombstones.

* admin: the dashboard card counts chunks, so name it that

Volumes store chunks, and a file is split into one or more of them, so
the 'Total Files' card always read far higher than the number of files in
the filer. Rename it to 'Total Chunks' and say so in the tooltip.

* admin: collections pages count chunks once and say so

The collections list and detail pages summed file_count straight off the
topology, so replicas multiplied the count, tombstones stayed in it, and
the detail page ignored EC volumes entirely. Take the numbers from the
shared collection aggregation and label them chunks.

* admin: dedupe replica chunk counts per volume instead of dividing

Dividing each replica's live count by the copy count truncated a chunk
per odd-sized volume, and reported half the count while a volume's
second replica had not checked in yet. Replicas mirror each other's
needles and deletes, so keep the fullest report per volume id.

* admin: fix the collections CSV export column mapping

The exporter read chunks from the EC-volume cell and shifted size and
disk types with it. Read every column the table actually has.
This commit is contained in:
Chris Lu
2026-08-06 11:22:06 -07:00
committed by GitHub
parent aa04889f05
commit 0cf62a921a
14 changed files with 261 additions and 143 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ const (
type AdminData struct {
Username string `json:"username"`
TotalVolumes int `json:"total_volumes"`
TotalFiles int64 `json:"total_files"`
TotalChunks int64 `json:"total_chunks"`
TotalSize int64 `json:"total_size"`
VolumeSizeLimitMB uint64 `json:"volume_size_limit_mb"`
MasterNodes []MasterNode `json:"master_nodes"`
@@ -199,7 +199,7 @@ func (s *AdminServer) GetAdminData(username string) (AdminData, error) {
adminData := AdminData{
Username: username,
TotalVolumes: topology.TotalVolumes,
TotalFiles: topology.TotalFiles,
TotalChunks: topology.TotalChunks,
TotalSize: topology.TotalSize,
VolumeSizeLimitMB: volumeSizeLimitMB,
MasterNodes: masterNodes,
+41 -3
View File
@@ -1928,9 +1928,20 @@ type ecVolumeCounts struct {
deleteCount uint64
}
// volumeLiveCount is the live chunk count of one regular volume. Replicas
// mirror each other's needles and their deletes, so the fullest report is the
// volume's count — dividing each report by the copy count instead would lose
// a chunk to integer truncation and would halve a volume whose second replica
// has not reported yet.
type volumeLiveCount struct {
collection string
live uint64
}
func collectCollectionStats(topologyInfo *master_pb.TopologyInfo) map[string]collectionStats {
collectionMap := make(map[string]collectionStats)
ecVolumeAgg := make(map[uint32]*ecVolumeCounts)
volumeAgg := make(map[uint32]*volumeLiveCount)
for _, dc := range topologyInfo.DataCenterInfos {
for _, rack := range dc.RackInfos {
for _, node := range rack.DataNodeInfos {
@@ -1951,10 +1962,18 @@ func collectCollectionStats(topologyInfo *master_pb.TopologyInfo) map[string]col
if volInfo.Size >= volInfo.DeletedByteCount {
data.LogicalSize += int64(volInfo.Size-volInfo.DeletedByteCount) / replicaCount
}
if volInfo.FileCount >= volInfo.DeleteCount {
data.FileCount += int64(volInfo.FileCount-volInfo.DeleteCount) / replicaCount
}
collectionMap[collection] = data
if volInfo.FileCount >= volInfo.DeleteCount {
agg, ok := volumeAgg[volInfo.Id]
if !ok {
agg = &volumeLiveCount{collection: collection}
volumeAgg[volInfo.Id] = agg
}
if live := volInfo.FileCount - volInfo.DeleteCount; live > agg.live {
agg.live = live
}
}
}
for _, ecShardInfo := range diskInfo.EcShardInfos {
collection := ecShardInfo.Collection
@@ -1987,6 +2006,14 @@ func collectCollectionStats(topologyInfo *master_pb.TopologyInfo) map[string]col
}
}
// Fold the per-volume live counts in, one entry per volume id no matter
// how many replicas reported it.
for _, agg := range volumeAgg {
data := collectionMap[agg.collection]
data.FileCount += int64(agg.live)
collectionMap[agg.collection] = data
}
// Fold EC per-volume counts into the collection totals. fileCount is
// deduped via max across every node reporting shards for the volume;
// deleteCount is summed across the same nodes.
@@ -2004,6 +2031,17 @@ func collectCollectionStats(topologyInfo *master_pb.TopologyInfo) map[string]col
return collectionMap
}
// totalCollectionFileCount is the cluster-wide live chunk count: the sum of
// every collection's deduped count. Volumes and EC volumes are reported by
// each replica or shard holder, so only this aggregation counts a chunk once.
func totalCollectionFileCount(topologyInfo *master_pb.TopologyInfo) int64 {
var total int64
for _, stats := range collectCollectionStats(topologyInfo) {
total += stats.FileCount
}
return total
}
// getCollectionStats returns current collection statistics with caching
func (s *AdminServer) getCollectionStats() (map[string]collectionStats, error) {
now := time.Now()
+6 -31
View File
@@ -124,13 +124,6 @@ func (s *AdminServer) getTopologyViaGRPC(topology *ClusterTopology) error {
}
if resp.TopologyInfo != nil {
// Dedupe EC volume file counts across the nodes that report
// shards for the same volume: every shard holder reports the
// same .ecx-derived file_count, so we keep the max and sum
// node-local tombstones.
ecFile := make(map[uint32]uint64)
ecDel := make(map[uint32]uint64)
// Process gRPC response
for _, dc := range resp.TopologyInfo.DataCenterInfos {
dataCenter := DataCenter{
@@ -149,7 +142,6 @@ func (s *AdminServer) getTopologyViaGRPC(topology *ClusterTopology) error {
var totalVolumes int64
var totalMaxVolumes int64
var totalSize int64
var totalFiles int64
// Prefer the real physical disk capacity the volume server
// reports per disk; the slot-based estimate overstates capacity
// when maxVolumeCount is configured higher than the disk holds.
@@ -167,23 +159,14 @@ func (s *AdminServer) getTopologyViaGRPC(topology *ClusterTopology) error {
// Sum up individual volume information
for _, volInfo := range diskInfo.VolumeInfos {
totalSize += int64(volInfo.Size)
totalFiles += int64(volInfo.FileCount)
}
// Sum up EC shard sizes on this node and collect
// volume-wide file/delete counts for later folding
// into topology.TotalFiles. ShardSizes is local to
// this node, so summing across nodes is correct;
// FileCount/DeleteCount are per-volume and must be
// deduped per volume id.
// ShardSizes is local to this node, so summing
// across nodes gives the physical footprint.
for _, ecShardInfo := range diskInfo.EcShardInfos {
for _, shardSize := range ecShardInfo.ShardSizes {
totalSize += shardSize
}
if ecShardInfo.FileCount > ecFile[ecShardInfo.Id] {
ecFile[ecShardInfo.Id] = ecShardInfo.FileCount
}
ecDel[ecShardInfo.Id] += ecShardInfo.DeleteCount
}
}
@@ -214,7 +197,6 @@ func (s *AdminServer) getTopologyViaGRPC(topology *ClusterTopology) error {
rackObj.Nodes = append(rackObj.Nodes, vs)
topology.VolumeServers = append(topology.VolumeServers, vs)
topology.TotalVolumes += vs.Volumes
topology.TotalFiles += totalFiles
topology.TotalSize += totalSize
}
@@ -224,17 +206,10 @@ func (s *AdminServer) getTopologyViaGRPC(topology *ClusterTopology) error {
topology.DataCenters = append(topology.DataCenters, dataCenter)
}
// Fold deduped EC file counts into the cluster total so the
// dashboard header does not drop after volumes are converted
// to erasure coding.
for vid, fc := range ecFile {
dc := ecDel[vid]
if fc >= dc {
topology.TotalFiles += int64(fc - dc)
} else {
glog.Warningf("ec volume %d: summed delete_count=%d exceeds file_count=%d; skipping from TotalFiles", vid, dc, fc)
}
}
// Chunk counts come from the shared collection aggregation, which
// nets out tombstones and counts a chunk once no matter how many
// volume replicas or EC shard holders report it.
topology.TotalChunks = totalCollectionFileCount(resp.TopologyInfo)
}
return nil
@@ -235,3 +235,96 @@ func TestCollectCollectionStatsECFileCountMaxDedupe(t *testing.T) {
t.Errorf("FileCount: got %d, want 6 (max across reporters)", got.FileCount)
}
}
// TestTotalCollectionFileCount verifies the cluster-wide chunk count counts a
// chunk once per replicated volume and per EC volume, and nets out deletes.
func TestTotalCollectionFileCount(t *testing.T) {
// Volume 1 has replication 001 (two copies), so both nodes report the same
// 101 chunks with 10 deleted: 91 live chunks in total, not 182 and not the
// 90 that halving each report would give.
replica := func() *master_pb.DiskInfo {
return &master_pb.DiskInfo{
VolumeInfos: []*master_pb.VolumeInformationMessage{
{
Id: 1,
Collection: "bucket-a",
ReplicaPlacement: 1,
FileCount: 101,
DeleteCount: 10,
},
},
}
}
// EC volume 2 has its 20 chunks reported by every shard holder.
ecShards := func(bits uint32, sizes []int64) *master_pb.DiskInfo {
return &master_pb.DiskInfo{
EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{
{
Id: 2,
Collection: "bucket-b",
EcIndexBits: bits,
ShardSizes: sizes,
FileCount: 20,
},
},
}
}
topo := &master_pb.TopologyInfo{
DataCenterInfos: []*master_pb.DataCenterInfo{
{
RackInfos: []*master_pb.RackInfo{
{
DataNodeInfos: []*master_pb.DataNodeInfo{
{DiskInfos: map[string]*master_pb.DiskInfo{
"disk1": replica(),
"disk2": ecShards((1<<0)|(1<<1), []int64{1, 1}),
}},
{DiskInfos: map[string]*master_pb.DiskInfo{
"disk1": replica(),
"disk2": ecShards((1<<2)|(1<<3), []int64{1, 1}),
}},
},
},
},
},
},
}
if got := totalCollectionFileCount(topo); got != 111 {
t.Errorf("totalCollectionFileCount: got %d, want 111 (91 replicated + 20 EC)", got)
}
}
// TestTotalCollectionFileCountUnderReplicated verifies a volume whose second
// replica has not reported yet still contributes its full live count.
func TestTotalCollectionFileCountUnderReplicated(t *testing.T) {
topo := &master_pb.TopologyInfo{
DataCenterInfos: []*master_pb.DataCenterInfo{
{
RackInfos: []*master_pb.RackInfo{
{
DataNodeInfos: []*master_pb.DataNodeInfo{
{DiskInfos: map[string]*master_pb.DiskInfo{
"disk1": {
VolumeInfos: []*master_pb.VolumeInformationMessage{
{
Id: 1,
Collection: "bucket-a",
ReplicaPlacement: 1,
FileCount: 50,
},
},
},
}},
},
},
},
},
},
}
if got := totalCollectionFileCount(topo); got != 50 {
t.Errorf("totalCollectionFileCount: got %d, want 50 (one replica reporting, not halved)", got)
}
}
+22 -13
View File
@@ -13,7 +13,7 @@ func (s *AdminServer) GetClusterCollections() (*ClusterCollectionsData, error) {
var collections []CollectionInfo
var totalVolumes int
var totalEcVolumes int
var totalFiles int64
var totalChunks int64
var totalSize int64
collectionMap := make(map[string]*CollectionInfo)
@@ -46,7 +46,6 @@ func (s *AdminServer) GetClusterCollections() (*ClusterCollectionsData, error) {
// Get or create collection info
if collection, exists := collectionMap[collectionName]; exists {
collection.VolumeCount++
collection.FileCount += int64(volInfo.FileCount)
collection.TotalSize += int64(volInfo.Size)
// Update data center if this collection spans multiple DCs
@@ -67,7 +66,6 @@ func (s *AdminServer) GetClusterCollections() (*ClusterCollectionsData, error) {
}
totalVolumes++
totalFiles += int64(volInfo.FileCount)
totalSize += int64(volInfo.Size)
} else {
newCollection := CollectionInfo{
@@ -75,13 +73,11 @@ func (s *AdminServer) GetClusterCollections() (*ClusterCollectionsData, error) {
DataCenter: dc.Id,
VolumeCount: 1,
EcVolumeCount: 0,
FileCount: int64(volInfo.FileCount),
TotalSize: int64(volInfo.Size),
DiskTypes: []string{diskType},
}
collectionMap[collectionName] = &newCollection
totalVolumes++
totalFiles += int64(volInfo.FileCount)
totalSize += int64(volInfo.Size)
}
}
@@ -133,7 +129,6 @@ func (s *AdminServer) GetClusterCollections() (*ClusterCollectionsData, error) {
DataCenter: dc.Id,
VolumeCount: 0,
EcVolumeCount: 1,
FileCount: 0,
TotalSize: 0,
DiskTypes: []string{diskType},
}
@@ -146,6 +141,16 @@ func (s *AdminServer) GetClusterCollections() (*ClusterCollectionsData, error) {
}
}
}
// Chunk counts come from the shared collection aggregation, which
// nets out tombstones and counts a chunk once no matter how many
// volume replicas or EC shard holders report it.
for collectionName, stats := range collectCollectionStats(resp.TopologyInfo) {
if collection, exists := collectionMap[collectionName]; exists {
collection.ChunkCount = stats.FileCount
totalChunks += stats.FileCount
}
}
}
return nil
@@ -173,7 +178,7 @@ func (s *AdminServer) GetClusterCollections() (*ClusterCollectionsData, error) {
TotalCollections: 0,
TotalVolumes: 0,
TotalEcVolumes: 0,
TotalFiles: 0,
TotalChunks: 0,
TotalSize: 0,
LastUpdated: time.Now(),
}, nil
@@ -184,7 +189,7 @@ func (s *AdminServer) GetClusterCollections() (*ClusterCollectionsData, error) {
TotalCollections: len(collections),
TotalVolumes: totalVolumes,
TotalEcVolumes: totalEcVolumes,
TotalFiles: totalFiles,
TotalChunks: totalChunks,
TotalSize: totalSize,
LastUpdated: time.Now(),
}, nil
@@ -208,7 +213,6 @@ func (s *AdminServer) GetCollectionDetails(collectionName string, page int, page
var regularVolumes []VolumeWithTopology
var ecVolumes []EcVolumeWithShards
var totalFiles int64
var totalSize int64
dataCenters := make(map[string]bool)
diskTypes := make(map[string]bool)
@@ -222,10 +226,15 @@ func (s *AdminServer) GetCollectionDetails(collectionName string, page int, page
regularVolumes = regularVolumeData.Volumes
totalSize = regularVolumeData.TotalSize
// Calculate total files from regular volumes
for _, vol := range regularVolumes {
totalFiles += int64(vol.FileCount)
// Chunk counts come from the shared collection aggregation, which nets out
// tombstones and counts a chunk once no matter how many volume replicas or
// EC shard holders report it. Fail rather than render a zero that reads
// like an empty collection.
stats, err := s.getCollectionStats()
if err != nil {
return nil, err
}
totalChunks := stats[collectionName].FileCount
// Collect data centers and disk types from regular volumes
for _, vol := range regularVolumes {
@@ -371,7 +380,7 @@ func (s *AdminServer) GetCollectionDetails(collectionName string, page int, page
EcVolumes: paginatedEcVolumes,
TotalVolumes: len(regularVolumes),
TotalEcVolumes: len(ecVolumes),
TotalFiles: totalFiles,
TotalChunks: totalChunks,
TotalSize: totalSize,
DataCenters: dcList,
DiskTypes: diskTypeList,
+4 -4
View File
@@ -18,7 +18,7 @@ type dashSample struct {
volumes float64
ecShards float64
diskUsed float64
files float64
chunks float64
tasks float64 // pending/assigned/in-progress maintenance tasks
workers float64
}
@@ -33,7 +33,7 @@ type DashboardTrends struct {
// Sparklines (raw <svg>) for the existing summary cards.
Volumes string `json:"-"`
Files string `json:"-"`
Chunks string `json:"-"`
DiskUsed string `json:"-"`
EcShards string `json:"-"`
@@ -61,7 +61,7 @@ func (s *AdminServer) recordDashboardSample() {
volumes: float64(topology.TotalVolumes),
ecShards: float64(ecShards),
diskUsed: float64(topology.TotalSize),
files: float64(topology.TotalFiles),
chunks: float64(topology.TotalChunks),
}
if s.maintenanceManager != nil {
if stats := s.maintenanceManager.GetStats(); stats != nil {
@@ -106,7 +106,7 @@ func (s *AdminServer) GetDashboardTrends() DashboardTrends {
return DashboardTrends{
Samples: len(samples),
Volumes: sparklineSVG(series(func(s dashSample) float64 { return s.volumes }), "#1cc88a"), // success
Files: sparklineSVG(series(func(s dashSample) float64 { return s.files }), "#36b9cc"), // info
Chunks: sparklineSVG(series(func(s dashSample) float64 { return s.chunks }), "#36b9cc"), // info
DiskUsed: sparklineSVG(series(func(s dashSample) float64 { return s.diskUsed }), "#f6c23e"), // warning
EcShards: sparklineSVG(series(func(s dashSample) float64 { return s.ecShards }), "#5a5c69"), // dark
Tasks: sparklineSVG(tasks, "#36b9cc"),
+8 -6
View File
@@ -14,9 +14,11 @@ type ClusterTopology struct {
DataCenters []DataCenter `json:"datacenters"`
VolumeServers []VolumeServer `json:"volume_servers"`
TotalVolumes int `json:"total_volumes"`
TotalFiles int64 `json:"total_files"`
TotalSize int64 `json:"total_size"`
UpdatedAt time.Time `json:"updated_at"`
// TotalChunks counts chunks stored in volumes, not filer entries: a file
// is split into one or more chunks.
TotalChunks int64 `json:"total_chunks"`
TotalSize int64 `json:"total_size"`
UpdatedAt time.Time `json:"updated_at"`
}
type MasterNode struct {
@@ -274,7 +276,7 @@ type CollectionInfo struct {
DataCenter string `json:"datacenter"`
VolumeCount int `json:"volume_count"`
EcVolumeCount int `json:"ec_volume_count"`
FileCount int64 `json:"file_count"`
ChunkCount int64 `json:"chunk_count"`
TotalSize int64 `json:"total_size"`
DiskTypes []string `json:"disk_types"`
}
@@ -285,7 +287,7 @@ type ClusterCollectionsData struct {
TotalCollections int `json:"total_collections"`
TotalVolumes int `json:"total_volumes"`
TotalEcVolumes int `json:"total_ec_volumes"`
TotalFiles int64 `json:"total_files"`
TotalChunks int64 `json:"total_chunks"`
TotalSize int64 `json:"total_size"`
LastUpdated time.Time `json:"last_updated"`
}
@@ -588,7 +590,7 @@ type CollectionDetailsData struct {
EcVolumes []EcVolumeWithShards `json:"ec_volumes"`
TotalVolumes int `json:"total_volumes"`
TotalEcVolumes int `json:"total_ec_volumes"`
TotalFiles int64 `json:"total_files"`
TotalChunks int64 `json:"total_chunks"`
TotalSize int64 `json:"total_size"`
DataCenters []string `json:"data_centers"`
DiskTypes []string `json:"disk_types"`
+1 -1
View File
@@ -577,7 +577,7 @@ func (h *AdminHandlers) getAdminData(r *http.Request) dash.AdminData {
return dash.AdminData{
Username: username,
TotalVolumes: 0,
TotalFiles: 0,
TotalChunks: 0,
TotalSize: 0,
MasterNodes: masterNodes,
VolumeServers: []dash.VolumeServer{},
+5 -5
View File
@@ -50,18 +50,18 @@ templ Admin(data dash.AdminData) {
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Total Files
<div class="text-xs font-weight-bold text-info text-uppercase mb-1" title="Chunks stored in volumes. A file is split into one or more chunks, so this is not the number of files.">
Total Chunks
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{formatNumber(data.TotalFiles)}
{formatNumber(data.TotalChunks)}
</div>
</div>
<div class="col-auto">
<i class="fas fa-file fa-2x text-gray-300"></i>
<i class="fas fa-cubes fa-2x text-gray-300"></i>
</div>
</div>
@templ.Raw(data.Trends.Files)
@templ.Raw(data.Trends.Chunks)
</div>
</div>
</div>
+6 -6
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
// templ: version: v0.3.1020
package app
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -68,24 +68,24 @@ func Admin(data dash.AdminData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div></div><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-info shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-info text-uppercase mb-1\">Total Files</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div></div><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-info shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-info text-uppercase mb-1\" title=\"Chunks stored in volumes. A file is split into one or more chunks, so this is not the number of files.\">Total Chunks</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(formatNumber(data.TotalFiles))
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(formatNumber(data.TotalChunks))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 57, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 57, Col: 67}
}
_, 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, "</div></div><div class=\"col-auto\"><i class=\"fas fa-file fa-2x text-gray-300\"></i></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div><div class=\"col-auto\"><i class=\"fas fa-cubes fa-2x text-gray-300\"></i></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Trends.Files).Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Err = templ.Raw(data.Trends.Chunks).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+20 -19
View File
@@ -87,15 +87,15 @@ templ ClusterCollections(data dash.ClusterCollectionsData) {
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Total Files
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1" title="Chunks stored in volumes. A file is split into one or more chunks, so this is not the number of files.">
Total Chunks
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{fmt.Sprintf("%d", data.TotalFiles)}
{fmt.Sprintf("%d", data.TotalChunks)}
</div>
</div>
<div class="col-auto">
<i class="fas fa-file fa-2x text-gray-300"></i>
<i class="fas fa-cubes fa-2x text-gray-300"></i>
</div>
</div>
</div>
@@ -139,7 +139,7 @@ templ ClusterCollections(data dash.ClusterCollectionsData) {
<th>Collection Name</th>
<th>Regular Volumes</th>
<th>EC Volumes</th>
<th>Files</th>
<th>Chunks</th>
<th>Size (Logical)</th>
<th>Disk Types</th>
<th>Actions</th>
@@ -179,8 +179,8 @@ templ ClusterCollections(data dash.ClusterCollectionsData) {
</td>
<td>
<div class="d-flex align-items-center">
<i class="fas fa-file me-2 text-muted"></i>
{fmt.Sprintf("%d", collection.FileCount)}
<i class="fas fa-cubes me-2 text-muted"></i>
{fmt.Sprintf("%d", collection.ChunkCount)}
</div>
</td>
<td>
@@ -209,7 +209,7 @@ templ ClusterCollections(data dash.ClusterCollectionsData) {
data-datacenter={collection.DataCenter}
data-volume-count={fmt.Sprintf("%d", collection.VolumeCount)}
data-ec-volume-count={fmt.Sprintf("%d", collection.EcVolumeCount)}
data-file-count={fmt.Sprintf("%d", collection.FileCount)}
data-chunk-count={fmt.Sprintf("%d", collection.ChunkCount)}
data-total-size={fmt.Sprintf("%d", collection.TotalSize)}
data-disk-types={formatDiskTypes(collection.DiskTypes)}>
<i class="fas fa-eye"></i>
@@ -262,7 +262,7 @@ templ ClusterCollections(data dash.ClusterCollectionsData) {
datacenter: button.getAttribute('data-datacenter'),
volumeCount: parseInt(button.getAttribute('data-volume-count')),
ecVolumeCount: parseInt(button.getAttribute('data-ec-volume-count')),
fileCount: parseInt(button.getAttribute('data-file-count')),
chunkCount: parseInt(button.getAttribute('data-chunk-count')),
totalSize: parseInt(button.getAttribute('data-total-size')),
diskTypes: button.getAttribute('data-disk-types')
};
@@ -311,10 +311,10 @@ templ ClusterCollections(data dash.ClusterCollectionsData) {
'<span>' + collection.ecVolumeCount.toLocaleString() + '</span>' +
'</div>' +
'</td></tr>' +
'<tr><td><strong>Total Files:</strong></td><td>' +
'<tr><td><strong>Total Chunks:</strong></td><td>' +
'<div class="d-flex align-items-center">' +
'<i class="fas fa-file me-2 text-muted"></i>' +
'<span>' + collection.fileCount.toLocaleString() + '</span>' +
'<i class="fas fa-cubes me-2 text-muted"></i>' +
'<span>' + collection.chunkCount.toLocaleString() + '</span>' +
'</div>' +
'</td></tr>' +
'<tr><td><strong>Total Size (Logical):</strong></td><td>' +
@@ -396,17 +396,18 @@ templ ClusterCollections(data dash.ClusterCollectionsData) {
return {
name: cells[0].textContent.trim(),
volumes: cells[1].textContent.trim(),
files: cells[2].textContent.trim(),
size: cells[3].textContent.trim(),
diskTypes: cells[4].textContent.trim()
ecVolumes: cells[2].textContent.trim(),
chunks: cells[3].textContent.trim(),
size: cells[4].textContent.trim(),
diskTypes: cells[5].textContent.trim()
};
}
return null;
}).filter(row => row !== null);
const csvContent = "data:text/csv;charset=utf-8," +
"Collection Name,Volumes,Files,Size,Disk Types\n" +
rows.map(r => '"' + r.name + '","' + r.volumes + '","' + r.files + '","' + r.size + '","' + r.diskTypes + '"').join("\n");
const csvContent = "data:text/csv;charset=utf-8," +
"Collection Name,Volumes,EC Volumes,Chunks,Size,Disk Types\n" +
rows.map(r => '"' + r.name + '","' + r.volumes + '","' + r.ecVolumes + '","' + r.chunks + '","' + r.size + '","' + r.diskTypes + '"').join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -71,12 +71,12 @@ templ CollectionDetails(data dash.CollectionDetailsData) {
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="card-title">Total Files</h6>
<h4 class="mb-0">{fmt.Sprintf("%d", data.TotalFiles)}</h4>
<small>Files stored</small>
<h6 class="card-title">Total Chunks</h6>
<h4 class="mb-0">{fmt.Sprintf("%d", data.TotalChunks)}</h4>
<small>A file is split into one or more chunks</small>
</div>
<div class="align-self-center">
<i class="fas fa-file fa-2x"></i>
<i class="fas fa-cubes fa-2x"></i>
</div>
</div>
</div>
@@ -169,7 +169,7 @@ templ CollectionDetails(data dash.CollectionDetailsData) {
</a>
</th>
<th class="text-dark">Logical Size / Shard Count</th>
<th class="text-dark">Files</th>
<th class="text-dark">Chunks</th>
<th class="text-dark">Status</th>
<th class="text-dark">Actions</th>
</tr>
+20 -20
View File
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
// templ: version: v0.3.1020
package app
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -113,20 +113,20 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h4><small>Erasure coded volumes</small></div><div class=\"align-self-center\"><i class=\"fas fa-th-large 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\">Total Files</h6><h4 class=\"mb-0\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h4><small>Erasure coded volumes</small></div><div class=\"align-self-center\"><i class=\"fas fa-th-large 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\">Total Chunks</h6><h4 class=\"mb-0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalFiles))
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalChunks))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/collection_details.templ`, Line: 75, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/collection_details.templ`, Line: 75, Col: 60}
}
_, 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, 8, "</h4><small>Files stored</small></div><div class=\"align-self-center\"><i class=\"fas fa-file 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\">Total Size (Logical)</h6><h4 class=\"mb-0\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</h4><small>A file is split into one or more chunks</small></div><div class=\"align-self-center\"><i class=\"fas fa-cubes fa-2x\"></i></div></div></div></div></div><div class=\"col-md-3\"><div class=\"card text-bg-warning\"><div class=\"card-body\"><div class=\"d-flex justify-content-between\"><div><h6 class=\"card-title\">Total Size (Logical)</h6><h4 class=\"mb-0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -269,7 +269,7 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</a></th><th class=\"text-dark\">Logical Size / Shard Count</th><th class=\"text-dark\">Files</th><th class=\"text-dark\">Status</th><th class=\"text-dark\">Actions</th></tr></thead> <tbody>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</a></th><th class=\"text-dark\">Logical Size / Shard Count</th><th class=\"text-dark\">Chunks</th><th class=\"text-dark\">Status</th><th class=\"text-dark\">Actions</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -333,11 +333,11 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", volume.Id))
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", volume.Id))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/collection_details.templ`, Line: 206, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -346,11 +346,11 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Server)
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(volume.Server)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/collection_details.templ`, Line: 207, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -419,11 +419,11 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", ecVolume.VolumeID))
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", ecVolume.VolumeID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/collection_details.templ`, Line: 249, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -466,11 +466,11 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.Page-1))
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", data.Page-1))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/collection_details.templ`, Line: 280, Col: 104}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -504,11 +504,11 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i))
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", i))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/collection_details.templ`, Line: 291, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -547,11 +547,11 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.Page+1))
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", data.Page+1))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/collection_details.templ`, Line: 306, Col: 104}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -560,11 +560,11 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalPages))
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", data.TotalPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/collection_details.templ`, Line: 309, Col: 108}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}