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
523 lines
26 KiB
Plaintext
523 lines
26 KiB
Plaintext
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
)
|
|
|
|
templ VolumeDetails(data dash.VolumeDetailsData) {
|
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
|
<div>
|
|
<h1 class="h2">
|
|
<i class="fas fa-database me-2"></i>Volume Details
|
|
</h1>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href={ dash.PUrl(ctx, "/admin") } class="text-decoration-none">Dashboard</a></li>
|
|
<li class="breadcrumb-item"><a href={ dash.PUrl(ctx, "/storage/volumes") } class="text-decoration-none">Volumes</a></li>
|
|
<li class="breadcrumb-item active" aria-current="page">Volume {fmt.Sprintf("%d", data.Volume.Id)}</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
<div class="btn-toolbar mb-2 mb-md-0">
|
|
<div class="btn-group me-2">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="history.back()">
|
|
<i class="fas fa-arrow-left me-1"></i>Back
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="window.location.reload()">
|
|
<i class="fas fa-refresh me-1"></i>Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Volume Information Card -->
|
|
<div class="col-lg-8">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-info-circle me-2"></i>Volume Information
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label"><strong>Volume ID:</strong></label>
|
|
<div><code class="fs-5">{fmt.Sprintf("%d", data.Volume.Id)}</code></div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label"><strong>Server:</strong></label>
|
|
<div>
|
|
<a href={templ.SafeURL(fmt.Sprintf("//%s/ui/index.html", data.Volume.Server))} target="_blank" class="text-decoration-none">
|
|
{data.Volume.Server}
|
|
<i class="fas fa-external-link-alt ms-1 text-muted"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label"><strong>Data Center:</strong></label>
|
|
<div><span class="badge bg-light text-dark">{data.Volume.DataCenter}</span></div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label"><strong>Rack:</strong></label>
|
|
<div><span class="badge bg-light text-dark">{data.Volume.Rack}</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label"><strong>Collection:</strong></label>
|
|
<div>
|
|
if data.Volume.Collection == "" {
|
|
<a href={ 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", data.Volume.Collection)) } class="text-decoration-none">
|
|
<span class="badge bg-secondary">{data.Volume.Collection}</span>
|
|
</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label"><strong>Replication:</strong></label>
|
|
<div><span class="badge bg-info">{fmt.Sprintf("%03d", data.Volume.ReplicaPlacement)}</span></div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label"><strong>Disk Type:</strong></label>
|
|
<div>
|
|
<span class="badge bg-primary">
|
|
if data.Volume.DiskType == "" {
|
|
hdd
|
|
} else {
|
|
{data.Volume.DiskType}
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label"><strong>Version:</strong></label>
|
|
<div><span class="badge bg-dark">{fmt.Sprintf("v%d", data.Volume.Version)}</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Card -->
|
|
<div class="col-lg-4">
|
|
<!-- Volume Statistics & Health Card -->
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-chart-pie me-2"></i>Volume Statistics & Health
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Storage Metrics -->
|
|
<div class="row mb-3">
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<div class="h4 mb-0 font-weight-bold text-success">
|
|
{formatBytes(int64(data.Volume.Size - data.Volume.DeletedByteCount))}
|
|
</div>
|
|
<small class="text-muted">Active Bytes</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<div class="h4 mb-0 font-weight-bold text-danger">
|
|
{formatBytes(int64(data.Volume.DeletedByteCount))}
|
|
</div>
|
|
<small class="text-muted">Deleted Bytes</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- File Metrics -->
|
|
<div class="row mb-3">
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<div class="h4 mb-0 font-weight-bold text-success">
|
|
{fmt.Sprintf("%d", data.Volume.FileCount)}
|
|
</div>
|
|
<small class="text-muted">Active Files</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<div class="h4 mb-0 font-weight-bold text-danger">
|
|
{fmt.Sprintf("%d", data.Volume.DeleteCount)}
|
|
</div>
|
|
<small class="text-muted">Deleted Files</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Storage Efficiency -->
|
|
if data.Volume.FileCount > 0 && data.Volume.Size > 0 {
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<small class="text-muted">Storage Efficiency</small>
|
|
<small class="text-muted">
|
|
{fmt.Sprintf("%.1f%%", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)}
|
|
</small>
|
|
</div>
|
|
<div class="progress" style="height: 8px;">
|
|
<div class="progress-bar bg-info" role="progressbar"
|
|
style={fmt.Sprintf("width: %.1f%%", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)}
|
|
aria-valuenow={fmt.Sprintf("%.1f", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)}
|
|
aria-valuemin="0" aria-valuemax="100">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<hr class="my-3">
|
|
|
|
<!-- Status & Configuration -->
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<div class="text-center mb-2">
|
|
if data.Volume.ReadOnly {
|
|
<span class="badge bg-warning fs-6 px-3 py-2">
|
|
<i class="fas fa-lock me-1"></i>Read Only
|
|
</span>
|
|
if data.Volume.Size >= data.VolumeSizeLimit {
|
|
<div class="mt-1">
|
|
<small class="text-muted">Size limit exceeded</small>
|
|
</div>
|
|
}
|
|
} else if data.VolumeSizeLimit > data.Volume.Size {
|
|
<span class="badge bg-success fs-6 px-3 py-2">
|
|
<i class="fas fa-edit me-1"></i>Read/Write
|
|
</span>
|
|
} else {
|
|
<span class="badge bg-warning fs-6 px-3 py-2">
|
|
<i class="fas fa-exclamation-triangle me-1"></i>Size Limit Reached
|
|
</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Maintenance Info -->
|
|
<div class="row mb-3">
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<div class="h6 mb-0 font-weight-bold text-info">
|
|
#{fmt.Sprintf("%d", data.Volume.CompactRevision)}
|
|
</div>
|
|
<small class="text-muted">Vacuum Revision</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<div class="h6 mb-0 font-weight-bold text-secondary">
|
|
if data.Volume.ModifiedAtSecond > 0 {
|
|
{formatTimestamp(data.Volume.ModifiedAtSecond)}
|
|
} else {
|
|
<span class="text-muted">Never modified</span>
|
|
}
|
|
</div>
|
|
<small class="text-muted">Last Modified</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TTL Configuration -->
|
|
if data.Volume.Ttl > 0 {
|
|
<div class="mb-3 text-center">
|
|
<span class="badge bg-info fs-6 px-3 py-2">
|
|
<i class="fas fa-clock me-1"></i>{formatTTL(data.Volume.Ttl)}
|
|
</span>
|
|
<div class="mt-1">
|
|
<small class="text-muted">Time To Live</small>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Remote Storage Configuration -->
|
|
if data.Volume.RemoteStorageName != "" {
|
|
<hr class="my-3">
|
|
<div class="mb-2">
|
|
<div class="text-center">
|
|
<div class="h6 mb-1 font-weight-bold text-info">
|
|
<i class="fas fa-cloud me-1"></i>{data.Volume.RemoteStorageName}
|
|
</div>
|
|
<small class="text-muted">Remote Storage</small>
|
|
</div>
|
|
</div>
|
|
if data.Volume.RemoteStorageKey != "" {
|
|
<div class="text-center">
|
|
<div class="text-xs font-monospace bg-light p-2 rounded text-truncate" title={data.Volume.RemoteStorageKey}>
|
|
{data.Volume.RemoteStorageKey}
|
|
</div>
|
|
<small class="text-muted">Storage Key</small>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Replicas Card -->
|
|
if len(data.Replicas) > 0 {
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-copy me-2"></i>Replicas ({fmt.Sprintf("%d", data.ReplicationCount)})
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Server</th>
|
|
<th>Data Center</th>
|
|
<th>Rack</th>
|
|
<th>Size</th>
|
|
<th>File Count</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<!-- Primary Volume (current one) -->
|
|
<tr class="table-primary">
|
|
<td>
|
|
<strong>
|
|
<a href={templ.SafeURL(fmt.Sprintf("//%s/ui/index.html", data.Volume.Server))} target="_blank" class="text-decoration-none">
|
|
{data.Volume.Server}
|
|
<i class="fas fa-external-link-alt ms-1 text-muted"></i>
|
|
</a>
|
|
</strong>
|
|
<span class="badge bg-success ms-2">Primary</span>
|
|
</td>
|
|
<td><span class="badge bg-light text-dark">{data.Volume.DataCenter}</span></td>
|
|
<td><span class="badge bg-light text-dark">{data.Volume.Rack}</span></td>
|
|
<td>{formatBytes(int64(data.Volume.Size))}</td>
|
|
<td>{fmt.Sprintf("%d", data.Volume.FileCount)}</td>
|
|
<td><span class="badge bg-success">Active</span></td>
|
|
<td>
|
|
<span class="text-muted">Current Volume</span>
|
|
</td>
|
|
</tr>
|
|
<!-- Replica Volumes -->
|
|
for _, replica := range data.Replicas {
|
|
<tr>
|
|
<td>
|
|
<a href={templ.SafeURL(fmt.Sprintf("//%s/ui/index.html", replica.Server))} target="_blank" class="text-decoration-none">
|
|
{replica.Server}
|
|
<i class="fas fa-external-link-alt ms-1 text-muted"></i>
|
|
</a>
|
|
</td>
|
|
<td><span class="badge bg-light text-dark">{replica.DataCenter}</span></td>
|
|
<td><span class="badge bg-light text-dark">{replica.Rack}</span></td>
|
|
<td>{formatBytes(int64(replica.Size))}</td>
|
|
<td>{fmt.Sprintf("%d", replica.FileCount)}</td>
|
|
<td><span class="badge bg-info">Replica</span></td>
|
|
<td>
|
|
<a href={ dash.PUrl(ctx, fmt.Sprintf("/storage/volumes/%d/%s", replica.Id, replica.Server)) } class="btn btn-sm btn-outline-primary">
|
|
<i class="fas fa-eye me-1"></i>View
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Actions Card -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-tools me-2"></i>Actions
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<button type="button" class="btn btn-outline-danger vacuum-btn"
|
|
title="Vacuum Volume"
|
|
data-volume-id={fmt.Sprintf("%d", data.Volume.Id)}
|
|
data-server={data.Volume.Server}>
|
|
<i class="fas fa-compress-alt me-1"></i>Vacuum
|
|
</button>
|
|
if !dash.IsReadOnlyRole(dash.RoleFromContext(ctx)) {
|
|
<button type="button" class="btn btn-outline-warning volume-access-btn"
|
|
data-volume-id={fmt.Sprintf("%d", data.Volume.Id)}
|
|
data-server={data.Volume.Server} data-read-only="true">
|
|
<i class="fas fa-lock me-1"></i>Set Read-Only
|
|
</button>
|
|
<button type="button" class="btn btn-outline-success volume-access-btn"
|
|
data-volume-id={fmt.Sprintf("%d", data.Volume.Id)}
|
|
data-server={data.Volume.Server} data-read-only="false">
|
|
<i class="fas fa-unlock me-1"></i>Set Read/Write
|
|
</button>
|
|
}
|
|
</div>
|
|
if !dash.IsReadOnlyRole(dash.RoleFromContext(ctx)) {
|
|
<div class="mt-3"><small class="text-muted">Access mode changes apply only to this replica and persist across restarts. Volume size and disk limits still apply.</small></div>
|
|
}
|
|
<div class="mt-3">
|
|
<small class="text-muted">
|
|
<i class="fas fa-info-circle me-1"></i>
|
|
Use these actions to perform maintenance operations on the volume.
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Last Updated -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<small class="text-muted">
|
|
<i class="fas fa-clock me-1"></i>
|
|
Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JavaScript for volume actions -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Add click handler for vacuum button
|
|
const vacuumBtn = document.querySelector('.vacuum-btn');
|
|
if (vacuumBtn) {
|
|
vacuumBtn.addEventListener('click', function() {
|
|
const volumeId = this.getAttribute('data-volume-id');
|
|
const server = this.getAttribute('data-server');
|
|
performVacuum(volumeId, server, this);
|
|
});
|
|
}
|
|
document.querySelectorAll('.volume-access-btn').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
setVolumeReadOnly(this.dataset.volumeId, this.dataset.server, this.dataset.readOnly === 'true');
|
|
});
|
|
});
|
|
});
|
|
|
|
async function setVolumeReadOnly(volumeId, server, readOnly) {
|
|
const mode = readOnly ? 'read-only' : 'read/write';
|
|
if (!confirm(`Set volume ${volumeId} on ${server} to ${mode}? This applies only to this replica.`)) {
|
|
return;
|
|
}
|
|
const buttons = document.querySelectorAll('.volume-access-btn');
|
|
buttons.forEach(button => { button.disabled = true; });
|
|
try {
|
|
const response = await fetch((window.__BASE_PATH__ || '') + `/api/volumes/${volumeId}/${encodeURIComponent(server)}/read-only`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ read_only: readOnly })
|
|
});
|
|
const data = await response.json();
|
|
if (!response.ok || data.error) {
|
|
throw new Error(data.error || 'Failed to change volume access mode');
|
|
}
|
|
showMessage(data.message, 'success');
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} catch (error) {
|
|
showMessage(error.message, 'error');
|
|
buttons.forEach(button => { button.disabled = false; });
|
|
}
|
|
}
|
|
|
|
function performVacuum(volumeId, server, button) {
|
|
// Disable button and show loading state
|
|
const originalText = button.innerHTML;
|
|
button.disabled = true;
|
|
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Vacuuming...';
|
|
|
|
// 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
|
|
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 = originalText;
|
|
});
|
|
}
|
|
|
|
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.textContent = message;
|
|
const closeButton = document.createElement('button');
|
|
closeButton.type = 'button';
|
|
closeButton.className = 'btn-close';
|
|
closeButton.setAttribute('data-bs-dismiss', 'alert');
|
|
closeButton.setAttribute('aria-label', 'Close');
|
|
toast.appendChild(closeButton);
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
// Auto-remove after 5 seconds
|
|
setTimeout(() => {
|
|
if (toast.parentNode) {
|
|
toast.parentNode.removeChild(toast);
|
|
}
|
|
}, 5000);
|
|
}
|
|
</script>
|
|
}
|
|
|
|
func formatTimestamp(unixTimestamp int64) string {
|
|
if unixTimestamp <= 0 {
|
|
return "Never"
|
|
}
|
|
t := time.Unix(unixTimestamp, 0)
|
|
return t.Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
func formatTTL(ttlEncoded uint32) string {
|
|
ttl := needle.LoadTTLFromUint32(ttlEncoded)
|
|
return ttl.String()
|
|
}
|