mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 04:06:44 +00:00
feat(admin): export full cluster volume list as JSON (#9876)
Adds an "Export All (JSON)" button on the Cluster Volumes page that pulls the whole cluster's volume list from the master in one call, a superset of volume.list. Beyond the table columns it carries garbage and fullness ratios, modified time, compact revision, remote tiering keys, per-disk capacity counts, EC shard sizes with file/delete counts, and a cluster-wide duplicate-volume-id scan. Honors the active collection filter. The existing per-page CSV export stays as "Export Page".
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
package dash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||||
)
|
||||
|
||||
// VolumeListExport is a full-cluster volume report, downloadable from the admin
|
||||
// UI as JSON. It mirrors the topology the `volume.list` shell command walks
|
||||
// (data center -> rack -> node -> disk -> volumes / EC shards) and adds derived
|
||||
// fields the shell command does not print: garbage and fullness ratios, a
|
||||
// human-readable modified time, remote-tiering keys, per-disk capacity counts,
|
||||
// and the cluster-wide duplicate-volume-id scan.
|
||||
type VolumeListExport struct {
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
VolumeSizeLimitMB uint64 `json:"volume_size_limit_mb"`
|
||||
FilterCollection string `json:"filter_collection,omitempty"`
|
||||
Totals VolumeExportTotals `json:"totals"`
|
||||
DuplicateVolumeIds []DuplicateVolumeId `json:"duplicate_volume_ids"`
|
||||
DataCenters []*ExportDataCenter `json:"data_centers"`
|
||||
}
|
||||
|
||||
// VolumeExportTotals aggregates the included records. TotalSize sums normal
|
||||
// volume sizes and EC shard sizes (matching volume.list's statistics); the
|
||||
// file/delete totals cover normal volumes only.
|
||||
type VolumeExportTotals struct {
|
||||
VolumeCount int `json:"volume_count"`
|
||||
EcShardCount int `json:"ec_shard_count"`
|
||||
TotalSize uint64 `json:"total_size"`
|
||||
FileCount uint64 `json:"file_count"`
|
||||
DeletedFileCount uint64 `json:"deleted_file_count"`
|
||||
DeletedBytes uint64 `json:"deleted_bytes"`
|
||||
}
|
||||
|
||||
// DuplicateVolumeId flags a volume id that appears under more than one
|
||||
// collection, where collection.delete or a bare-id operation is dangerous.
|
||||
type DuplicateVolumeId struct {
|
||||
VolumeId uint32 `json:"volume_id"`
|
||||
Collections []string `json:"collections"`
|
||||
}
|
||||
|
||||
type ExportDataCenter struct {
|
||||
Id string `json:"id"`
|
||||
Racks []*ExportRack `json:"racks"`
|
||||
}
|
||||
|
||||
type ExportRack struct {
|
||||
Id string `json:"id"`
|
||||
Nodes []*ExportNode `json:"nodes"`
|
||||
}
|
||||
|
||||
type ExportNode struct {
|
||||
Id string `json:"id"`
|
||||
GrpcPort uint32 `json:"grpc_port,omitempty"`
|
||||
Disks []*ExportDisk `json:"disks"`
|
||||
}
|
||||
|
||||
// ExportDisk capacity counts describe the whole physical disk; the Volumes /
|
||||
// EcShards lists are filtered when a collection filter is active.
|
||||
type ExportDisk struct {
|
||||
DiskType string `json:"disk_type"`
|
||||
DiskId uint32 `json:"disk_id"`
|
||||
VolumeCount int64 `json:"volume_count"`
|
||||
MaxVolumeCount int64 `json:"max_volume_count"`
|
||||
ActiveVolumeCount int64 `json:"active_volume_count"`
|
||||
FreeVolumeCount int64 `json:"free_volume_count"`
|
||||
RemoteVolumeCount int64 `json:"remote_volume_count"`
|
||||
Volumes []*ExportVolume `json:"volumes,omitempty"`
|
||||
EcShards []*ExportEcShard `json:"ec_shards,omitempty"`
|
||||
}
|
||||
|
||||
type ExportVolume struct {
|
||||
Id uint32 `json:"id"`
|
||||
Collection string `json:"collection"`
|
||||
Size uint64 `json:"size"`
|
||||
FileCount uint64 `json:"file_count"`
|
||||
DeleteCount uint64 `json:"delete_count"`
|
||||
DeletedByteCount uint64 `json:"deleted_byte_count"`
|
||||
GarbageRatio float64 `json:"garbage_ratio"`
|
||||
FullnessRatio float64 `json:"fullness_ratio"`
|
||||
ReplicaPlacement string `json:"replica_placement"`
|
||||
Version uint32 `json:"version"`
|
||||
Ttl string `json:"ttl"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
CompactRevision uint32 `json:"compact_revision"`
|
||||
ModifiedAtSecond int64 `json:"modified_at_second"`
|
||||
ModifiedAt string `json:"modified_at,omitempty"`
|
||||
DiskType string `json:"disk_type"`
|
||||
DiskId uint32 `json:"disk_id"`
|
||||
RemoteStorageName string `json:"remote_storage_name,omitempty"`
|
||||
RemoteStorageKey string `json:"remote_storage_key,omitempty"`
|
||||
}
|
||||
|
||||
type ExportEcShard struct {
|
||||
Id uint32 `json:"id"`
|
||||
Collection string `json:"collection"`
|
||||
ShardIds []uint32 `json:"shard_ids"`
|
||||
ShardSizes []int64 `json:"shard_sizes"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
FileCount uint64 `json:"file_count"`
|
||||
DeleteCount uint64 `json:"delete_count"`
|
||||
DiskType string `json:"disk_type"`
|
||||
DiskId uint32 `json:"disk_id"`
|
||||
ExpireAtSec uint64 `json:"expire_at_sec,omitempty"`
|
||||
}
|
||||
|
||||
// ExportClusterVolumeList builds a full-cluster volume report from the master
|
||||
// topology. A non-empty collection limits the listed volumes and EC shards to
|
||||
// that collection; the duplicate-id scan always covers the whole cluster.
|
||||
// generatedAt is passed in so the report is deterministic and testable.
|
||||
func (s *AdminServer) ExportClusterVolumeList(ctx context.Context, collection string, generatedAt time.Time) (*VolumeListExport, error) {
|
||||
export := &VolumeListExport{
|
||||
GeneratedAt: generatedAt,
|
||||
FilterCollection: collection,
|
||||
DuplicateVolumeIds: []DuplicateVolumeId{},
|
||||
DataCenters: []*ExportDataCenter{},
|
||||
}
|
||||
|
||||
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
|
||||
resp, err := client.VolumeList(ctx, &master_pb.VolumeListRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
export.VolumeSizeLimitMB = resp.VolumeSizeLimitMb
|
||||
topo := resp.TopologyInfo
|
||||
if topo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
volumeSizeLimit := resp.VolumeSizeLimitMb * 1024 * 1024
|
||||
export.DuplicateVolumeIds = findDuplicateVolumeIdsForExport(topo)
|
||||
|
||||
dcs := append([]*master_pb.DataCenterInfo(nil), topo.DataCenterInfos...)
|
||||
sort.Slice(dcs, func(i, j int) bool { return dcs[i].Id < dcs[j].Id })
|
||||
for _, dc := range dcs {
|
||||
exportDc := &ExportDataCenter{Id: dc.Id, Racks: []*ExportRack{}}
|
||||
racks := append([]*master_pb.RackInfo(nil), dc.RackInfos...)
|
||||
sort.Slice(racks, func(i, j int) bool { return racks[i].Id < racks[j].Id })
|
||||
for _, rack := range racks {
|
||||
exportRack := &ExportRack{Id: rack.Id, Nodes: []*ExportNode{}}
|
||||
nodes := append([]*master_pb.DataNodeInfo(nil), rack.DataNodeInfos...)
|
||||
sort.Slice(nodes, func(i, j int) bool { return nodes[i].Id < nodes[j].Id })
|
||||
for _, node := range nodes {
|
||||
exportNode := &ExportNode{Id: node.Id, GrpcPort: node.GrpcPort, Disks: []*ExportDisk{}}
|
||||
for _, diskType := range sortedDiskTypes(node.DiskInfos) {
|
||||
// Split per physical disk like volume.list so DiskId is meaningful.
|
||||
for _, diskInfo := range node.DiskInfos[diskType].SplitByPhysicalDisk() {
|
||||
if disk := buildExportDisk(diskInfo, collection, volumeSizeLimit, &export.Totals); disk != nil {
|
||||
exportNode.Disks = append(exportNode.Disks, disk)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(exportNode.Disks) > 0 {
|
||||
exportRack.Nodes = append(exportRack.Nodes, exportNode)
|
||||
}
|
||||
}
|
||||
if len(exportRack.Nodes) > 0 {
|
||||
exportDc.Racks = append(exportDc.Racks, exportRack)
|
||||
}
|
||||
}
|
||||
if len(exportDc.Racks) > 0 {
|
||||
export.DataCenters = append(export.DataCenters, exportDc)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return export, nil
|
||||
}
|
||||
|
||||
func sortedDiskTypes(diskInfos map[string]*master_pb.DiskInfo) []string {
|
||||
types := make([]string, 0, len(diskInfos))
|
||||
for t := range diskInfos {
|
||||
types = append(types, t)
|
||||
}
|
||||
sort.Strings(types)
|
||||
return types
|
||||
}
|
||||
|
||||
// buildExportDisk returns nil when the disk holds nothing matching the filter.
|
||||
func buildExportDisk(diskInfo *master_pb.DiskInfo, collection string, volumeSizeLimit uint64, totals *VolumeExportTotals) *ExportDisk {
|
||||
diskType := diskInfo.Type
|
||||
if diskType == "" {
|
||||
diskType = "hdd"
|
||||
}
|
||||
|
||||
vols := append([]*master_pb.VolumeInformationMessage(nil), diskInfo.VolumeInfos...)
|
||||
sort.Slice(vols, func(i, j int) bool { return vols[i].Id < vols[j].Id })
|
||||
var volumes []*ExportVolume
|
||||
for _, vi := range vols {
|
||||
if collection != "" && !matchesCollection(vi.Collection, collection) {
|
||||
continue
|
||||
}
|
||||
volumes = append(volumes, buildExportVolume(vi, volumeSizeLimit))
|
||||
totals.VolumeCount++
|
||||
totals.TotalSize += vi.Size
|
||||
totals.FileCount += vi.FileCount
|
||||
totals.DeletedFileCount += vi.DeleteCount
|
||||
totals.DeletedBytes += vi.DeletedByteCount
|
||||
}
|
||||
|
||||
shards := append([]*master_pb.VolumeEcShardInformationMessage(nil), diskInfo.EcShardInfos...)
|
||||
sort.Slice(shards, func(i, j int) bool { return shards[i].Id < shards[j].Id })
|
||||
var ecShards []*ExportEcShard
|
||||
for _, eci := range shards {
|
||||
if collection != "" && !matchesCollection(eci.Collection, collection) {
|
||||
continue
|
||||
}
|
||||
shard := buildExportEcShard(eci)
|
||||
ecShards = append(ecShards, shard)
|
||||
totals.EcShardCount++
|
||||
totals.TotalSize += uint64(shard.TotalSize)
|
||||
}
|
||||
|
||||
if len(volumes) == 0 && len(ecShards) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ExportDisk{
|
||||
DiskType: diskType,
|
||||
DiskId: diskInfo.DiskId,
|
||||
VolumeCount: diskInfo.VolumeCount,
|
||||
MaxVolumeCount: diskInfo.MaxVolumeCount,
|
||||
ActiveVolumeCount: diskInfo.ActiveVolumeCount,
|
||||
FreeVolumeCount: diskInfo.FreeVolumeCount,
|
||||
RemoteVolumeCount: diskInfo.RemoteVolumeCount,
|
||||
Volumes: volumes,
|
||||
EcShards: ecShards,
|
||||
}
|
||||
}
|
||||
|
||||
func buildExportVolume(m *master_pb.VolumeInformationMessage, volumeSizeLimit uint64) *ExportVolume {
|
||||
v := &ExportVolume{
|
||||
Id: m.Id,
|
||||
Collection: m.Collection,
|
||||
Size: m.Size,
|
||||
FileCount: m.FileCount,
|
||||
DeleteCount: m.DeleteCount,
|
||||
DeletedByteCount: m.DeletedByteCount,
|
||||
Version: m.Version,
|
||||
ReadOnly: m.ReadOnly,
|
||||
CompactRevision: m.CompactRevision,
|
||||
ModifiedAtSecond: m.ModifiedAtSecond,
|
||||
DiskType: m.DiskType,
|
||||
DiskId: m.DiskId,
|
||||
RemoteStorageName: m.RemoteStorageName,
|
||||
RemoteStorageKey: m.RemoteStorageKey,
|
||||
}
|
||||
// Decode replica placement and TTL the way volume.list does.
|
||||
if vi, err := storage.NewVolumeInfo(m); err == nil {
|
||||
v.ReplicaPlacement = vi.ReplicaPlacement.String()
|
||||
v.Ttl = vi.Ttl.String()
|
||||
}
|
||||
if m.Size > 0 {
|
||||
v.GarbageRatio = float64(m.DeletedByteCount) / float64(m.Size)
|
||||
}
|
||||
if volumeSizeLimit > 0 {
|
||||
v.FullnessRatio = float64(m.Size) / float64(volumeSizeLimit)
|
||||
}
|
||||
if m.ModifiedAtSecond > 0 {
|
||||
v.ModifiedAt = time.Unix(m.ModifiedAtSecond, 0).UTC().Format(time.RFC3339)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func buildExportEcShard(m *master_pb.VolumeEcShardInformationMessage) *ExportEcShard {
|
||||
si := erasure_coding.ShardsInfoFromVolumeEcShardInformationMessage(m)
|
||||
return &ExportEcShard{
|
||||
Id: m.Id,
|
||||
Collection: m.Collection,
|
||||
ShardIds: si.IdsUint32(),
|
||||
ShardSizes: si.SizesInt64(),
|
||||
TotalSize: int64(si.TotalSize()),
|
||||
FileCount: m.FileCount,
|
||||
DeleteCount: m.DeleteCount,
|
||||
DiskType: m.DiskType,
|
||||
DiskId: m.DiskId,
|
||||
ExpireAtSec: m.ExpireAtSec,
|
||||
}
|
||||
}
|
||||
|
||||
// findDuplicateVolumeIdsForExport reports volume ids living under more than one
|
||||
// collection. Volume ids are meant to be globally unique; a duplicate is a
|
||||
// cluster-health hazard (collection.delete destroys one collection's copy,
|
||||
// bare-id lookups are ambiguous). This mirrors volume.list's findDuplicateVolumeIds
|
||||
// rather than reaching into the shell package, per the admin-renderer design.
|
||||
// First collection per id is tracked alone, and a set is allocated only on the
|
||||
// first clash, keeping allocations O(duplicates) on million-volume clusters.
|
||||
func findDuplicateVolumeIdsForExport(topo *master_pb.TopologyInfo) []DuplicateVolumeId {
|
||||
firstCollectionByVid := make(map[uint32]string)
|
||||
collectionsByVid := make(map[uint32]map[string]struct{})
|
||||
note := func(vid uint32, collection string) {
|
||||
if collections := collectionsByVid[vid]; collections != nil {
|
||||
collections[collection] = struct{}{}
|
||||
return
|
||||
}
|
||||
first, seen := firstCollectionByVid[vid]
|
||||
if !seen {
|
||||
firstCollectionByVid[vid] = collection
|
||||
return
|
||||
}
|
||||
if first == collection {
|
||||
return
|
||||
}
|
||||
collectionsByVid[vid] = map[string]struct{}{first: {}, collection: {}}
|
||||
}
|
||||
for _, dc := range topo.DataCenterInfos {
|
||||
for _, rack := range dc.RackInfos {
|
||||
for _, node := range rack.DataNodeInfos {
|
||||
for _, disk := range node.DiskInfos {
|
||||
for _, vi := range disk.VolumeInfos {
|
||||
note(vi.Id, vi.Collection)
|
||||
}
|
||||
for _, ec := range disk.EcShardInfos {
|
||||
note(ec.Id, ec.Collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
duplicates := make([]DuplicateVolumeId, 0, len(collectionsByVid))
|
||||
for vid, set := range collectionsByVid {
|
||||
names := make([]string, 0, len(set))
|
||||
for name := range set {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
duplicates = append(duplicates, DuplicateVolumeId{VolumeId: vid, Collections: names})
|
||||
}
|
||||
sort.Slice(duplicates, func(i, j int) bool { return duplicates[i].VolumeId < duplicates[j].VolumeId })
|
||||
return duplicates
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package dash
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
)
|
||||
|
||||
func TestBuildExportVolumeDerivedFields(t *testing.T) {
|
||||
v := buildExportVolume(&master_pb.VolumeInformationMessage{
|
||||
Id: 7,
|
||||
Collection: "photos",
|
||||
Size: 100,
|
||||
DeletedByteCount: 25,
|
||||
ReplicaPlacement: 0,
|
||||
ModifiedAtSecond: 1700000000,
|
||||
}, 1000)
|
||||
|
||||
if v.GarbageRatio != 0.25 {
|
||||
t.Errorf("garbage ratio = %v, want 0.25", v.GarbageRatio)
|
||||
}
|
||||
if v.FullnessRatio != 0.1 {
|
||||
t.Errorf("fullness ratio = %v, want 0.1", v.FullnessRatio)
|
||||
}
|
||||
if v.ReplicaPlacement != "000" {
|
||||
t.Errorf("replica placement = %q, want 000", v.ReplicaPlacement)
|
||||
}
|
||||
if v.ModifiedAt == "" {
|
||||
t.Error("modified_at should be set when modified_at_second > 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildExportDiskFilterAndTotals(t *testing.T) {
|
||||
disk := &master_pb.DiskInfo{
|
||||
Type: "hdd",
|
||||
VolumeCount: 2,
|
||||
VolumeInfos: []*master_pb.VolumeInformationMessage{
|
||||
{Id: 1, Collection: "keep", Size: 10, FileCount: 3, DeleteCount: 1, DeletedByteCount: 2},
|
||||
{Id: 2, Collection: "drop", Size: 99, FileCount: 9},
|
||||
},
|
||||
EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{
|
||||
{Id: 3, Collection: "keep", EcIndexBits: 0b111, ShardSizes: []int64{4, 4, 4}, FileCount: 5},
|
||||
{Id: 4, Collection: "drop", EcIndexBits: 0b1, ShardSizes: []int64{7}},
|
||||
},
|
||||
}
|
||||
|
||||
var totals VolumeExportTotals
|
||||
got := buildExportDisk(disk, "keep", 1000, &totals)
|
||||
if got == nil {
|
||||
t.Fatal("expected disk with matching records, got nil")
|
||||
}
|
||||
if len(got.Volumes) != 1 || got.Volumes[0].Id != 1 {
|
||||
t.Errorf("expected only volume 1 after filter, got %+v", got.Volumes)
|
||||
}
|
||||
if len(got.EcShards) != 1 || got.EcShards[0].Id != 3 {
|
||||
t.Errorf("expected only ec shard 3 after filter, got %+v", got.EcShards)
|
||||
}
|
||||
if got.VolumeCount != 2 {
|
||||
t.Errorf("disk VolumeCount should stay physical (2), got %d", got.VolumeCount)
|
||||
}
|
||||
if got.EcShards[0].TotalSize != 12 {
|
||||
t.Errorf("ec shard total size = %d, want 12", got.EcShards[0].TotalSize)
|
||||
}
|
||||
|
||||
if totals.VolumeCount != 1 || totals.EcShardCount != 1 {
|
||||
t.Errorf("totals counts = vol %d ec %d, want 1/1", totals.VolumeCount, totals.EcShardCount)
|
||||
}
|
||||
// 10 (volume size) + 12 (kept ec shard sizes).
|
||||
if totals.TotalSize != 22 {
|
||||
t.Errorf("totals TotalSize = %d, want 22", totals.TotalSize)
|
||||
}
|
||||
if totals.FileCount != 3 || totals.DeletedFileCount != 1 || totals.DeletedBytes != 2 {
|
||||
t.Errorf("totals files = %d del %d delBytes %d, want 3/1/2", totals.FileCount, totals.DeletedFileCount, totals.DeletedBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildExportDiskPrunesWhenNoMatch(t *testing.T) {
|
||||
disk := &master_pb.DiskInfo{
|
||||
Type: "hdd",
|
||||
VolumeInfos: []*master_pb.VolumeInformationMessage{{Id: 1, Collection: "other"}},
|
||||
}
|
||||
var totals VolumeExportTotals
|
||||
if got := buildExportDisk(disk, "keep", 0, &totals); got != nil {
|
||||
t.Errorf("expected nil for disk with no matching records, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindDuplicateVolumeIdsForExport(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{"hdd": {
|
||||
VolumeInfos: []*master_pb.VolumeInformationMessage{
|
||||
{Id: 5, Collection: "a"},
|
||||
{Id: 7, Collection: "shared"}, // replica below, same collection
|
||||
},
|
||||
}}},
|
||||
{DiskInfos: map[string]*master_pb.DiskInfo{"hdd": {
|
||||
VolumeInfos: []*master_pb.VolumeInformationMessage{
|
||||
{Id: 5, Collection: "b"}, // 5 now spans a + b -> duplicate
|
||||
{Id: 7, Collection: "shared"}, // replica, not a duplicate
|
||||
},
|
||||
EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{
|
||||
{Id: 5, Collection: "c"}, // 5 also as EC in c
|
||||
},
|
||||
}}},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
dups := findDuplicateVolumeIdsForExport(topo)
|
||||
if len(dups) != 1 {
|
||||
t.Fatalf("expected exactly 1 duplicate, got %+v", dups)
|
||||
}
|
||||
if dups[0].VolumeId != 5 {
|
||||
t.Errorf("duplicate volume id = %d, want 5", dups[0].VolumeId)
|
||||
}
|
||||
want := []string{"a", "b", "c"}
|
||||
if len(dups[0].Collections) != len(want) {
|
||||
t.Fatalf("collections = %v, want %v", dups[0].Collections, want)
|
||||
}
|
||||
for i := range want {
|
||||
if dups[0].Collections[i] != want[i] {
|
||||
t.Errorf("collections[%d] = %q, want %q", i, dups[0].Collections[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,6 +250,7 @@ func (h *AdminHandlers) registerAPIRoutes(api *mux.Router, enforceWrite bool) {
|
||||
filesApi.HandleFunc("/metadata", h.fileBrowserHandlers.ExportMetadata).Methods(http.MethodGet)
|
||||
|
||||
volumeApi := api.PathPrefix("/volumes").Subrouter()
|
||||
volumeApi.HandleFunc("/export", h.clusterHandlers.ExportClusterVolumes).Methods(http.MethodGet)
|
||||
volumeApi.Handle("/{id}/{server}/vacuum", wrapWrite(h.clusterHandlers.VacuumVolume)).Methods(http.MethodPost)
|
||||
|
||||
pluginApi := api.PathPrefix("/plugin").Subrouter()
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
)
|
||||
|
||||
// ClusterHandlers contains all the HTTP handlers for cluster management
|
||||
@@ -88,6 +93,30 @@ func (h *ClusterHandlers) ShowClusterVolumes(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
}
|
||||
|
||||
// ExportClusterVolumes streams the full-cluster volume list as a downloadable
|
||||
// JSON report: every volume and EC shard across the topology, with more fields
|
||||
// than the paginated table (a superset of the volume.list shell command).
|
||||
func (h *ClusterHandlers) ExportClusterVolumes(w http.ResponseWriter, r *http.Request) {
|
||||
collection := r.URL.Query().Get("collection") // Optional collection filter
|
||||
|
||||
export, err := h.adminServer.ExportClusterVolumeList(r.Context(), collection, time.Now().UTC())
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to export volume list: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("seaweedfs-volumes-%s.json", export.GeneratedAt.Format("20060102-150405"))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(export); err != nil {
|
||||
// The response is already streaming, so we can only log a late failure.
|
||||
glog.Errorf("export volume list: encode failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ShowVolumeDetails renders the volume details page
|
||||
func (h *ClusterHandlers) ShowVolumeDetails(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
@@ -736,6 +736,18 @@ function exportVolumes() {
|
||||
downloadCSV(csv, 'seaweedfs-volumes.csv');
|
||||
}
|
||||
|
||||
// Export the full cluster volume list (all volumes + EC shards, more fields than
|
||||
// the table) as a JSON report served by the admin API. Carries the active
|
||||
// collection filter from the page URL.
|
||||
function exportVolumeList() {
|
||||
let url = basePath('/api/volumes/export');
|
||||
const collection = new URLSearchParams(window.location.search).get('collection');
|
||||
if (collection) {
|
||||
url += '?collection=' + encodeURIComponent(collection);
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
// Export collections data as CSV
|
||||
function exportCollections() {
|
||||
const table = document.getElementById('collectionsTable');
|
||||
|
||||
@@ -32,7 +32,10 @@ templ ClusterVolumes(data dash.ClusterVolumesData) {
|
||||
<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="exportVolumes()">
|
||||
<i class="fas fa-download me-1"></i>Export
|
||||
<i class="fas fa-download me-1"></i>Export Page
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="exportVolumeList()" title="Download the full cluster volume list as JSON">
|
||||
<i class="fas fa-file-export me-1"></i>Export All (JSON)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,14 +111,14 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">500 per page</option></select> <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" onclick=\"exportVolumes()\"><i class=\"fas fa-download me-1\"></i>Export</button></div></div></div><div id=\"volumes-content\"><!-- Summary Cards --><div class=\"row mb-4\"><div class=\"col-xl-2 col-md-4 col-sm-6 mb-4\"><div class=\"card border-left-primary 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-primary text-uppercase mb-1\">Total Volumes</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">500 per page</option></select> <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" onclick=\"exportVolumes()\"><i class=\"fas fa-download me-1\"></i>Export Page</button> <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" onclick=\"exportVolumeList()\" title=\"Download the full cluster volume list as JSON\"><i class=\"fas fa-file-export me-1\"></i>Export All (JSON)</button></div></div></div><div id=\"volumes-content\"><!-- Summary Cards --><div class=\"row mb-4\"><div class=\"col-xl-2 col-md-4 col-sm-6 mb-4\"><div class=\"card border-left-primary 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-primary text-uppercase mb-1\">Total Volumes</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(fmt.Sprintf("%d", data.TotalVolumes))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 53, Col: 73}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 56, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -147,7 +147,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.SingleCollection)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 78, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 81, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -157,7 +157,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CollectionCount))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 80, Col: 80}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 83, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -187,7 +187,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.SingleDataCenter)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 106, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 109, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -197,7 +197,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.DataCenterCount))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 108, Col: 80}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 111, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -227,7 +227,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.SingleRack)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 134, Col: 56}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 137, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -237,7 +237,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.RackCount))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 136, Col: 74}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 139, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -267,7 +267,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.SingleDiskType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 162, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 165, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -277,7 +277,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(data.AllDiskTypes, ", "))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 164, Col: 78}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 167, Col: 78}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -307,7 +307,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.SingleVersion)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 190, Col: 59}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 193, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -317,7 +317,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(data.AllVersions, ", "))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 192, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 195, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -331,7 +331,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(data.TotalSize))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 213, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 216, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -472,7 +472,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", volume.Id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 317, Col: 94}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 320, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -485,7 +485,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", volume.Id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 319, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 322, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -498,7 +498,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var18 templ.SafeURL
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("//%s/ui/index.html", volume.Server)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 323, Col: 116}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 326, Col: 116}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -511,7 +511,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Server)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 324, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 327, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -529,7 +529,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(volume.DataCenter)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 330, Col: 105}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 333, Col: 105}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -548,7 +548,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Rack)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 335, Col: 99}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 338, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -572,7 +572,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var22 templ.SafeURL
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(dash.PUrl(ctx, "/storage/volumes?collection=default"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 341, Col: 115}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 344, Col: 115}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -590,7 +590,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var23 templ.SafeURL
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(dash.PUrl(ctx, fmt.Sprintf("/storage/volumes?collection=%s", volume.Collection)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 345, Col: 142}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 348, Col: 142}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -603,7 +603,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Collection)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 346, Col: 107}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 349, Col: 107}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -626,7 +626,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(int64(volume.Size)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 351, Col: 100}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 354, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -648,7 +648,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
return 0
|
||||
}()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 366, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 369, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -661,7 +661,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Active: %s", formatBytes(int64(volume.Size-volume.DeletedByteCount))))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 367, Col: 132}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 370, Col: 132}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -683,7 +683,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
return 0
|
||||
}()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 380, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 383, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -696,7 +696,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Garbage: %s", formatBytes(int64(volume.DeletedByteCount))))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 381, Col: 119}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 384, Col: 119}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -714,7 +714,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
return "N/A"
|
||||
}())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 390, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 393, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -727,7 +727,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", volume.FileCount))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 394, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 397, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -740,7 +740,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%03d", volume.ReplicaPlacement))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 396, Col: 101}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 399, Col: 101}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -758,7 +758,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(volume.DiskType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 400, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 403, Col: 95}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -777,7 +777,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var34 string
|
||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("v%d", volume.Version))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 405, Col: 111}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 408, Col: 111}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -795,7 +795,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var35 string
|
||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", volume.Id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 411, Col: 121}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 414, Col: 121}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -808,7 +808,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", volume.Id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 416, Col: 100}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 419, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -821,7 +821,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var37 string
|
||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Server)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 417, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 420, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -839,7 +839,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var38 string
|
||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", (data.CurrentPage-1)*data.PageSize+1))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 432, Col: 98}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 435, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -852,7 +852,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", minInt(data.CurrentPage*data.PageSize, data.TotalVolumes)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 432, Col: 180}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 435, Col: 180}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -865,7 +865,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var40 string
|
||||
templ_7745c5c3_Var40, 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_volumes.templ`, Line: 432, Col: 222}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 435, Col: 222}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -883,7 +883,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var41 string
|
||||
templ_7745c5c3_Var41, 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_volumes.templ`, Line: 438, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 441, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -896,7 +896,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var42 string
|
||||
templ_7745c5c3_Var42, 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_volumes.templ`, Line: 438, Col: 117}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 441, Col: 117}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -924,7 +924,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var43 string
|
||||
templ_7745c5c3_Var43, 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_volumes.templ`, Line: 452, Col: 138}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 455, Col: 138}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -953,7 +953,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var44 string
|
||||
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 468, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 471, Col: 93}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -971,7 +971,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var45 string
|
||||
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 472, Col: 125}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 475, Col: 125}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -984,7 +984,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var46 string
|
||||
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 472, Col: 148}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 475, Col: 148}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1008,7 +1008,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var47 string
|
||||
templ_7745c5c3_Var47, 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_volumes.templ`, Line: 480, Col: 138}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 483, Col: 138}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1042,7 +1042,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component {
|
||||
var templ_7745c5c3_Var48 string
|
||||
templ_7745c5c3_Var48, 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/cluster_volumes.templ`, Line: 510, Col: 81}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 513, Col: 81}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
Reference in New Issue
Block a user