mirror of
https://github.com/chrislusf/seaweedfs
synced 2025-07-04 18:52:47 +02:00
581 lines
No EOL
29 KiB
Text
581 lines
No EOL
29 KiB
Text
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>
|
|
<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>{fmt.Sprintf("%d", volume.ID)}</code>
|
|
</td>
|
|
<td>
|
|
<a href={templ.SafeURL(fmt.Sprintf("http://%s", 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(volume.Size)}</td>
|
|
<td>{fmt.Sprintf("%d", volume.FileCount)}</td>
|
|
<td>
|
|
<span class="badge bg-info">{volume.Replication}</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"
|
|
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>
|
|
}
|
|
</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);
|
|
});
|
|
});
|
|
});
|
|
|
|
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');
|
|
}
|
|
</script>
|
|
}
|
|
|
|
func countActiveVolumes(volumes []dash.VolumeInfo) int {
|
|
// Since we removed status tracking, consider all volumes as active
|
|
return len(volumes)
|
|
}
|
|
|
|
func countUniqueDataCenters(volumes []dash.VolumeInfo) int {
|
|
dcMap := make(map[string]bool)
|
|
for _, volume := range volumes {
|
|
dcMap[volume.DataCenter] = true
|
|
}
|
|
return len(dcMap)
|
|
}
|
|
|
|
func countUniqueRacks(volumes []dash.VolumeInfo) int {
|
|
rackMap := make(map[string]bool)
|
|
for _, volume := range volumes {
|
|
if volume.Rack != "" {
|
|
rackMap[volume.Rack] = true
|
|
}
|
|
}
|
|
return len(rackMap)
|
|
}
|
|
|
|
func countUniqueDiskTypes(volumes []dash.VolumeInfo) 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
|
|
} |