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.
 
 
 
 
 
 

497 lines
24 KiB

package app
import (
"fmt"
"time"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
)
templ VolumeDetails(data dash.VolumeDetailsData) {
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div>
<h1 class="h2">
<i class="fas fa-database me-2"></i>Volume Details
</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin" class="text-decoration-none">Dashboard</a></li>
<li class="breadcrumb-item"><a href="/cluster/volumes" class="text-decoration-none">Volumes</a></li>
<li class="breadcrumb-item active" aria-current="page">Volume {fmt.Sprintf("%d", data.Volume.Id)}</li>
</ol>
</nav>
</div>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="history.back()">
<i class="fas fa-arrow-left me-1"></i>Back
</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="window.location.reload()">
<i class="fas fa-refresh me-1"></i>Refresh
</button>
</div>
</div>
</div>
<div class="row">
<!-- Volume Information Card -->
<div class="col-lg-8">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-info-circle me-2"></i>Volume Information
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label"><strong>Volume ID:</strong></label>
<div><code class="fs-5">{fmt.Sprintf("%d", data.Volume.Id)}</code></div>
</div>
<div class="mb-3">
<label class="form-label"><strong>Server:</strong></label>
<div>
<a href={templ.SafeURL(fmt.Sprintf("http://%s/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={templ.SafeURL("/cluster/volumes?collection=default")} class="text-decoration-none">
<span class="badge bg-secondary">default</span>
</a>
} else {
<a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes?collection=%s", data.Volume.Collection))} class="text-decoration-none">
<span class="badge bg-secondary">{data.Volume.Collection}</span>
</a>
}
</div>
</div>
<div class="mb-3">
<label class="form-label"><strong>Replication:</strong></label>
<div><span class="badge bg-info">{fmt.Sprintf("%03d", data.Volume.ReplicaPlacement)}</span></div>
</div>
<div class="mb-3">
<label class="form-label"><strong>Disk Type:</strong></label>
<div>
<span class="badge bg-primary">
if data.Volume.DiskType == "" {
hdd
} else {
{data.Volume.DiskType}
}
</span>
</div>
</div>
<div class="mb-3">
<label class="form-label"><strong>Version:</strong></label>
<div><span class="badge bg-dark">{fmt.Sprintf("v%d", data.Volume.Version)}</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics Card -->
<div class="col-lg-4">
<!-- Volume Statistics & Health Card -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-pie me-2"></i>Volume Statistics & Health
</h6>
</div>
<div class="card-body">
<!-- Storage Metrics -->
<div class="row mb-3">
<div class="col-6">
<div class="text-center">
<div class="h4 mb-0 font-weight-bold text-success">
{formatBytes(int64(data.Volume.Size - data.Volume.DeletedByteCount))}
</div>
<small class="text-muted">Active Bytes</small>
</div>
</div>
<div class="col-6">
<div class="text-center">
<div class="h4 mb-0 font-weight-bold text-danger">
{formatBytes(int64(data.Volume.DeletedByteCount))}
</div>
<small class="text-muted">Deleted Bytes</small>
</div>
</div>
</div>
<!-- File Metrics -->
<div class="row mb-3">
<div class="col-6">
<div class="text-center">
<div class="h4 mb-0 font-weight-bold text-success">
{fmt.Sprintf("%d", data.Volume.FileCount)}
</div>
<small class="text-muted">Active Files</small>
</div>
</div>
<div class="col-6">
<div class="text-center">
<div class="h4 mb-0 font-weight-bold text-danger">
{fmt.Sprintf("%d", data.Volume.DeleteCount)}
</div>
<small class="text-muted">Deleted Files</small>
</div>
</div>
</div>
<!-- Storage Efficiency -->
if data.Volume.FileCount > 0 && data.Volume.Size > 0 {
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<small class="text-muted">Storage Efficiency</small>
<small class="text-muted">
{fmt.Sprintf("%.1f%%", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)}
</small>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-info" role="progressbar"
style={fmt.Sprintf("width: %.1f%%", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)}
aria-valuenow={fmt.Sprintf("%.1f", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)}
aria-valuemin="0" aria-valuemax="100">
</div>
</div>
</div>
}
<hr class="my-3">
<!-- Status & Configuration -->
<div class="row mb-3">
<div class="col-12">
<div class="text-center mb-2">
if data.Volume.ReadOnly {
<span class="badge bg-warning fs-6 px-3 py-2">
<i class="fas fa-lock me-1"></i>Read Only
</span>
if data.Volume.Size >= data.VolumeSizeLimit {
<div class="mt-1">
<small class="text-muted">Size limit exceeded</small>
</div>
}
} else if data.VolumeSizeLimit > data.Volume.Size {
<span class="badge bg-success fs-6 px-3 py-2">
<i class="fas fa-edit me-1"></i>Read/Write
</span>
} else {
<span class="badge bg-warning fs-6 px-3 py-2">
<i class="fas fa-exclamation-triangle me-1"></i>Size Limit Reached
</span>
}
</div>
</div>
</div>
<!-- Maintenance Info -->
<div class="row mb-3">
<div class="col-6">
<div class="text-center">
<div class="h6 mb-0 font-weight-bold text-info">
#{fmt.Sprintf("%d", data.Volume.CompactRevision)}
</div>
<small class="text-muted">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("http://%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("http://%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={templ.SafeURL(fmt.Sprintf("/cluster/volumes/%d/%s", replica.Id, replica.Server))} class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye me-1"></i>View
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
}
<!-- Actions Card -->
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-tools me-2"></i>Actions
</h6>
</div>
<div class="card-body">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-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>
</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);
});
}
});
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(`/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.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 formatTimestamp(unixTimestamp int64) string {
if unixTimestamp <= 0 {
return "Never"
}
t := time.Unix(unixTimestamp, 0)
return t.Format("2006-01-02 15:04:05")
}
func formatTTL(ttlSeconds uint32) string {
if ttlSeconds == 0 {
return "No TTL"
}
duration := time.Duration(ttlSeconds) * time.Second
// Convert to human readable format
days := int(duration.Hours()) / 24
hours := int(duration.Hours()) % 24
minutes := int(duration.Minutes()) % 60
if days > 0 {
if hours > 0 {
return fmt.Sprintf("%dd %dh", days, hours)
}
return fmt.Sprintf("%d days", days)
} else if hours > 0 {
if minutes > 0 {
return fmt.Sprintf("%dh %dm", hours, minutes)
}
return fmt.Sprintf("%d hours", hours)
} else if minutes > 0 {
return fmt.Sprintf("%d minutes", minutes)
} else {
return fmt.Sprintf("%d seconds", int(duration.Seconds()))
}
}