From 40e3eae21f3c9fa6d751307b5843c733b8a196bc Mon Sep 17 00:00:00 2001 From: chrislu Date: Wed, 2 Jul 2025 22:48:21 -0700 Subject: [PATCH] admin ui: filter by collection --- weed/admin/dash/admin_server.go | 48 +- weed/admin/handlers/cluster_handlers.go | 3 +- weed/admin/view/app/cluster_collections.templ | 14 +- .../view/app/cluster_collections_templ.go | 118 ++-- weed/admin/view/app/cluster_volumes.templ | 62 ++- weed/admin/view/app/cluster_volumes_templ.go | 508 ++++++++++-------- 6 files changed, 439 insertions(+), 314 deletions(-) diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index 6fb158894..d15951e49 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -151,16 +151,22 @@ type ClusterVolumesData struct { DataCenterCount int `json:"datacenter_count"` RackCount int `json:"rack_count"` DiskTypeCount int `json:"disk_type_count"` + CollectionCount int `json:"collection_count"` // Conditional display flags ShowDataCenterColumn bool `json:"show_datacenter_column"` ShowRackColumn bool `json:"show_rack_column"` ShowDiskTypeColumn bool `json:"show_disk_type_column"` + ShowCollectionColumn bool `json:"show_collection_column"` // Single values when only one exists SingleDataCenter string `json:"single_datacenter"` SingleRack string `json:"single_rack"` SingleDiskType string `json:"single_disk_type"` + SingleCollection string `json:"single_collection"` + + // Filtering + FilterCollection string `json:"filter_collection"` } type CollectionInfo struct { @@ -795,8 +801,8 @@ func (s *AdminServer) GetClusterVolumeServers() (*ClusterVolumeServersData, erro }, nil } -// GetClusterVolumes retrieves cluster volumes data with pagination and sorting -func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, sortOrder string) (*ClusterVolumesData, error) { +// GetClusterVolumes retrieves cluster volumes data with pagination, sorting, and optional collection filtering +func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, sortOrder string, collection string) (*ClusterVolumesData, error) { // Set defaults if page < 1 { page = 1 @@ -812,7 +818,6 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s } var volumes []VolumeInfo var totalSize int64 - volumeID := 1 // Get detailed volume information via gRPC err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { @@ -840,7 +845,7 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s } volume := VolumeInfo{ - ID: volumeID, + ID: int(volInfo.Id), // Use actual SeaweedFS volume ID Server: node.Id, DataCenter: dc.Id, Rack: rack.Id, @@ -853,7 +858,6 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s } volumes = append(volumes, volume) totalSize += volume.Size - volumeID++ } } } @@ -868,10 +872,25 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s return nil, err } - // Calculate unique data center, rack, and disk type counts from all volumes + // Filter by collection if specified + if collection != "" { + var filteredVolumes []VolumeInfo + var filteredTotalSize int64 + for _, volume := range volumes { + if volume.Collection == collection { + filteredVolumes = append(filteredVolumes, volume) + filteredTotalSize += volume.Size + } + } + volumes = filteredVolumes + totalSize = filteredTotalSize + } + + // Calculate unique data center, rack, disk type, and collection counts from all volumes dataCenterMap := make(map[string]bool) rackMap := make(map[string]bool) diskTypeMap := make(map[string]bool) + collectionMap := make(map[string]bool) for _, volume := range volumes { if volume.DataCenter != "" { dataCenterMap[volume.DataCenter] = true @@ -884,10 +903,14 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s diskType = "hdd" // Default to hdd if not specified } diskTypeMap[diskType] = true + if volume.Collection != "" { + collectionMap[volume.Collection] = true + } } dataCenterCount := len(dataCenterMap) rackCount := len(rackMap) diskTypeCount := len(diskTypeMap) + collectionCount := len(collectionMap) // Sort volumes s.sortVolumes(volumes, sortBy, sortOrder) @@ -915,8 +938,9 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s showDataCenterColumn := dataCenterCount > 1 showRackColumn := rackCount > 1 showDiskTypeColumn := diskTypeCount > 1 + showCollectionColumn := collectionCount > 1 && collection == "" // Hide column when filtering by collection - var singleDataCenter, singleRack, singleDiskType string + var singleDataCenter, singleRack, singleDiskType, singleCollection string if dataCenterCount == 1 { for dc := range dataCenterMap { singleDataCenter = dc @@ -935,6 +959,12 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s break } } + if collectionCount == 1 { + for collection := range collectionMap { + singleCollection = collection + break + } + } return &ClusterVolumesData{ Volumes: volumes, @@ -949,12 +979,16 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s DataCenterCount: dataCenterCount, RackCount: rackCount, DiskTypeCount: diskTypeCount, + CollectionCount: collectionCount, ShowDataCenterColumn: showDataCenterColumn, ShowRackColumn: showRackColumn, ShowDiskTypeColumn: showDiskTypeColumn, + ShowCollectionColumn: showCollectionColumn, SingleDataCenter: singleDataCenter, SingleRack: singleRack, SingleDiskType: singleDiskType, + SingleCollection: singleCollection, + FilterCollection: collection, }, nil } diff --git a/weed/admin/handlers/cluster_handlers.go b/weed/admin/handlers/cluster_handlers.go index 515cdaecb..3c4107533 100644 --- a/weed/admin/handlers/cluster_handlers.go +++ b/weed/admin/handlers/cluster_handlers.go @@ -68,9 +68,10 @@ func (h *ClusterHandlers) ShowClusterVolumes(c *gin.Context) { sortBy := c.DefaultQuery("sortBy", "id") sortOrder := c.DefaultQuery("sortOrder", "asc") + collection := c.Query("collection") // Optional collection filter // Get cluster volumes data - volumesData, err := h.adminServer.GetClusterVolumes(page, pageSize, sortBy, sortOrder) + volumesData, err := h.adminServer.GetClusterVolumes(page, pageSize, sortBy, sortOrder, collection) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster volumes: " + err.Error()}) return diff --git a/weed/admin/view/app/cluster_collections.templ b/weed/admin/view/app/cluster_collections.templ index c91fab539..972998d18 100644 --- a/weed/admin/view/app/cluster_collections.templ +++ b/weed/admin/view/app/cluster_collections.templ @@ -131,13 +131,17 @@ templ ClusterCollections(data dash.ClusterCollectionsData) { for _, collection := range data.Collections { - {collection.Name} + + {collection.Name} + -
- - {fmt.Sprintf("%d", collection.VolumeCount)} -
+ +
+ + {fmt.Sprintf("%d", collection.VolumeCount)} +
+
diff --git a/weed/admin/view/app/cluster_collections_templ.go b/weed/admin/view/app/cluster_collections_templ.go index 1ab6ef924..bb7187ece 100644 --- a/weed/admin/view/app/cluster_collections_templ.go +++ b/weed/admin/view/app/cluster_collections_templ.go @@ -96,157 +96,175 @@ func ClusterCollections(data dash.ClusterCollectionsData) templ.Component { return templ_7745c5c3_Err } for _, collection := range data.Collections { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"text-decoration-none\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", collection.VolumeCount)) + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 139, Col: 90} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 135, Col: 72} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"text-decoration-none\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(collection.TotalSize)) + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", collection.VolumeCount)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 151, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 142, Col: 94} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", collection.FileCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 149, Col: 88} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(collection.TotalSize)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 155, Col: 82} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for i, diskType := range collection.DiskTypes { if i > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var10 = []any{fmt.Sprintf("badge bg-%s me-1", getDiskTypeColor(diskType))} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...) + var templ_7745c5c3_Var12 = []any{fmt.Sprintf("badge bg-%s me-1", getDiskTypeColor(diskType))} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(diskType) + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(diskType) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 159, Col: 131} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 163, Col: 131} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(collection.DiskTypes) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Unknown") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Unknown") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" onclick=\"confirmDeleteCollection(this)\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
No Collections Found

