Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
732 lines
37 KiB
Plaintext
732 lines
37 KiB
Plaintext
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={ dash.PUrl(ctx, "/storage/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 Page
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="exportVolumeList()" title="Download the full cluster volume list as JSON">
|
|
<i class="fas fa-file-export me-1"></i>Export All (JSON)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="volumes-content">
|
|
<!-- Summary Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-2 col-md-4 col-sm-6 mb-4">
|
|
<div class="card border-left-primary shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
|
Total Volumes
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{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("//%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={ dash.PUrl(ctx, "/storage/volumes?collection=default") } class="text-decoration-none">
|
|
<span class="badge bg-secondary">default</span>
|
|
</a>
|
|
} else {
|
|
<a href={ dash.PUrl(ctx, fmt.Sprintf("/storage/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 {
|
|
if data.VolumeSizeLimit > 0 {
|
|
return float64(volume.Size - volume.DeletedByteCount) / float64(data.VolumeSizeLimit) * 100
|
|
}
|
|
return float64(volume.Size - volume.DeletedByteCount) / float64(volume.Size) * 100
|
|
}
|
|
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 {
|
|
if data.VolumeSizeLimit > 0 {
|
|
return float64(volume.DeletedByteCount) / float64(data.VolumeSizeLimit) * 100
|
|
}
|
|
return float64(volume.DeletedByteCount) / float64(volume.Size) * 100
|
|
}
|
|
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">{displayDiskType(volume.DiskType)}</span>
|
|
if volume.RemoteStorageName != "" {
|
|
<span class="badge bg-info text-white ms-1" title={volume.RemoteStorageKey}>
|
|
<i class="fas fa-cloud me-1"></i>{volume.RemoteStorageName}
|
|
</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 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 = (window.__BASE_PATH__ || '') + `/storage/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((window.__BASE_PATH__ || '') + `/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)
|
|
}
|
|
|
|
// displayDiskType maps the empty disk type to its display name.
|
|
func displayDiskType(diskType string) string {
|
|
if diskType == "" {
|
|
return "hdd"
|
|
}
|
|
return diskType
|
|
}
|
|
|
|
func countUniqueDiskTypes(volumes []dash.VolumeWithTopology) int {
|
|
diskTypeMap := make(map[string]bool)
|
|
for _, volume := range volumes {
|
|
diskTypeMap[displayDiskType(volume.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
|
|
}
|