mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-19 13:46:58 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user