No collections are currently configured in the cluster.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
No Collections Found

No collections are currently configured in the cluster.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Last updated: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
Last updated: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, 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_collections.templ`, Line: 206, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 210, Col: 81} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
Create New Collection
Enter a unique name for the collection
Optional: Specify how long files should be kept
Delete Collection

Are you sure you want to delete the collection ?

This action cannot be undone. All volumes in this collection will be affected.
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
Create New Collection
Enter a unique name for the collection
Optional: Specify how long files should be kept
Delete Collection

Are you sure you want to delete the collection ?

This action cannot be undone. All volumes in this collection will be affected.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/cluster_volumes.templ b/weed/admin/view/app/cluster_volumes.templ index 8d4952db2..7c771b46e 100644 --- a/weed/admin/view/app/cluster_volumes.templ +++ b/weed/admin/view/app/cluster_volumes.templ @@ -7,9 +7,21 @@ import ( templ ClusterVolumes(data dash.ClusterVolumesData) {
-

- Cluster Volumes -

+
+

+ Cluster Volumes +

+ if data.FilterCollection != "" { +
+ + Collection: {data.FilterCollection} + + + Clear Filter + +
+ } +
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -762,16 +824,6 @@ func countUniqueDiskTypes(volumes []dash.VolumeInfo) int { return len(diskTypeMap) } -func countUniqueCollections(volumes []dash.VolumeInfo) int { - collectionMap := make(map[string]bool) - for _, volume := range volumes { - if volume.Collection != "" { - collectionMap[volume.Collection] = true - } - } - return len(collectionMap) -} - func getSortIcon(column, currentSort, currentOrder string) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -788,23 +840,23 @@ func getSortIcon(column, currentSort, currentOrder string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var32 := templ.GetChildren(ctx) - if templ_7745c5c3_Var32 == nil { - templ_7745c5c3_Var32 = templ.NopComponent + templ_7745c5c3_Var34 := templ.GetChildren(ctx) + if templ_7745c5c3_Var34 == nil { + templ_7745c5c3_Var34 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if column != currentSort { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if currentOrder == "asc" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }