diff options
| author | chrislu <chris.lu@gmail.com> | 2025-07-04 12:49:34 -0700 |
|---|---|---|
| committer | chrislu <chris.lu@gmail.com> | 2025-07-04 12:49:34 -0700 |
| commit | 84d4ea0995a9cc01808f3fedbf1b671a6538be32 (patch) | |
| tree | 3971a806257c01b8f5b8173242ed6126af635ab2 | |
| parent | 2cfe079a1fac21d5d654d0e6ed2f1996d5b89209 (diff) | |
| download | seaweedfs-84d4ea0995a9cc01808f3fedbf1b671a6538be32.tar.xz seaweedfs-84d4ea0995a9cc01808f3fedbf1b671a6538be32.zip | |
show volume details
| -rw-r--r-- | weed/admin/dash/admin_server.go | 162 | ||||
| -rw-r--r-- | weed/admin/handlers/admin_handlers.go | 2 | ||||
| -rw-r--r-- | weed/admin/handlers/cluster_handlers.go | 39 | ||||
| -rw-r--r-- | weed/admin/view/app/cluster_volumes.templ | 43 | ||||
| -rw-r--r-- | weed/admin/view/app/cluster_volumes_templ.go | 165 | ||||
| -rw-r--r-- | weed/admin/view/app/volume_details.templ | 425 | ||||
| -rw-r--r-- | weed/admin/view/app/volume_details_templ.go | 690 |
7 files changed, 1391 insertions, 135 deletions
diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index 61d15c755..89bae8ea2 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -115,25 +115,19 @@ type ClusterVolumeServersData struct { LastUpdated time.Time `json:"last_updated"` } -type VolumeInfo struct { - ID int `json:"id"` - Server string `json:"server"` - DataCenter string `json:"datacenter"` - Rack string `json:"rack"` - Collection string `json:"collection"` - Size int64 `json:"size"` - FileCount int64 `json:"file_count"` - Replication string `json:"replication"` - DiskType string `json:"disk_type"` - Version uint32 `json:"version"` +type VolumeWithTopology struct { + *master_pb.VolumeInformationMessage + Server string `json:"server"` + DataCenter string `json:"datacenter"` + Rack string `json:"rack"` } type ClusterVolumesData struct { - Username string `json:"username"` - Volumes []VolumeInfo `json:"volumes"` - TotalVolumes int `json:"total_volumes"` - TotalSize int64 `json:"total_size"` - LastUpdated time.Time `json:"last_updated"` + Username string `json:"username"` + Volumes []VolumeWithTopology `json:"volumes"` + TotalVolumes int `json:"total_volumes"` + TotalSize int64 `json:"total_size"` + LastUpdated time.Time `json:"last_updated"` // Pagination CurrentPage int `json:"current_page"` @@ -175,6 +169,14 @@ type ClusterVolumesData struct { FilterCollection string `json:"filter_collection"` } +type VolumeDetailsData struct { + Volume VolumeWithTopology `json:"volume"` + Replicas []VolumeWithTopology `json:"replicas"` + VolumeSizeLimit uint64 `json:"volume_size_limit"` + ReplicationCount int `json:"replication_count"` + LastUpdated time.Time `json:"last_updated"` +} + type CollectionInfo struct { Name string `json:"name"` DataCenter string `json:"datacenter"` @@ -816,7 +818,7 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s if sortOrder == "" { sortOrder = "asc" } - var volumes []VolumeInfo + var volumes []VolumeWithTopology var totalSize int64 // Get detailed volume information via gRPC @@ -832,31 +834,14 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s for _, node := range rack.DataNodeInfos { for _, diskInfo := range node.DiskInfos { for _, volInfo := range diskInfo.VolumeInfos { - // Extract collection name from volume info - collectionName := volInfo.Collection - // Keep original collection name, don't default to "default" - // This way filtering works correctly - - // Get disk type from volume info, default to hdd if empty - diskType := volInfo.DiskType - if diskType == "" { - diskType = "hdd" - } - - volume := VolumeInfo{ - ID: int(volInfo.Id), // Use actual SeaweedFS volume ID - Server: node.Id, - DataCenter: dc.Id, - Rack: rack.Id, - Collection: collectionName, // Keep original, even if empty - Size: int64(volInfo.Size), - FileCount: int64(volInfo.FileCount), - Replication: fmt.Sprintf("%03d", volInfo.ReplicaPlacement), - DiskType: diskType, - Version: volInfo.Version, + volume := VolumeWithTopology{ + VolumeInformationMessage: volInfo, + Server: node.Id, + DataCenter: dc.Id, + Rack: rack.Id, } volumes = append(volumes, volume) - totalSize += volume.Size + totalSize += int64(volInfo.Size) } } } @@ -873,7 +858,7 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s // Filter by collection if specified if collection != "" { - var filteredVolumes []VolumeInfo + var filteredVolumes []VolumeWithTopology var filteredTotalSize int64 for _, volume := range volumes { // Handle "default" collection filtering for empty collections @@ -884,7 +869,7 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s if volumeCollection == collection { filteredVolumes = append(filteredVolumes, volume) - filteredTotalSize += volume.Size + filteredTotalSize += int64(volume.Size) } } volumes = filteredVolumes @@ -939,7 +924,7 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s startIndex := (page - 1) * pageSize endIndex := startIndex + pageSize if startIndex >= totalVolumes { - volumes = []VolumeInfo{} + volumes = []VolumeWithTopology{} } else { if endIndex > totalVolumes { endIndex = totalVolumes @@ -1032,13 +1017,13 @@ func (s *AdminServer) GetClusterVolumes(page int, pageSize int, sortBy string, s } // sortVolumes sorts the volumes slice based on the specified field and order -func (s *AdminServer) sortVolumes(volumes []VolumeInfo, sortBy string, sortOrder string) { +func (s *AdminServer) sortVolumes(volumes []VolumeWithTopology, sortBy string, sortOrder string) { sort.Slice(volumes, func(i, j int) bool { var less bool switch sortBy { case "id": - less = volumes[i].ID < volumes[j].ID + less = volumes[i].Id < volumes[j].Id case "server": less = volumes[i].Server < volumes[j].Server case "datacenter": @@ -1052,13 +1037,13 @@ func (s *AdminServer) sortVolumes(volumes []VolumeInfo, sortBy string, sortOrder case "filecount": less = volumes[i].FileCount < volumes[j].FileCount case "replication": - less = volumes[i].Replication < volumes[j].Replication + less = volumes[i].ReplicaPlacement < volumes[j].ReplicaPlacement case "disktype": less = volumes[i].DiskType < volumes[j].DiskType case "version": less = volumes[i].Version < volumes[j].Version default: - less = volumes[i].ID < volumes[j].ID + less = volumes[i].Id < volumes[j].Id } if sortOrder == "desc" { @@ -1321,3 +1306,86 @@ func (s *AdminServer) GetClusterFilers() (*ClusterFilersData, error) { func (s *AdminServer) GetAllFilers() []string { return s.getDiscoveredFilers() } + +// GetVolumeDetails retrieves detailed information about a specific volume +func (s *AdminServer) GetVolumeDetails(volumeID int, server string) (*VolumeDetailsData, error) { + var primaryVolume VolumeWithTopology + var replicas []VolumeWithTopology + var volumeSizeLimit uint64 + var found bool + + // Find the volume and all its replicas in the cluster + err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { + resp, err := client.VolumeList(context.Background(), &master_pb.VolumeListRequest{}) + if err != nil { + return err + } + + if resp.TopologyInfo != nil { + for _, dc := range resp.TopologyInfo.DataCenterInfos { + for _, rack := range dc.RackInfos { + for _, node := range rack.DataNodeInfos { + for _, diskInfo := range node.DiskInfos { + for _, volInfo := range diskInfo.VolumeInfos { + if int(volInfo.Id) == volumeID { + diskType := volInfo.DiskType + if diskType == "" { + diskType = "hdd" + } + + volume := VolumeWithTopology{ + VolumeInformationMessage: volInfo, + Server: node.Id, + DataCenter: dc.Id, + Rack: rack.Id, + } + + // If this is the requested server, it's the primary volume + if node.Id == server { + primaryVolume = volume + found = true + } else { + // This is a replica on another server + replicas = append(replicas, volume) + } + } + } + } + } + } + } + } + return nil + }) + + if err != nil { + return nil, err + } + + if !found { + return nil, fmt.Errorf("volume %d not found on server %s", volumeID, server) + } + + // Get volume size limit from master + err = s.WithMasterClient(func(client master_pb.SeaweedClient) error { + resp, err := client.GetMasterConfiguration(context.Background(), &master_pb.GetMasterConfigurationRequest{}) + if err != nil { + return err + } + volumeSizeLimit = uint64(resp.VolumeSizeLimitMB) * 1024 * 1024 // Convert MB to bytes + return nil + }) + + if err != nil { + // If we can't get the limit, set a default + volumeSizeLimit = 30 * 1024 * 1024 * 1024 // 30GB default + } + + return &VolumeDetailsData{ + Volume: primaryVolume, + Replicas: replicas, + VolumeSizeLimit: volumeSizeLimit, + ReplicationCount: len(replicas) + 1, // Include the primary volume + LastUpdated: time.Now(), + }, nil +} diff --git a/weed/admin/handlers/admin_handlers.go b/weed/admin/handlers/admin_handlers.go index 7617a0539..012694ffb 100644 --- a/weed/admin/handlers/admin_handlers.go +++ b/weed/admin/handlers/admin_handlers.go @@ -66,6 +66,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, protected.GET("/cluster/filers", h.clusterHandlers.ShowClusterFilers) protected.GET("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers) protected.GET("/cluster/volumes", h.clusterHandlers.ShowClusterVolumes) + protected.GET("/cluster/volumes/:id/:server", h.clusterHandlers.ShowVolumeDetails) protected.GET("/cluster/collections", h.clusterHandlers.ShowClusterCollections) // API routes for AJAX calls @@ -130,6 +131,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, r.GET("/cluster/filers", h.clusterHandlers.ShowClusterFilers) r.GET("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers) r.GET("/cluster/volumes", h.clusterHandlers.ShowClusterVolumes) + r.GET("/cluster/volumes/:id/:server", h.clusterHandlers.ShowVolumeDetails) r.GET("/cluster/collections", h.clusterHandlers.ShowClusterCollections) // API routes for AJAX calls diff --git a/weed/admin/handlers/cluster_handlers.go b/weed/admin/handlers/cluster_handlers.go index b89d6aa29..769cc2894 100644 --- a/weed/admin/handlers/cluster_handlers.go +++ b/weed/admin/handlers/cluster_handlers.go @@ -95,6 +95,45 @@ func (h *ClusterHandlers) ShowClusterVolumes(c *gin.Context) { } } +// ShowVolumeDetails renders the volume details page +func (h *ClusterHandlers) ShowVolumeDetails(c *gin.Context) { + volumeIDStr := c.Param("id") + server := c.Param("server") + + if volumeIDStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Volume ID is required"}) + return + } + + if server == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Server is required"}) + return + } + + volumeID, err := strconv.Atoi(volumeIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid volume ID"}) + return + } + + // Get volume details + volumeDetails, err := h.adminServer.GetVolumeDetails(volumeID, server) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get volume details: " + err.Error()}) + return + } + + // Render HTML template + c.Header("Content-Type", "text/html") + volumeDetailsComponent := app.VolumeDetails(*volumeDetails) + layoutComponent := layout.Layout(c, volumeDetailsComponent) + err = layoutComponent.Render(c.Request.Context(), c.Writer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) + return + } +} + // ShowClusterCollections renders the cluster collections page func (h *ClusterHandlers) ShowClusterCollections(c *gin.Context) { // Get cluster collections data diff --git a/weed/admin/view/app/cluster_volumes.templ b/weed/admin/view/app/cluster_volumes.templ index 1973fdcdb..b21918066 100644 --- a/weed/admin/view/app/cluster_volumes.templ +++ b/weed/admin/view/app/cluster_volumes.templ @@ -312,7 +312,7 @@ templ ClusterVolumes(data dash.ClusterVolumesData) { for _, volume := range data.Volumes { <tr> <td> - <code>{fmt.Sprintf("%d", volume.ID)}</code> + <code>{fmt.Sprintf("%d", volume.Id)}</code> </td> <td> <a href={templ.SafeURL(fmt.Sprintf("http://%s", volume.Server))} target="_blank" class="text-decoration-none"> @@ -343,11 +343,11 @@ templ ClusterVolumes(data dash.ClusterVolumesData) { } </td> } - <td>{formatBytes(volume.Size)}</td> - <td>{fmt.Sprintf("%d", volume.FileCount)}</td> - <td> - <span class="badge bg-info">{volume.Replication}</span> - </td> + <td>{formatBytes(int64(volume.Size))}</td> + <td>{fmt.Sprintf("%d", volume.FileCount)}</td> + <td> + <span class="badge bg-info">{fmt.Sprintf("%03d", volume.ReplicaPlacement)}</span> + </td> if data.ShowDiskTypeColumn { <td> <span class="badge bg-primary">{volume.DiskType}</span> @@ -360,8 +360,8 @@ templ ClusterVolumes(data dash.ClusterVolumesData) { } <td> <div class="btn-group btn-group-sm"> - <button type="button" class="btn btn-outline-primary btn-sm" - title="View Details"> + <button type="button" class="btn btn-outline-primary btn-sm view-details-btn" + title="View Details" data-volume-id={fmt.Sprintf("%d", volume.Id)}> <i class="fas fa-eye"></i> </button> <button type="button" class="btn btn-outline-secondary btn-sm" @@ -480,6 +480,15 @@ templ ClusterVolumes(data dash.ClusterVolumesData) { goToPage(page); }); }); + + // Add click handlers to view details buttons + document.querySelectorAll('.view-details-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const volumeId = this.getAttribute('data-volume-id'); + viewVolumeDetails(volumeId); + }); + }); }); function goToPage(page) { @@ -516,15 +525,25 @@ templ ClusterVolumes(data dash.ClusterVolumesData) { // TODO: Implement volume export functionality alert('Export functionality to be implemented'); } + + function viewVolumeDetails(volumeId) { + // Get the server from the current row + const button = event.target.closest('button'); + const row = button.closest('tr'); + const serverCell = row.querySelector('td:nth-child(2) a'); + const server = serverCell ? serverCell.textContent.trim() : 'unknown'; + + window.location.href = `/cluster/volumes/${volumeId}/${encodeURIComponent(server)}`; + } </script> } -func countActiveVolumes(volumes []dash.VolumeInfo) int { +func countActiveVolumes(volumes []dash.VolumeWithTopology) int { // Since we removed status tracking, consider all volumes as active return len(volumes) } -func countUniqueDataCenters(volumes []dash.VolumeInfo) int { +func countUniqueDataCenters(volumes []dash.VolumeWithTopology) int { dcMap := make(map[string]bool) for _, volume := range volumes { dcMap[volume.DataCenter] = true @@ -532,7 +551,7 @@ func countUniqueDataCenters(volumes []dash.VolumeInfo) int { return len(dcMap) } -func countUniqueRacks(volumes []dash.VolumeInfo) int { +func countUniqueRacks(volumes []dash.VolumeWithTopology) int { rackMap := make(map[string]bool) for _, volume := range volumes { if volume.Rack != "" { @@ -542,7 +561,7 @@ func countUniqueRacks(volumes []dash.VolumeInfo) int { return len(rackMap) } -func countUniqueDiskTypes(volumes []dash.VolumeInfo) int { +func countUniqueDiskTypes(volumes []dash.VolumeWithTopology) int { diskTypeMap := make(map[string]bool) for _, volume := range volumes { diskType := volume.DiskType diff --git a/weed/admin/view/app/cluster_volumes_templ.go b/weed/admin/view/app/cluster_volumes_templ.go index 8d9f1cbe4..3133ab8bd 100644 --- a/weed/admin/view/app/cluster_volumes_templ.go +++ b/weed/admin/view/app/cluster_volumes_templ.go @@ -457,7 +457,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", volume.ID)) + templ_7745c5c3_Var15, 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: 315, Col: 79} } @@ -586,9 +586,9 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(volume.Size)) + templ_7745c5c3_Var23, 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: 346, Col: 69} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 346, Col: 100} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { @@ -601,7 +601,7 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component { var templ_7745c5c3_Var24 string templ_7745c5c3_Var24, 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: 347, Col: 80} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 347, Col: 64} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { @@ -612,9 +612,9 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Replication) + templ_7745c5c3_Var25, 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: 349, Col: 91} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 349, Col: 101} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { @@ -662,228 +662,241 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<td><div class=\"btn-group btn-group-sm\"><button type=\"button\" class=\"btn btn-outline-primary btn-sm\" title=\"View Details\"><i class=\"fas fa-eye\"></i></button> <button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" title=\"Compact\"><i class=\"fas fa-compress-alt\"></i></button> <button type=\"button\" class=\"btn btn-outline-warning btn-sm\" title=\"Fix\"><i class=\"fas fa-wrench\"></i></button></div></td></tr>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<td><div class=\"btn-group btn-group-sm\"><button type=\"button\" class=\"btn btn-outline-primary btn-sm view-details-btn\" title=\"View Details\" data-volume-id=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var28 string + templ_7745c5c3_Var28, 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: 364, Col: 121} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\"><i class=\"fas fa-eye\"></i></button> <button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" title=\"Compact\"><i class=\"fas fa-compress-alt\"></i></button> <button type=\"button\" class=\"btn btn-outline-warning btn-sm\" title=\"Fix\"><i class=\"fas fa-wrench\"></i></button></div></td></tr>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</tbody></table></div><!-- Volume Summary --> <div class=\"d-flex justify-content-between align-items-center mt-3\"><div><small class=\"text-muted\">Showing ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</tbody></table></div><!-- Volume Summary --> <div class=\"d-flex justify-content-between align-items-center mt-3\"><div><small class=\"text-muted\">Showing ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", (data.CurrentPage-1)*data.PageSize+1)) + var templ_7745c5c3_Var29 string + templ_7745c5c3_Var29, 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: 387, Col: 98} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, " to ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, " to ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", minInt(data.CurrentPage*data.PageSize, data.TotalVolumes))) + var templ_7745c5c3_Var30 string + templ_7745c5c3_Var30, 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: 387, Col: 180} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, " of ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, " of ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalVolumes)) + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, 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: 387, Col: 222} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, " volumes</small></div>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " volumes</small></div>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.TotalPages > 1 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<div><small class=\"text-muted\">Page ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "<div><small class=\"text-muted\">Page ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage)) + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, 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: 393, Col: 77} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, " of ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, " of ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalPages)) + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, 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: 393, Col: 117} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</small></div>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</small></div>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</div><!-- Pagination Controls --> ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</div><!-- Pagination Controls --> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.TotalPages > 1 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<div class=\"d-flex justify-content-center mt-3\"><nav aria-label=\"Volumes pagination\"><ul class=\"pagination pagination-sm mb-0\"><!-- Previous Button -->") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<div class=\"d-flex justify-content-center mt-3\"><nav aria-label=\"Volumes pagination\"><ul class=\"pagination pagination-sm mb-0\"><!-- Previous Button -->") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.CurrentPage > 1 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var33 string - templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage-1)) + var templ_7745c5c3_Var34 string + templ_7745c5c3_Var34, 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: 407, Col: 138} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "\"><i class=\"fas fa-chevron-left\"></i></a></li>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\"><i class=\"fas fa-chevron-left\"></i></a></li>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<li class=\"page-item disabled\"><span class=\"page-link\"><i class=\"fas fa-chevron-left\"></i></span></li>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<li class=\"page-item disabled\"><span class=\"page-link\"><i class=\"fas fa-chevron-left\"></i></span></li>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<!-- Page Numbers -->") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<!-- Page Numbers -->") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for i := maxInt(1, data.CurrentPage-2); i <= minInt(data.TotalPages, data.CurrentPage+2); i++ { if i == data.CurrentPage { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<li class=\"page-item active\"><span class=\"page-link\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "<li class=\"page-item active\"><span class=\"page-link\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var34 string - templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i)) + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, 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: 423, Col: 93} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</span></li>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "</span></li>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var35 string - templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i)) + var templ_7745c5c3_Var36 string + templ_7745c5c3_Var36, 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: 427, Col: 125} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var36 string - templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i)) + var templ_7745c5c3_Var37 string + templ_7745c5c3_Var37, 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: 427, Col: 148} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</a></li>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "</a></li>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<!-- Next Button -->") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<!-- Next Button -->") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.CurrentPage < data.TotalPages { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var37 string - templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage+1)) + var templ_7745c5c3_Var38 string + templ_7745c5c3_Var38, 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: 435, Col: 138} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\"><i class=\"fas fa-chevron-right\"></i></a></li>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\"><i class=\"fas fa-chevron-right\"></i></a></li>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "<li class=\"page-item disabled\"><span class=\"page-link\"><i class=\"fas fa-chevron-right\"></i></span></li>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "<li class=\"page-item disabled\"><span class=\"page-link\"><i class=\"fas fa-chevron-right\"></i></span></li>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "</ul></nav></div>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "</ul></nav></div>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "<div class=\"text-center py-5\"><i class=\"fas fa-database fa-3x text-muted mb-3\"></i><h5 class=\"text-muted\">No Volumes Found</h5><p class=\"text-muted\">No volumes are currently available in the cluster.</p></div>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "<div class=\"text-center py-5\"><i class=\"fas fa-database fa-3x text-muted mb-3\"></i><h5 class=\"text-muted\">No Volumes Found</h5><p class=\"text-muted\">No volumes are currently available in the cluster.</p></div>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "</div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "</div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var38 string - templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) + var templ_7745c5c3_Var39 string + templ_7745c5c3_Var39, 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: 465, Col: 81} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "</small></div></div></div><!-- JavaScript for pagination and sorting --><script>\n // Initialize pagination links when page loads\n document.addEventListener('DOMContentLoaded', function() {\n // Add click handlers to pagination links\n document.querySelectorAll('.pagination-link').forEach(link => {\n link.addEventListener('click', function(e) {\n e.preventDefault();\n const page = this.getAttribute('data-page');\n goToPage(page);\n });\n });\n });\n \n function goToPage(page) {\n const url = new URL(window.location);\n url.searchParams.set('page', page);\n window.location.href = url.toString();\n }\n \n function changePageSize() {\n const pageSize = document.getElementById('pageSizeSelect').value;\n const url = new URL(window.location);\n url.searchParams.set('pageSize', pageSize);\n url.searchParams.set('page', '1'); // Reset to first page\n window.location.href = url.toString();\n }\n \n function sortTable(column) {\n const url = new URL(window.location);\n const currentSort = url.searchParams.get('sortBy');\n const currentOrder = url.searchParams.get('sortOrder') || 'asc';\n \n let newOrder = 'asc';\n if (currentSort === column && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n url.searchParams.set('sortBy', column);\n url.searchParams.set('sortOrder', newOrder);\n url.searchParams.set('page', '1'); // Reset to first page\n window.location.href = url.toString();\n }\n \n function exportVolumes() {\n // TODO: Implement volume export functionality\n alert('Export functionality to be implemented');\n }\n </script>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "</small></div></div></div><!-- JavaScript for pagination and sorting --><script>\n // Initialize pagination links when page loads\n document.addEventListener('DOMContentLoaded', function() {\n // Add click handlers to pagination links\n document.querySelectorAll('.pagination-link').forEach(link => {\n link.addEventListener('click', function(e) {\n e.preventDefault();\n const page = this.getAttribute('data-page');\n goToPage(page);\n });\n });\n \n // Add click handlers to view details buttons\n document.querySelectorAll('.view-details-btn').forEach(button => {\n button.addEventListener('click', function(e) {\n e.preventDefault();\n const volumeId = this.getAttribute('data-volume-id');\n viewVolumeDetails(volumeId);\n });\n });\n });\n \n function goToPage(page) {\n const url = new URL(window.location);\n url.searchParams.set('page', page);\n window.location.href = url.toString();\n }\n \n function changePageSize() {\n const pageSize = document.getElementById('pageSizeSelect').value;\n const url = new URL(window.location);\n url.searchParams.set('pageSize', pageSize);\n url.searchParams.set('page', '1'); // Reset to first page\n window.location.href = url.toString();\n }\n \n function sortTable(column) {\n const url = new URL(window.location);\n const currentSort = url.searchParams.get('sortBy');\n const currentOrder = url.searchParams.get('sortOrder') || 'asc';\n \n let newOrder = 'asc';\n if (currentSort === column && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n url.searchParams.set('sortBy', column);\n url.searchParams.set('sortOrder', newOrder);\n url.searchParams.set('page', '1'); // Reset to first page\n window.location.href = url.toString();\n }\n \n function exportVolumes() {\n // TODO: Implement volume export functionality\n alert('Export functionality to be implemented');\n }\n \n function viewVolumeDetails(volumeId) {\n // Get the server from the current row\n const button = event.target.closest('button');\n const row = button.closest('tr');\n const serverCell = row.querySelector('td:nth-child(2) a');\n const server = serverCell ? serverCell.textContent.trim() : 'unknown';\n \n window.location.href = `/cluster/volumes/${volumeId}/${encodeURIComponent(server)}`;\n }\n </script>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -891,12 +904,12 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component { }) } -func countActiveVolumes(volumes []dash.VolumeInfo) int { +func countActiveVolumes(volumes []dash.VolumeWithTopology) int { // Since we removed status tracking, consider all volumes as active return len(volumes) } -func countUniqueDataCenters(volumes []dash.VolumeInfo) int { +func countUniqueDataCenters(volumes []dash.VolumeWithTopology) int { dcMap := make(map[string]bool) for _, volume := range volumes { dcMap[volume.DataCenter] = true @@ -904,7 +917,7 @@ func countUniqueDataCenters(volumes []dash.VolumeInfo) int { return len(dcMap) } -func countUniqueRacks(volumes []dash.VolumeInfo) int { +func countUniqueRacks(volumes []dash.VolumeWithTopology) int { rackMap := make(map[string]bool) for _, volume := range volumes { if volume.Rack != "" { @@ -914,7 +927,7 @@ func countUniqueRacks(volumes []dash.VolumeInfo) int { return len(rackMap) } -func countUniqueDiskTypes(volumes []dash.VolumeInfo) int { +func countUniqueDiskTypes(volumes []dash.VolumeWithTopology) int { diskTypeMap := make(map[string]bool) for _, volume := range volumes { diskType := volume.DiskType @@ -942,23 +955,23 @@ func getSortIcon(column, currentSort, currentOrder string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var39 := templ.GetChildren(ctx) - if templ_7745c5c3_Var39 == nil { - templ_7745c5c3_Var39 = templ.NopComponent + templ_7745c5c3_Var40 := templ.GetChildren(ctx) + if templ_7745c5c3_Var40 == nil { + templ_7745c5c3_Var40 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if column != currentSort { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "<i class=\"fas fa-sort text-muted ms-1\"></i>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<i class=\"fas fa-sort text-muted ms-1\"></i>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if currentOrder == "asc" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<i class=\"fas fa-sort-up text-primary ms-1\"></i>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<i class=\"fas fa-sort-up text-primary ms-1\"></i>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "<i class=\"fas fa-sort-down text-primary ms-1\"></i>") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "<i class=\"fas fa-sort-down text-primary ms-1\"></i>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/volume_details.templ b/weed/admin/view/app/volume_details.templ new file mode 100644 index 000000000..01876b46f --- /dev/null +++ b/weed/admin/view/app/volume_details.templ @@ -0,0 +1,425 @@ +package app + +import ( + "fmt" + "time" + "github.com/seaweedfs/seaweedfs/weed/admin/dash" +) + +templ VolumeDetails(data dash.VolumeDetailsData) { + <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> + <div> + <h1 class="h2"> + <i class="fas fa-database me-2"></i>Volume Details + </h1> + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a href="/admin" class="text-decoration-none">Dashboard</a></li> + <li class="breadcrumb-item"><a href="/cluster/volumes" class="text-decoration-none">Volumes</a></li> + <li class="breadcrumb-item active" aria-current="page">Volume {fmt.Sprintf("%d", data.Volume.Id)}</li> + </ol> + </nav> + </div> + <div class="btn-toolbar mb-2 mb-md-0"> + <div class="btn-group me-2"> + <button type="button" class="btn btn-sm btn-outline-secondary" onclick="history.back()"> + <i class="fas fa-arrow-left me-1"></i>Back + </button> + <button type="button" class="btn btn-sm btn-outline-primary" onclick="window.location.reload()"> + <i class="fas fa-refresh me-1"></i>Refresh + </button> + </div> + </div> + </div> + + <div class="row"> + <!-- Volume Information Card --> + <div class="col-lg-8"> + <div class="card shadow mb-4"> + <div class="card-header py-3"> + <h6 class="m-0 font-weight-bold text-primary"> + <i class="fas fa-info-circle me-2"></i>Volume Information + </h6> + </div> + <div class="card-body"> + <div class="row"> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label"><strong>Volume ID:</strong></label> + <div><code class="fs-5">{fmt.Sprintf("%d", data.Volume.Id)}</code></div> + </div> + <div class="mb-3"> + <label class="form-label"><strong>Server:</strong></label> + <div> + <a href={templ.SafeURL(fmt.Sprintf("http://%s", data.Volume.Server))} target="_blank" class="text-decoration-none"> + {data.Volume.Server} + <i class="fas fa-external-link-alt ms-1 text-muted"></i> + </a> + </div> + </div> + <div class="mb-3"> + <label class="form-label"><strong>Data Center:</strong></label> + <div><span class="badge bg-light text-dark">{data.Volume.DataCenter}</span></div> + </div> + <div class="mb-3"> + <label class="form-label"><strong>Rack:</strong></label> + <div><span class="badge bg-light text-dark">{data.Volume.Rack}</span></div> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label"><strong>Collection:</strong></label> + <div> + if data.Volume.Collection == "" { + <a href={templ.SafeURL("/cluster/volumes?collection=default")} class="text-decoration-none"> + <span class="badge bg-secondary">default</span> + </a> + } else { + <a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes?collection=%s", data.Volume.Collection))} class="text-decoration-none"> + <span class="badge bg-secondary">{data.Volume.Collection}</span> + </a> + } + </div> + </div> + <div class="mb-3"> + <label class="form-label"><strong>Replication:</strong></label> + <div><span class="badge bg-info">{fmt.Sprintf("%03d", data.Volume.ReplicaPlacement)}</span></div> + </div> + <div class="mb-3"> + <label class="form-label"><strong>Disk Type:</strong></label> + <div> + <span class="badge bg-primary"> + if data.Volume.DiskType == "" { + hdd + } else { + {data.Volume.DiskType} + } + </span> + </div> + </div> + <div class="mb-3"> + <label class="form-label"><strong>Version:</strong></label> + <div><span class="badge bg-dark">{fmt.Sprintf("v%d", data.Volume.Version)}</span></div> + </div> + </div> + </div> + </div> + </div> + </div> + + <!-- Statistics Card --> + <div class="col-lg-4"> + <!-- Volume Statistics & Health Card --> + <div class="card shadow mb-4"> + <div class="card-header py-3"> + <h6 class="m-0 font-weight-bold text-primary"> + <i class="fas fa-chart-pie me-2"></i>Volume Statistics & Health + </h6> + </div> + <div class="card-body"> + <!-- Storage Metrics --> + <div class="row mb-3"> + <div class="col-6"> + <div class="text-center"> + <div class="h4 mb-0 font-weight-bold text-success"> + {formatBytes(int64(data.Volume.Size - data.Volume.DeletedByteCount))} + </div> + <small class="text-muted">Active Bytes</small> + </div> + </div> + <div class="col-6"> + <div class="text-center"> + <div class="h4 mb-0 font-weight-bold text-danger"> + {formatBytes(int64(data.Volume.DeletedByteCount))} + </div> + <small class="text-muted">Deleted Bytes</small> + </div> + </div> + </div> + + + + <!-- File Metrics --> + <div class="row mb-3"> + <div class="col-6"> + <div class="text-center"> + <div class="h4 mb-0 font-weight-bold text-success"> + {fmt.Sprintf("%d", data.Volume.FileCount)} + </div> + <small class="text-muted">Active Files</small> + </div> + </div> + <div class="col-6"> + <div class="text-center"> + <div class="h4 mb-0 font-weight-bold text-danger"> + {fmt.Sprintf("%d", data.Volume.DeleteCount)} + </div> + <small class="text-muted">Deleted Files</small> + </div> + </div> + </div> + + + + <!-- Storage Efficiency --> + if data.Volume.FileCount > 0 && data.Volume.Size > 0 { + <div class="mb-3"> + <div class="d-flex justify-content-between align-items-center mb-1"> + <small class="text-muted">Storage Efficiency</small> + <small class="text-muted"> + {fmt.Sprintf("%.1f%%", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)} + </small> + </div> + <div class="progress" style="height: 8px;"> + <div class="progress-bar bg-info" role="progressbar" + style={fmt.Sprintf("width: %.1f%%", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)} + aria-valuenow={fmt.Sprintf("%.1f", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)} + aria-valuemin="0" aria-valuemax="100"> + </div> + </div> + </div> + } + + <hr class="my-3"> + + <!-- Status & Configuration --> + <div class="row mb-3"> + <div class="col-12"> + <div class="text-center mb-2"> + if data.Volume.ReadOnly { + <span class="badge bg-warning fs-6 px-3 py-2"> + <i class="fas fa-lock me-1"></i>Read Only + </span> + if data.Volume.Size >= data.VolumeSizeLimit { + <div class="mt-1"> + <small class="text-muted">Size limit exceeded</small> + </div> + } + } else if data.VolumeSizeLimit > data.Volume.Size { + <span class="badge bg-success fs-6 px-3 py-2"> + <i class="fas fa-edit me-1"></i>Read/Write + </span> + } else { + <span class="badge bg-warning fs-6 px-3 py-2"> + <i class="fas fa-exclamation-triangle me-1"></i>Size Limit Reached + </span> + } + </div> + </div> + </div> + + <!-- Maintenance Info --> + <div class="row mb-3"> + <div class="col-6"> + <div class="text-center"> + <div class="h6 mb-0 font-weight-bold text-info"> + #{fmt.Sprintf("%d", data.Volume.CompactRevision)} + </div> + <small class="text-muted">Compact Revision</small> + </div> + </div> + <div class="col-6"> + <div class="text-center"> + <div class="h6 mb-0 font-weight-bold text-secondary"> + if data.Volume.ModifiedAtSecond > 0 { + {formatTimestamp(data.Volume.ModifiedAtSecond)} + } else { + <span class="text-muted">Never modified</span> + } + </div> + <small class="text-muted">Last Modified</small> + </div> + </div> + </div> + + <!-- TTL Configuration --> + if data.Volume.Ttl > 0 { + <div class="mb-3 text-center"> + <span class="badge bg-info fs-6 px-3 py-2"> + <i class="fas fa-clock me-1"></i>{formatTTL(data.Volume.Ttl)} + </span> + <div class="mt-1"> + <small class="text-muted">Time To Live</small> + </div> + </div> + } + + <!-- Remote Storage Configuration --> + if data.Volume.RemoteStorageName != "" { + <hr class="my-3"> + <div class="mb-2"> + <div class="text-center"> + <div class="h6 mb-1 font-weight-bold text-info"> + <i class="fas fa-cloud me-1"></i>{data.Volume.RemoteStorageName} + </div> + <small class="text-muted">Remote Storage</small> + </div> + </div> + if data.Volume.RemoteStorageKey != "" { + <div class="text-center"> + <div class="text-xs font-monospace bg-light p-2 rounded text-truncate" title={data.Volume.RemoteStorageKey}> + {data.Volume.RemoteStorageKey} + </div> + <small class="text-muted">Storage Key</small> + </div> + } + } + </div> + </div> + </div> + </div> + + <!-- Replicas Card --> + if len(data.Replicas) > 0 { + <div class="row"> + <div class="col-12"> + <div class="card shadow mb-4"> + <div class="card-header py-3"> + <h6 class="m-0 font-weight-bold text-primary"> + <i class="fas fa-copy me-2"></i>Replicas ({fmt.Sprintf("%d", data.ReplicationCount)}) + </h6> + </div> + <div class="card-body"> + <div class="table-responsive"> + <table class="table table-hover"> + <thead> + <tr> + <th>Server</th> + <th>Data Center</th> + <th>Rack</th> + <th>Size</th> + <th>File Count</th> + <th>Status</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + <!-- Primary Volume (current one) --> + <tr class="table-primary"> + <td> + <strong> + <a href={templ.SafeURL(fmt.Sprintf("http://%s", data.Volume.Server))} target="_blank" class="text-decoration-none"> + {data.Volume.Server} + <i class="fas fa-external-link-alt ms-1 text-muted"></i> + </a> + </strong> + <span class="badge bg-success ms-2">Primary</span> + </td> + <td><span class="badge bg-light text-dark">{data.Volume.DataCenter}</span></td> + <td><span class="badge bg-light text-dark">{data.Volume.Rack}</span></td> + <td>{formatBytes(int64(data.Volume.Size))}</td> + <td>{fmt.Sprintf("%d", data.Volume.FileCount)}</td> + <td><span class="badge bg-success">Active</span></td> + <td> + <span class="text-muted">Current Volume</span> + </td> + </tr> + <!-- Replica Volumes --> + for _, replica := range data.Replicas { + <tr> + <td> + <a href={templ.SafeURL(fmt.Sprintf("http://%s", replica.Server))} target="_blank" class="text-decoration-none"> + {replica.Server} + <i class="fas fa-external-link-alt ms-1 text-muted"></i> + </a> + </td> + <td><span class="badge bg-light text-dark">{replica.DataCenter}</span></td> + <td><span class="badge bg-light text-dark">{replica.Rack}</span></td> + <td>{formatBytes(int64(replica.Size))}</td> + <td>{fmt.Sprintf("%d", replica.FileCount)}</td> + <td><span class="badge bg-info">Replica</span></td> + <td> + <a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes/%d/%s", replica.Id, replica.Server))} class="btn btn-sm btn-outline-primary"> + <i class="fas fa-eye me-1"></i>View + </a> + </td> + </tr> + } + </tbody> + </table> + </div> + </div> + </div> + </div> + </div> + } + + <!-- Actions Card --> + <div class="row"> + <div class="col-12"> + <div class="card shadow mb-4"> + <div class="card-header py-3"> + <h6 class="m-0 font-weight-bold text-primary"> + <i class="fas fa-tools me-2"></i>Actions + </h6> + </div> + <div class="card-body"> + <div class="btn-group" role="group"> + <button type="button" class="btn btn-outline-secondary" title="Compact Volume"> + <i class="fas fa-compress-alt me-1"></i>Compact + </button> + <button type="button" class="btn btn-outline-warning" title="Fix Volume"> + <i class="fas fa-wrench me-1"></i>Fix + </button> + <button type="button" class="btn btn-outline-danger" title="Vacuum Volume"> + <i class="fas fa-trash me-1"></i>Vacuum + </button> + </div> + <div class="mt-3"> + <small class="text-muted"> + <i class="fas fa-info-circle me-1"></i> + Use these actions to perform maintenance operations on the volume. + </small> + </div> + </div> + </div> + </div> + </div> + + <!-- Last Updated --> + <div class="row"> + <div class="col-12"> + <small class="text-muted"> + <i class="fas fa-clock me-1"></i> + Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")} + </small> + </div> + </div> +} + +func formatTimestamp(unixTimestamp int64) string { + if unixTimestamp <= 0 { + return "Never" + } + t := time.Unix(unixTimestamp, 0) + return t.Format("2006-01-02 15:04:05") +} + +func formatTTL(ttlSeconds uint32) string { + if ttlSeconds == 0 { + return "No TTL" + } + + duration := time.Duration(ttlSeconds) * time.Second + + // Convert to human readable format + days := int(duration.Hours()) / 24 + hours := int(duration.Hours()) % 24 + minutes := int(duration.Minutes()) % 60 + + if days > 0 { + if hours > 0 { + return fmt.Sprintf("%dd %dh", days, hours) + } + return fmt.Sprintf("%d days", days) + } else if hours > 0 { + if minutes > 0 { + return fmt.Sprintf("%dh %dm", hours, minutes) + } + return fmt.Sprintf("%d hours", hours) + } else if minutes > 0 { + return fmt.Sprintf("%d minutes", minutes) + } else { + return fmt.Sprintf("%d seconds", int(duration.Seconds())) + } +}
\ No newline at end of file diff --git a/weed/admin/view/app/volume_details_templ.go b/weed/admin/view/app/volume_details_templ.go new file mode 100644 index 000000000..6217fa695 --- /dev/null +++ b/weed/admin/view/app/volume_details_templ.go @@ -0,0 +1,690 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.833 +package app + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "time" +) + +func VolumeDetails(data dash.VolumeDetailsData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom\"><div><h1 class=\"h2\"><i class=\"fas fa-database me-2\"></i>Volume Details</h1><nav aria-label=\"breadcrumb\"><ol class=\"breadcrumb\"><li class=\"breadcrumb-item\"><a href=\"/admin\" class=\"text-decoration-none\">Dashboard</a></li><li class=\"breadcrumb-item\"><a href=\"/cluster/volumes\" class=\"text-decoration-none\">Volumes</a></li><li class=\"breadcrumb-item active\" aria-current=\"page\">Volume ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.Volume.Id)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 19, Col: 116} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</li></ol></nav></div><div class=\"btn-toolbar mb-2 mb-md-0\"><div class=\"btn-group me-2\"><button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" onclick=\"history.back()\"><i class=\"fas fa-arrow-left me-1\"></i>Back</button> <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" onclick=\"window.location.reload()\"><i class=\"fas fa-refresh me-1\"></i>Refresh</button></div></div></div><div class=\"row\"><!-- Volume Information Card --><div class=\"col-lg-8\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-info-circle me-2\"></i>Volume Information</h6></div><div class=\"card-body\"><div class=\"row\"><div class=\"col-md-6\"><div class=\"mb-3\"><label class=\"form-label\"><strong>Volume ID:</strong></label><div><code class=\"fs-5\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.Volume.Id)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 49, Col: 90} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</code></div></div><div class=\"mb-3\"><label class=\"form-label\"><strong>Server:</strong></label><div><a href=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(fmt.Sprintf("http://%s", data.Volume.Server)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4))) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" target=\"_blank\" class=\"text-decoration-none\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.Server) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 55, Col: 59} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " <i class=\"fas fa-external-link-alt ms-1 text-muted\"></i></a></div></div><div class=\"mb-3\"><label class=\"form-label\"><strong>Data Center:</strong></label><div><span class=\"badge bg-light text-dark\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.DataCenter) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 62, Col: 99} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span></div></div><div class=\"mb-3\"><label class=\"form-label\"><strong>Rack:</strong></label><div><span class=\"badge bg-light text-dark\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.Rack) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 66, Col: 93} + } + _, 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, 7, "</span></div></div></div><div class=\"col-md-6\"><div class=\"mb-3\"><label class=\"form-label\"><strong>Collection:</strong></label><div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Volume.Collection == "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 templ.SafeURL = templ.SafeURL("/cluster/volumes?collection=default") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var8))) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" class=\"text-decoration-none\"><span class=\"badge bg-secondary\">default</span></a>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<a href=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/cluster/volumes?collection=%s", data.Volume.Collection)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var9))) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" class=\"text-decoration-none\"><span class=\"badge bg-secondary\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.Collection) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 79, Col: 100} + } + _, 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, "</span></a>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></div><div class=\"mb-3\"><label class=\"form-label\"><strong>Replication:</strong></label><div><span class=\"badge bg-info\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%03d", data.Volume.ReplicaPlacement)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 86, Col: 115} + } + _, 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, 14, "</span></div></div><div class=\"mb-3\"><label class=\"form-label\"><strong>Disk Type:</strong></label><div><span class=\"badge bg-primary\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Volume.DiskType == "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "hdd") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.DiskType) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 95, Col: 65} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></div></div><div class=\"mb-3\"><label class=\"form-label\"><strong>Version:</strong></label><div><span class=\"badge bg-dark\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("v%d", data.Volume.Version)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 102, Col: 105} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span></div></div></div></div></div></div></div><!-- Statistics Card --><div class=\"col-lg-4\"><!-- Volume Statistics & Health Card --><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-chart-pie me-2\"></i>Volume Statistics & Health</h6></div><div class=\"card-body\"><!-- Storage Metrics --><div class=\"row mb-3\"><div class=\"col-6\"><div class=\"text-center\"><div class=\"h4 mb-0 font-weight-bold text-success\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(int64(data.Volume.Size - data.Volume.DeletedByteCount))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 125, Col: 104} + } + _, 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, 18, "</div><small class=\"text-muted\">Active Bytes</small></div></div><div class=\"col-6\"><div class=\"text-center\"><div class=\"h4 mb-0 font-weight-bold text-danger\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(int64(data.Volume.DeletedByteCount))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 133, Col: 85} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div><small class=\"text-muted\">Deleted Bytes</small></div></div></div><!-- File Metrics --><div class=\"row mb-3\"><div class=\"col-6\"><div class=\"text-center\"><div class=\"h4 mb-0 font-weight-bold text-success\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.Volume.FileCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 147, Col: 77} + } + _, 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, 20, "</div><small class=\"text-muted\">Active Files</small></div></div><div class=\"col-6\"><div class=\"text-center\"><div class=\"h4 mb-0 font-weight-bold text-danger\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.Volume.DeleteCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 155, Col: 79} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><small class=\"text-muted\">Deleted Files</small></div></div></div><!-- Storage Efficiency -->") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Volume.FileCount > 0 && data.Volume.Size > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"mb-3\"><div class=\"d-flex justify-content-between align-items-center mb-1\"><small class=\"text-muted\">Storage Efficiency</small> <small class=\"text-muted\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f%%", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 170, Col: 144} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</small></div><div class=\"progress\" style=\"height: 8px;\"><div class=\"progress-bar bg-info\" role=\"progressbar\" style=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %.1f%%", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 175, Col: 158} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" aria-valuenow=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 176, Col: 157} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" aria-valuemin=\"0\" aria-valuemax=\"100\"></div></div></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<hr class=\"my-3\"><!-- Status & Configuration --><div class=\"row mb-3\"><div class=\"col-12\"><div class=\"text-center mb-2\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Volume.ReadOnly { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<span class=\"badge bg-warning fs-6 px-3 py-2\"><i class=\"fas fa-lock me-1\"></i>Read Only</span> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Volume.Size >= data.VolumeSizeLimit { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"mt-1\"><small class=\"text-muted\">Size limit exceeded</small></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else if data.VolumeSizeLimit > data.Volume.Size { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<span class=\"badge bg-success fs-6 px-3 py-2\"><i class=\"fas fa-edit me-1\"></i>Read/Write</span>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<span class=\"badge bg-warning fs-6 px-3 py-2\"><i class=\"fas fa-exclamation-triangle me-1\"></i>Size Limit Reached</span>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div></div></div><!-- Maintenance Info --><div class=\"row mb-3\"><div class=\"col-6\"><div class=\"text-center\"><div class=\"h6 mb-0 font-weight-bold text-info\">#") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.Volume.CompactRevision)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 216, Col: 84} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div><small class=\"text-muted\">Compact Revision</small></div></div><div class=\"col-6\"><div class=\"text-center\"><div class=\"h6 mb-0 font-weight-bold text-secondary\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Volume.ModifiedAtSecond > 0 { + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(formatTimestamp(data.Volume.ModifiedAtSecond)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 225, Col: 86} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<span class=\"text-muted\">Never modified</span>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</div><small class=\"text-muted\">Last Modified</small></div></div></div><!-- TTL Configuration -->") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Volume.Ttl > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<div class=\"mb-3 text-center\"><span class=\"badge bg-info fs-6 px-3 py-2\"><i class=\"fas fa-clock me-1\"></i>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(formatTTL(data.Volume.Ttl)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 239, Col: 92} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</span><div class=\"mt-1\"><small class=\"text-muted\">Time To Live</small></div></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<!-- Remote Storage Configuration -->") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Volume.RemoteStorageName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<hr class=\"my-3\"><div class=\"mb-2\"><div class=\"text-center\"><div class=\"h6 mb-1 font-weight-bold text-info\"><i class=\"fas fa-cloud me-1\"></i>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.RemoteStorageName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 253, Col: 99} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</div><small class=\"text-muted\">Remote Storage</small></div></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Volume.RemoteStorageKey != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<div class=\"text-center\"><div class=\"text-xs font-monospace bg-light p-2 rounded text-truncate\" title=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.RemoteStorageKey) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 260, Col: 138} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.RemoteStorageKey) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 261, Col: 65} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div><small class=\"text-muted\">Storage Key</small></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div></div></div></div><!-- Replicas Card -->") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(data.Replicas) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-copy me-2\"></i>Replicas (") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.ReplicationCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 279, Col: 111} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ")</h6></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\"><thead><tr><th>Server</th><th>Data Center</th><th>Rack</th><th>Size</th><th>File Count</th><th>Status</th><th>Actions</th></tr></thead> <tbody><!-- Primary Volume (current one) --><tr class=\"table-primary\"><td><strong><a href=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var28 templ.SafeURL = templ.SafeURL(fmt.Sprintf("http://%s", data.Volume.Server)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var28))) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" target=\"_blank\" class=\"text-decoration-none\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var29 string + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.Server) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 302, Col: 71} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " <i class=\"fas fa-external-link-alt ms-1 text-muted\"></i></a></strong> <span class=\"badge bg-success ms-2\">Primary</span></td><td><span class=\"badge bg-light text-dark\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var30 string + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.DataCenter) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 308, Col: 106} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span></td><td><span class=\"badge bg-light text-dark\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.Rack) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 309, Col: 100} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</span></td><td>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(int64(data.Volume.Size))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 310, Col: 81} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</td><td>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.Volume.FileCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 311, Col: 85} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</td><td><span class=\"badge bg-success\">Active</span></td><td><span class=\"text-muted\">Current Volume</span></td></tr><!-- Replica Volumes -->") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, replica := range data.Replicas { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<tr><td><a href=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var34 templ.SafeURL = templ.SafeURL(fmt.Sprintf("http://%s", replica.Server)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var34))) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" target=\"_blank\" class=\"text-decoration-none\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(replica.Server) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 322, Col: 67} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " <i class=\"fas fa-external-link-alt ms-1 text-muted\"></i></a></td><td><span class=\"badge bg-light text-dark\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var36 string + templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(replica.DataCenter) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 326, Col: 106} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span></td><td><span class=\"badge bg-light text-dark\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var37 string + templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(replica.Rack) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 327, Col: 100} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</span></td><td>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var38 string + templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(int64(replica.Size))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 328, Col: 81} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</td><td>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var39 string + templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", replica.FileCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 329, Col: 85} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</td><td><span class=\"badge bg-info\">Replica</span></td><td><a href=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var40 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/cluster/volumes/%d/%s", replica.Id, replica.Server)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var40))) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\" class=\"btn btn-sm btn-outline-primary\"><i class=\"fas fa-eye me-1\"></i>View</a></td></tr>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</tbody></table></div></div></div></div></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<!-- Actions Card --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-tools me-2\"></i>Actions</h6></div><div class=\"card-body\"><div class=\"btn-group\" role=\"group\"><button type=\"button\" class=\"btn btn-outline-secondary\" title=\"Compact Volume\"><i class=\"fas fa-compress-alt me-1\"></i>Compact</button> <button type=\"button\" class=\"btn btn-outline-warning\" title=\"Fix Volume\"><i class=\"fas fa-wrench me-1\"></i>Fix</button> <button type=\"button\" class=\"btn btn-outline-danger\" title=\"Vacuum Volume\"><i class=\"fas fa-trash me-1\"></i>Vacuum</button></div><div class=\"mt-3\"><small class=\"text-muted\"><i class=\"fas fa-info-circle me-1\"></i> Use these actions to perform maintenance operations on the volume.</small></div></div></div></div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var41 string + templ_7745c5c3_Var41, 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/volume_details.templ`, Line: 384, Col: 77} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</small></div></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func formatTimestamp(unixTimestamp int64) string { + if unixTimestamp <= 0 { + return "Never" + } + t := time.Unix(unixTimestamp, 0) + return t.Format("2006-01-02 15:04:05") +} + +func formatTTL(ttlSeconds uint32) string { + if ttlSeconds == 0 { + return "No TTL" + } + + duration := time.Duration(ttlSeconds) * time.Second + + // Convert to human readable format + days := int(duration.Hours()) / 24 + hours := int(duration.Hours()) % 24 + minutes := int(duration.Minutes()) % 60 + + if days > 0 { + if hours > 0 { + return fmt.Sprintf("%dd %dh", days, hours) + } + return fmt.Sprintf("%d days", days) + } else if hours > 0 { + if minutes > 0 { + return fmt.Sprintf("%dh %dm", hours, minutes) + } + return fmt.Sprintf("%d hours", hours) + } else if minutes > 0 { + return fmt.Sprintf("%d minutes", minutes) + } else { + return fmt.Sprintf("%d seconds", int(duration.Seconds())) + } +} + +var _ = templruntime.GeneratedTemplate |
