You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							726 lines
						
					
					
						
							36 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							726 lines
						
					
					
						
							36 KiB
						
					
					
				
								package app
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
								    "fmt"
							 | 
						|
								    "strings"
							 | 
						|
								    "github.com/seaweedfs/seaweedfs/weed/admin/dash"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								templ ClusterVolumes(data dash.ClusterVolumesData) {
							 | 
						|
								    <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>Cluster Volumes
							 | 
						|
								            </h1>
							 | 
						|
								            if data.FilterCollection != "" {
							 | 
						|
								                <div class="d-flex align-items-center mt-2">
							 | 
						|
								                    <span class="badge bg-info me-2">
							 | 
						|
								                        <i class="fas fa-filter me-1"></i>Collection: {data.FilterCollection}
							 | 
						|
								                    </span>
							 | 
						|
								                    <a href="/cluster/volumes" class="btn btn-sm btn-outline-secondary">
							 | 
						|
								                        <i class="fas fa-times me-1"></i>Clear Filter
							 | 
						|
								                    </a>
							 | 
						|
								                </div>
							 | 
						|
								            }
							 | 
						|
								        </div>
							 | 
						|
								        <div class="btn-toolbar mb-2 mb-md-0">
							 | 
						|
								            <div class="btn-group me-2">
							 | 
						|
								                <select class="form-select form-select-sm me-2" id="pageSizeSelect" onchange="changePageSize()" style="width: auto;">
							 | 
						|
								                    <option value="50" if data.PageSize == 50 { selected="selected" }>50 per page</option>
							 | 
						|
								                    <option value="100" if data.PageSize == 100 { selected="selected" }>100 per page</option>
							 | 
						|
								                    <option value="200" if data.PageSize == 200 { selected="selected" }>200 per page</option>
							 | 
						|
								                    <option value="500" if data.PageSize == 500 { selected="selected" }>500 per page</option>
							 | 
						|
								                </select>
							 | 
						|
								                <button type="button" class="btn btn-sm btn-outline-primary" onclick="exportVolumes()">
							 | 
						|
								                    <i class="fas fa-download me-1"></i>Export
							 | 
						|
								                </button>
							 | 
						|
								            </div>
							 | 
						|
								        </div>
							 | 
						|
								    </div>
							 | 
						|
								
							 | 
						|
								    <div id="volumes-content">
							 | 
						|
								        <!-- Summary Cards -->
							 | 
						|
								        <div class="row mb-4">
							 | 
						|
								            <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
							 | 
						|
								                <div class="card border-left-primary shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
							 | 
						|
								                                    Total Volumes
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">
							 | 
						|
								                                    {fmt.Sprintf("%d", data.TotalVolumes)}
							 | 
						|
								                                </div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-database fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								
							 | 
						|
								            <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
							 | 
						|
								                <div class="card border-left-success shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-success text-uppercase mb-1">
							 | 
						|
								                                    if data.CollectionCount == 1 {
							 | 
						|
								                                        Collection
							 | 
						|
								                                    } else {
							 | 
						|
								                                        Collections
							 | 
						|
								                                    }
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">
							 | 
						|
								                                    if data.CollectionCount == 1 {
							 | 
						|
								                                        {data.SingleCollection}
							 | 
						|
								                                    } else {
							 | 
						|
								                                        {fmt.Sprintf("%d", data.CollectionCount)}
							 | 
						|
								                                    }
							 | 
						|
								                                </div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-layer-group fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								
							 | 
						|
								            <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
							 | 
						|
								                <div class="card border-left-info shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-info text-uppercase mb-1">
							 | 
						|
								                                    if data.DataCenterCount == 1 {
							 | 
						|
								                                        Data Center
							 | 
						|
								                                    } else {
							 | 
						|
								                                        Data Centers
							 | 
						|
								                                    }
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">
							 | 
						|
								                                    if data.DataCenterCount == 1 {
							 | 
						|
								                                        {data.SingleDataCenter}
							 | 
						|
								                                    } else {
							 | 
						|
								                                        {fmt.Sprintf("%d", data.DataCenterCount)}
							 | 
						|
								                                    }
							 | 
						|
								                                </div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-building fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								
							 | 
						|
								            <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
							 | 
						|
								                <div class="card border-left-secondary shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-secondary text-uppercase mb-1">
							 | 
						|
								                                    if data.RackCount == 1 {
							 | 
						|
								                                        Rack
							 | 
						|
								                                    } else {
							 | 
						|
								                                        Racks
							 | 
						|
								                                    }
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">
							 | 
						|
								                                    if data.RackCount == 1 {
							 | 
						|
								                                        {data.SingleRack}
							 | 
						|
								                                    } else {
							 | 
						|
								                                        {fmt.Sprintf("%d", data.RackCount)}
							 | 
						|
								                                    }
							 | 
						|
								                                </div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-server fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								
							 | 
						|
								            <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
							 | 
						|
								                <div class="card border-left-dark shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-dark text-uppercase mb-1">
							 | 
						|
								                                    if data.DiskTypeCount == 1 {
							 | 
						|
								                                        Disk Type
							 | 
						|
								                                    } else {
							 | 
						|
								                                        Disk Types
							 | 
						|
								                                    }
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">
							 | 
						|
								                                    if data.DiskTypeCount == 1 {
							 | 
						|
								                                        {data.SingleDiskType}
							 | 
						|
								                                    } else {
							 | 
						|
								                                        {strings.Join(data.AllDiskTypes, ", ")}
							 | 
						|
								                                    }
							 | 
						|
								                                </div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-hdd fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								
							 | 
						|
								            <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
							 | 
						|
								                <div class="card border-left-purple shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-purple text-uppercase mb-1">
							 | 
						|
								                                    if data.VersionCount == 1 {
							 | 
						|
								                                        Version
							 | 
						|
								                                    } else {
							 | 
						|
								                                        Versions
							 | 
						|
								                                    }
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">
							 | 
						|
								                                    if data.VersionCount == 1 {
							 | 
						|
								                                        {data.SingleVersion}
							 | 
						|
								                                    } else {
							 | 
						|
								                                        {strings.Join(data.AllVersions, ", ")}
							 | 
						|
								                                    }
							 | 
						|
								                                </div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-code-branch fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								
							 | 
						|
								            <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
							 | 
						|
								                <div class="card border-left-warning shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
							 | 
						|
								                                    Total Size
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">
							 | 
						|
								                                    {formatBytes(data.TotalSize)}
							 | 
						|
								                                </div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-chart-area fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								        </div>
							 | 
						|
								
							 | 
						|
								        <!-- Volumes Table -->
							 | 
						|
								        <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-database me-2"></i>Volume Details
							 | 
						|
								                </h6>
							 | 
						|
								            </div>
							 | 
						|
								            <div class="card-body">
							 | 
						|
								                if len(data.Volumes) > 0 {
							 | 
						|
								                    <div class="table-responsive">
							 | 
						|
								                        <table class="table table-hover" id="volumesTable">
							 | 
						|
								                            <thead>
							 | 
						|
								                                <tr>
							 | 
						|
								                                    <th>
							 | 
						|
								                                        <a href="#" onclick="sortTable('id')" class="text-decoration-none text-dark">
							 | 
						|
								                                            Volume ID
							 | 
						|
								                                            @getSortIcon("id", data.SortBy, data.SortOrder)
							 | 
						|
								                                        </a>
							 | 
						|
								                                    </th>
							 | 
						|
								                                    <th>
							 | 
						|
								                                        <a href="#" onclick="sortTable('server')" class="text-decoration-none text-dark">
							 | 
						|
								                                            Server
							 | 
						|
								                                            @getSortIcon("server", data.SortBy, data.SortOrder)
							 | 
						|
								                                        </a>
							 | 
						|
								                                    </th>
							 | 
						|
								                                    if data.ShowDataCenterColumn {
							 | 
						|
								                                        <th>
							 | 
						|
								                                            <a href="#" onclick="sortTable('datacenter')" class="text-decoration-none text-dark">
							 | 
						|
								                                                Data Center
							 | 
						|
								                                                @getSortIcon("datacenter", data.SortBy, data.SortOrder)
							 | 
						|
								                                            </a>
							 | 
						|
								                                        </th>
							 | 
						|
								                                    }
							 | 
						|
								                                    if data.ShowRackColumn {
							 | 
						|
								                                        <th>
							 | 
						|
								                                            <a href="#" onclick="sortTable('rack')" class="text-decoration-none text-dark">
							 | 
						|
								                                                Rack
							 | 
						|
								                                                @getSortIcon("rack", data.SortBy, data.SortOrder)
							 | 
						|
								                                            </a>
							 | 
						|
								                                        </th>
							 | 
						|
								                                    }
							 | 
						|
								                                    if data.ShowCollectionColumn {
							 | 
						|
								                                        <th>
							 | 
						|
								                                            <a href="#" onclick="sortTable('collection')" class="text-decoration-none text-dark">
							 | 
						|
								                                                Collection
							 | 
						|
								                                                @getSortIcon("collection", data.SortBy, data.SortOrder)
							 | 
						|
								                                            </a>
							 | 
						|
								                                        </th>
							 | 
						|
								                                    }
							 | 
						|
								                                    <th>
							 | 
						|
								                                        <a href="#" onclick="sortTable('size')" class="text-decoration-none text-dark">
							 | 
						|
								                                            Size
							 | 
						|
								                                            @getSortIcon("size", data.SortBy, data.SortOrder)
							 | 
						|
								                                        </a>
							 | 
						|
								                                    </th>
							 | 
						|
								                                    <th>Volume Utilization</th>
							 | 
						|
								                                    <th>
							 | 
						|
								                                        <a href="#" onclick="sortTable('filecount')" class="text-decoration-none text-dark">
							 | 
						|
								                                            File Count
							 | 
						|
								                                            @getSortIcon("filecount", data.SortBy, data.SortOrder)
							 | 
						|
								                                        </a>
							 | 
						|
								                                    </th>
							 | 
						|
								                                    <th>
							 | 
						|
								                                        <a href="#" onclick="sortTable('replication')" class="text-decoration-none text-dark">
							 | 
						|
								                                            Replication
							 | 
						|
								                                            @getSortIcon("replication", data.SortBy, data.SortOrder)
							 | 
						|
								                                        </a>
							 | 
						|
								                                    </th>
							 | 
						|
								                                    if data.ShowDiskTypeColumn {
							 | 
						|
								                                        <th>
							 | 
						|
								                                            <a href="#" onclick="sortTable('disktype')" class="text-decoration-none text-dark">
							 | 
						|
								                                                Disk Type
							 | 
						|
								                                                @getSortIcon("disktype", data.SortBy, data.SortOrder)
							 | 
						|
								                                            </a>
							 | 
						|
								                                        </th>
							 | 
						|
								                                    }
							 | 
						|
								                                    if data.ShowVersionColumn {
							 | 
						|
								                                        <th>
							 | 
						|
								                                            <a href="#" onclick="sortTable('version')" class="text-decoration-none text-dark">
							 | 
						|
								                                                Version
							 | 
						|
								                                                @getSortIcon("version", data.SortBy, data.SortOrder)
							 | 
						|
								                                            </a>
							 | 
						|
								                                        </th>
							 | 
						|
								                                    }
							 | 
						|
								                                    <th>Actions</th>
							 | 
						|
								                                </tr>
							 | 
						|
								                            </thead>
							 | 
						|
								                            <tbody>
							 | 
						|
								                                for _, volume := range data.Volumes {
							 | 
						|
								                                    <tr>
							 | 
						|
								                                        <td>
							 | 
						|
								                                            <code class="volume-id-link" style="cursor: pointer; text-decoration: underline; color: #0d6efd;" 
							 | 
						|
								                                                  data-volume-id={fmt.Sprintf("%d", volume.Id)}
							 | 
						|
								                                                  title="Click to view volume details">
							 | 
						|
								                                                {fmt.Sprintf("%d", volume.Id)}
							 | 
						|
								                                            </code>
							 | 
						|
								                                        </td>
							 | 
						|
								                                        <td>
							 | 
						|
								                                            <a href={templ.SafeURL(fmt.Sprintf("http://%s/ui/index.html", volume.Server))} target="_blank" class="text-decoration-none">
							 | 
						|
								                                                {volume.Server}
							 | 
						|
								                                                <i class="fas fa-external-link-alt ms-1 text-muted"></i>
							 | 
						|
								                                            </a>
							 | 
						|
								                                        </td>
							 | 
						|
								                                        if data.ShowDataCenterColumn {
							 | 
						|
								                                            <td>
							 | 
						|
								                                                <span class="badge bg-light text-dark">{volume.DataCenter}</span>
							 | 
						|
								                                            </td>
							 | 
						|
								                                        }
							 | 
						|
								                                        if data.ShowRackColumn {
							 | 
						|
								                                            <td>
							 | 
						|
								                                                <span class="badge bg-light text-dark">{volume.Rack}</span>
							 | 
						|
								                                            </td>
							 | 
						|
								                                        }
							 | 
						|
								                                        if data.ShowCollectionColumn {
							 | 
						|
								                                            <td>
							 | 
						|
								                                                if 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", volume.Collection))} class="text-decoration-none">
							 | 
						|
								                                                        <span class="badge bg-secondary">{volume.Collection}</span>
							 | 
						|
								                                                    </a>
							 | 
						|
								                                                }
							 | 
						|
								                                            </td>
							 | 
						|
								                                        }
							 | 
						|
								                                                                <td>{formatBytes(int64(volume.Size))}</td>
							 | 
						|
								                        <td>
							 | 
						|
								                            <div class="d-flex align-items-center">
							 | 
						|
								                                <div class="progress me-2" style="width: 80px; height: 16px; background-color: #e9ecef;">
							 | 
						|
								                                    <!-- Active data (green) -->
							 | 
						|
								                                    <div class="progress-bar bg-success" role="progressbar" 
							 | 
						|
								                                         style={fmt.Sprintf("width: %.1f%%", 
							 | 
						|
								                                             func() float64 {
							 | 
						|
								                                                 if volume.Size > 0 {
							 | 
						|
								                                                     activePct := float64(volume.Size - volume.DeletedByteCount) / float64(volume.Size) * 100
							 | 
						|
								                                                     if data.VolumeSizeLimit > 0 {
							 | 
						|
								                                                         return activePct * float64(volume.Size) / float64(data.VolumeSizeLimit) * 100
							 | 
						|
								                                                     }
							 | 
						|
								                                                     return activePct
							 | 
						|
								                                                 }
							 | 
						|
								                                                 return 0
							 | 
						|
								                                             }())}
							 | 
						|
								                                         title={fmt.Sprintf("Active: %s", formatBytes(int64(volume.Size - volume.DeletedByteCount)))}>
							 | 
						|
								                                    </div>
							 | 
						|
								                                    <!-- Garbage data (red) -->
							 | 
						|
								                                    <div class="progress-bar bg-danger" role="progressbar" 
							 | 
						|
								                                         style={fmt.Sprintf("width: %.1f%%", 
							 | 
						|
								                                             func() float64 {
							 | 
						|
								                                                 if volume.Size > 0 && volume.DeletedByteCount > 0 {
							 | 
						|
								                                                     garbagePct := float64(volume.DeletedByteCount) / float64(volume.Size) * 100
							 | 
						|
								                                                     if data.VolumeSizeLimit > 0 {
							 | 
						|
								                                                         return garbagePct * float64(volume.Size) / float64(data.VolumeSizeLimit) * 100
							 | 
						|
								                                                     }
							 | 
						|
								                                                     return garbagePct
							 | 
						|
								                                                 }
							 | 
						|
								                                                 return 0
							 | 
						|
								                                             }())}
							 | 
						|
								                                         title={fmt.Sprintf("Garbage: %s", formatBytes(int64(volume.DeletedByteCount)))}>
							 | 
						|
								                                    </div>
							 | 
						|
								                                </div>
							 | 
						|
								                                <small class="text-muted">
							 | 
						|
								                                    {func() string {
							 | 
						|
								                                        if data.VolumeSizeLimit > 0 {
							 | 
						|
								                                            return fmt.Sprintf("%.0f%%", float64(volume.Size)/float64(data.VolumeSizeLimit)*100)
							 | 
						|
								                                        }
							 | 
						|
								                                        return "N/A"
							 | 
						|
								                                    }()}
							 | 
						|
								                                </small>
							 | 
						|
								                            </div>
							 | 
						|
								                        </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>
							 | 
						|
								                                            </td>
							 | 
						|
								                                        }
							 | 
						|
								                                        if data.ShowVersionColumn {
							 | 
						|
								                                            <td>
							 | 
						|
								                                                <span class="badge bg-dark">{fmt.Sprintf("v%d", volume.Version)}</span>
							 | 
						|
								                                            </td>
							 | 
						|
								                                        }
							 | 
						|
								                                        <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={fmt.Sprintf("%d", volume.Id)}>
							 | 
						|
								                                                    <i class="fas fa-eye"></i>
							 | 
						|
								                                                </button>
							 | 
						|
								                                                <button type="button" class="btn btn-outline-secondary btn-sm vacuum-btn" 
							 | 
						|
								                                                        title="Vacuum"
							 | 
						|
								                                                        data-volume-id={fmt.Sprintf("%d", volume.Id)}
							 | 
						|
								                                                        data-server={volume.Server}>
							 | 
						|
								                                                    <i class="fas fa-compress-alt"></i>
							 | 
						|
								                                                </button>
							 | 
						|
								                                            </div>
							 | 
						|
								                                        </td>
							 | 
						|
								                                    </tr>
							 | 
						|
								                                }
							 | 
						|
								                            </tbody>
							 | 
						|
								                        </table>
							 | 
						|
								                    </div>
							 | 
						|
								                    
							 | 
						|
								                    <!-- Volume Summary -->
							 | 
						|
								                    <div class="d-flex justify-content-between align-items-center mt-3">
							 | 
						|
								                        <div>
							 | 
						|
								                            <small class="text-muted">
							 | 
						|
								                                Showing {fmt.Sprintf("%d", (data.CurrentPage-1)*data.PageSize + 1)} to {fmt.Sprintf("%d", minInt(data.CurrentPage*data.PageSize, data.TotalVolumes))} of {fmt.Sprintf("%d", data.TotalVolumes)} volumes
							 | 
						|
								                            </small>
							 | 
						|
								                        </div>
							 | 
						|
								                        if data.TotalPages > 1 {
							 | 
						|
								                            <div>
							 | 
						|
								                                <small class="text-muted">
							 | 
						|
								                                    Page {fmt.Sprintf("%d", data.CurrentPage)} of {fmt.Sprintf("%d", data.TotalPages)}
							 | 
						|
								                                </small>
							 | 
						|
								                            </div>
							 | 
						|
								                        }
							 | 
						|
								                    </div>
							 | 
						|
								                    
							 | 
						|
								                    <!-- Pagination Controls -->
							 | 
						|
								                    if data.TotalPages > 1 {
							 | 
						|
								                        <div class="d-flex justify-content-center mt-3">
							 | 
						|
								                            <nav aria-label="Volumes pagination">
							 | 
						|
								                                <ul class="pagination pagination-sm mb-0">
							 | 
						|
								                                    <!-- Previous Button -->
							 | 
						|
								                                    if data.CurrentPage > 1 {
							 | 
						|
								                                        <li class="page-item">
							 | 
						|
								                                            <a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", data.CurrentPage-1)}>
							 | 
						|
								                                                <i class="fas fa-chevron-left"></i>
							 | 
						|
								                                            </a>
							 | 
						|
								                                        </li>
							 | 
						|
								                                    } else {
							 | 
						|
								                                        <li class="page-item disabled">
							 | 
						|
								                                            <span class="page-link">
							 | 
						|
								                                                <i class="fas fa-chevron-left"></i>
							 | 
						|
								                                            </span>
							 | 
						|
								                                        </li>
							 | 
						|
								                                    }
							 | 
						|
								                                    
							 | 
						|
								                                    <!-- Page Numbers -->
							 | 
						|
								                                    for i := maxInt(1, data.CurrentPage-2); i <= minInt(data.TotalPages, data.CurrentPage+2); i++ {
							 | 
						|
								                                        if i == data.CurrentPage {
							 | 
						|
								                                            <li class="page-item active">
							 | 
						|
								                                                <span class="page-link">{fmt.Sprintf("%d", i)}</span>
							 | 
						|
								                                            </li>
							 | 
						|
								                                        } else {
							 | 
						|
								                                            <li class="page-item">
							 | 
						|
								                                                <a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", i)}>{fmt.Sprintf("%d", i)}</a>
							 | 
						|
								                                            </li>
							 | 
						|
								                                        }
							 | 
						|
								                                    }
							 | 
						|
								                                    
							 | 
						|
								                                    <!-- Next Button -->
							 | 
						|
								                                    if data.CurrentPage < data.TotalPages {
							 | 
						|
								                                        <li class="page-item">
							 | 
						|
								                                            <a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", data.CurrentPage+1)}>
							 | 
						|
								                                                <i class="fas fa-chevron-right"></i>
							 | 
						|
								                                            </a>
							 | 
						|
								                                        </li>
							 | 
						|
								                                    } else {
							 | 
						|
								                                        <li class="page-item disabled">
							 | 
						|
								                                            <span class="page-link">
							 | 
						|
								                                                <i class="fas fa-chevron-right"></i>
							 | 
						|
								                                            </span>
							 | 
						|
								                                        </li>
							 | 
						|
								                                    }
							 | 
						|
								                                </ul>
							 | 
						|
								                            </nav>
							 | 
						|
								                        </div>
							 | 
						|
								                    }
							 | 
						|
								                } else {
							 | 
						|
								                    <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>
							 | 
						|
								                }
							 | 
						|
								            </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>
							 | 
						|
								    </div>
							 | 
						|
								    
							 | 
						|
								    <!-- JavaScript for pagination and sorting -->
							 | 
						|
								    <script>
							 | 
						|
								        // Initialize pagination links when page loads
							 | 
						|
								        document.addEventListener('DOMContentLoaded', function() {
							 | 
						|
								            // Add click handlers to pagination links
							 | 
						|
								            document.querySelectorAll('.pagination-link').forEach(link => {
							 | 
						|
								                link.addEventListener('click', function(e) {
							 | 
						|
								                    e.preventDefault();
							 | 
						|
								                    const page = this.getAttribute('data-page');
							 | 
						|
								                    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);
							 | 
						|
								                });
							 | 
						|
								            });
							 | 
						|
								
							 | 
						|
								            // Add click handlers to volume ID links
							 | 
						|
								            document.querySelectorAll('.volume-id-link').forEach(link => {
							 | 
						|
								                link.addEventListener('click', function(e) {
							 | 
						|
								                    e.preventDefault();
							 | 
						|
								                    const volumeId = this.getAttribute('data-volume-id');
							 | 
						|
								                    viewVolumeDetails(volumeId);
							 | 
						|
								                });
							 | 
						|
								            });
							 | 
						|
								
							 | 
						|
								            // Add click handlers to vacuum buttons
							 | 
						|
								            document.querySelectorAll('.vacuum-btn').forEach(button => {
							 | 
						|
								                button.addEventListener('click', function(e) {
							 | 
						|
								                    e.preventDefault();
							 | 
						|
								                    const volumeId = this.getAttribute('data-volume-id');
							 | 
						|
								                    const server = this.getAttribute('data-server');
							 | 
						|
								                    performVacuum(volumeId, server, this);
							 | 
						|
								                });
							 | 
						|
								            });
							 | 
						|
								        });
							 | 
						|
								        
							 | 
						|
								        function goToPage(page) {
							 | 
						|
								            const url = new URL(window.location);
							 | 
						|
								            url.searchParams.set('page', page);
							 | 
						|
								            window.location.href = url.toString();
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        function changePageSize() {
							 | 
						|
								            const pageSize = document.getElementById('pageSizeSelect').value;
							 | 
						|
								            const url = new URL(window.location);
							 | 
						|
								            url.searchParams.set('pageSize', pageSize);
							 | 
						|
								            url.searchParams.set('page', '1'); // Reset to first page
							 | 
						|
								            window.location.href = url.toString();
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        function sortTable(column) {
							 | 
						|
								            const url = new URL(window.location);
							 | 
						|
								            const currentSort = url.searchParams.get('sortBy');
							 | 
						|
								            const currentOrder = url.searchParams.get('sortOrder') || 'asc';
							 | 
						|
								            
							 | 
						|
								            let newOrder = 'asc';
							 | 
						|
								            if (currentSort === column && currentOrder === 'asc') {
							 | 
						|
								                newOrder = 'desc';
							 | 
						|
								            }
							 | 
						|
								            
							 | 
						|
								            url.searchParams.set('sortBy', column);
							 | 
						|
								            url.searchParams.set('sortOrder', newOrder);
							 | 
						|
								            url.searchParams.set('page', '1'); // Reset to first page
							 | 
						|
								            window.location.href = url.toString();
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        function exportVolumes() {
							 | 
						|
								            // TODO: Implement volume export functionality
							 | 
						|
								            alert('Export functionality to be implemented');
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        function viewVolumeDetails(volumeId) {
							 | 
						|
								            // Get the server from the current row - works for both buttons and volume ID links
							 | 
						|
								            const clickedElement = event.target;
							 | 
						|
								            const row = clickedElement.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)}`;
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        function performVacuum(volumeId, server, button) {
							 | 
						|
								            // Disable button and show loading state
							 | 
						|
								            const originalHTML = button.innerHTML;
							 | 
						|
								            button.disabled = true;
							 | 
						|
								            button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
							 | 
						|
								
							 | 
						|
								            // Send vacuum request
							 | 
						|
								            fetch(`/api/volumes/${volumeId}/${encodeURIComponent(server)}/vacuum`, {
							 | 
						|
								                method: 'POST',
							 | 
						|
								                headers: {
							 | 
						|
								                    'Content-Type': 'application/json',
							 | 
						|
								                }
							 | 
						|
								            })
							 | 
						|
								            .then(response => response.json())
							 | 
						|
								            .then(data => {
							 | 
						|
								                if (data.error) {
							 | 
						|
								                    showMessage(data.error, 'error');
							 | 
						|
								                } else {
							 | 
						|
								                    showMessage(data.message || 'Volume vacuum started successfully', 'success');
							 | 
						|
								                    // Optionally refresh the page after a delay to show updated vacuum status
							 | 
						|
								                    setTimeout(() => {
							 | 
						|
								                        window.location.reload();
							 | 
						|
								                    }, 2000);
							 | 
						|
								                }
							 | 
						|
								            })
							 | 
						|
								            .catch(error => {
							 | 
						|
								                console.error('Error:', error);
							 | 
						|
								                showMessage('Failed to start vacuum operation', 'error');
							 | 
						|
								            })
							 | 
						|
								            .finally(() => {
							 | 
						|
								                // Re-enable button
							 | 
						|
								                button.disabled = false;
							 | 
						|
								                button.innerHTML = originalHTML;
							 | 
						|
								            });
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        function showMessage(message, type) {
							 | 
						|
								            // Create toast notification
							 | 
						|
								            const toast = document.createElement('div');
							 | 
						|
								            toast.className = `alert alert-${type === 'error' ? 'danger' : 'success'} alert-dismissible fade show position-fixed`;
							 | 
						|
								            toast.style.top = '20px';
							 | 
						|
								            toast.style.right = '20px';
							 | 
						|
								            toast.style.zIndex = '9999';
							 | 
						|
								            toast.style.minWidth = '300px';
							 | 
						|
								            
							 | 
						|
								            toast.innerHTML = `
							 | 
						|
								                ${message}
							 | 
						|
								                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
							 | 
						|
								            `;
							 | 
						|
								            
							 | 
						|
								            document.body.appendChild(toast);
							 | 
						|
								            
							 | 
						|
								            // Auto-remove after 5 seconds
							 | 
						|
								            setTimeout(() => {
							 | 
						|
								                if (toast.parentNode) {
							 | 
						|
								                    toast.parentNode.removeChild(toast);
							 | 
						|
								                }
							 | 
						|
								            }, 5000);
							 | 
						|
								        }
							 | 
						|
								    </script>
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func countActiveVolumes(volumes []dash.VolumeWithTopology) int {
							 | 
						|
									// Since we removed status tracking, consider all volumes as active
							 | 
						|
									return len(volumes)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func countUniqueDataCenters(volumes []dash.VolumeWithTopology) int {
							 | 
						|
								    dcMap := make(map[string]bool)
							 | 
						|
								    for _, volume := range volumes {
							 | 
						|
								        dcMap[volume.DataCenter] = true
							 | 
						|
								    }
							 | 
						|
								    return len(dcMap)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func countUniqueRacks(volumes []dash.VolumeWithTopology) int {
							 | 
						|
								    rackMap := make(map[string]bool)
							 | 
						|
								    for _, volume := range volumes {
							 | 
						|
								        if volume.Rack != "" {
							 | 
						|
								            rackMap[volume.Rack] = true
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    return len(rackMap)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func countUniqueDiskTypes(volumes []dash.VolumeWithTopology) int {
							 | 
						|
								    diskTypeMap := make(map[string]bool)
							 | 
						|
								    for _, volume := range volumes {
							 | 
						|
								        diskType := volume.DiskType
							 | 
						|
								        if diskType == "" {
							 | 
						|
								            diskType = "hdd"
							 | 
						|
								        }
							 | 
						|
								        diskTypeMap[diskType] = true
							 | 
						|
								    }
							 | 
						|
								    return len(diskTypeMap)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								
							 | 
						|
								templ getSortIcon(column, currentSort, currentOrder string) {
							 | 
						|
								    if column != currentSort {
							 | 
						|
								        <i class="fas fa-sort text-muted ms-1"></i>
							 | 
						|
								    } else if currentOrder == "asc" {
							 | 
						|
								        <i class="fas fa-sort-up text-primary ms-1"></i>
							 | 
						|
								    } else {
							 | 
						|
								        <i class="fas fa-sort-down text-primary ms-1"></i>
							 | 
						|
								    }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func minInt(a, b int) int {
							 | 
						|
								    if a < b {
							 | 
						|
								        return a
							 | 
						|
								    }
							 | 
						|
								    return b
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func maxInt(a, b int) int {
							 | 
						|
								    if a > b {
							 | 
						|
								        return a
							 | 
						|
								    }
							 | 
						|
								    return b
							 | 
						|
								} 
							 |