mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-07 20:47:46 +01:00
* admin: show disk usage on the dashboard instead of the data size The dashboard's Total Size card summed the volume and EC shard sizes the volume servers report. That leaves out deleted needles waiting for vacuum, EC generations kept after a vacuum, and anything else on the disks, so on a cluster with a lot of EC vacuum activity it showed far less than the disks held. The card now shows the bytes in use on the volume servers' disks, the same statfs numbers the volume server list uses, with the disks' capacity, and its trend line follows that number. Servers that report no statfs numbers count their data size, as on the volume server list. Claude-Session: https://claude.ai/code/session_011UpvafcfUuC2mLxNAXLQJi * admin: compute the disk usage percentage in float64 current*100 overflows int64 past about 92 PB of used space, so the dashboard card showed a negative percentage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: size the volume server disk bar with calculatePercent64 int(host.DiskUsage) truncates byte counts on 32-bit builds, so the bar width was wrong for any disk over 2 GB there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: test the cluster disk totals across statfs and fallback servers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
426 lines
24 KiB
Templ
426 lines
24 KiB
Templ
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
)
|
|
|
|
templ ClusterVolumeServers(data dash.ClusterVolumeServersData) {
|
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
|
<h1 class="h2">
|
|
<i class="bi bi-server me-2"></i>Volume Servers
|
|
</h1>
|
|
<div class="btn-toolbar mb-2 mb-md-0">
|
|
<div class="btn-group me-2">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="exportVolumeServers()">
|
|
<i class="bi bi-download me-1"></i>Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="hosts-content">
|
|
<!-- Summary Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-3 col-md-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 Volume Servers
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalVolumeServers)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="bi bi-server icon-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="col-xl-3 col-md-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">
|
|
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="bi bi-database icon-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-3 col-md-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 Capacity
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{formatBytes(data.TotalCapacity)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="bi bi-device-hdd icon-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hosts Table -->
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="bi bi-server me-2"></i>Volume Servers
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
if len(data.VolumeServers) > 0 {
|
|
<div class="table-responsive">
|
|
<table class="table table-hover" id="hostsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Address</th>
|
|
<th>Data Center</th>
|
|
<th>Rack</th>
|
|
<th>Version</th>
|
|
<th>Volumes</th>
|
|
<th>Max Volumes</th>
|
|
<th>EC Shards</th>
|
|
<th>Capacity</th>
|
|
<th>Usage</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, host := range data.VolumeServers {
|
|
<tr>
|
|
<td>
|
|
if host.PublicURL != "" {
|
|
<a href={templ.SafeURL(fmt.Sprintf("//%s/ui/index.html", host.PublicURL))} target="_blank" class="text-decoration-none">
|
|
{host.Address}
|
|
<i class="bi bi-box-arrow-up-right ms-1 text-muted"></i>
|
|
</a>
|
|
} else {
|
|
<a href={templ.SafeURL(fmt.Sprintf("//%s/ui/index.html", host.Address))} target="_blank" class="text-decoration-none">
|
|
{host.Address}
|
|
<i class="bi bi-box-arrow-up-right ms-1 text-muted"></i>
|
|
</a>
|
|
}
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-light text-dark">{host.DataCenter}</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-light text-dark">{host.Rack}</span>
|
|
</td>
|
|
<td title={host.Version}>
|
|
{dash.ShortVersion(host.Version)}
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="progress me-2" style="width: 60px; height: 16px;">
|
|
<div class="progress-bar" role="progressbar"
|
|
style={fmt.Sprintf("width: %d%%", calculatePercent(host.Volumes, host.MaxVolumes))}>
|
|
</div>
|
|
</div>
|
|
<span class="badge bg-primary">{fmt.Sprintf("%d", host.Volumes)}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary">{fmt.Sprintf("%d", host.MaxVolumes)}</span>
|
|
</td>
|
|
<td>
|
|
if host.EcShards > 0 {
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-layers me-1 text-info"></i>
|
|
<span class="badge bg-info text-white me-1">{fmt.Sprintf("%d", host.EcShards)}</span>
|
|
<small class="text-muted">shards</small>
|
|
</div>
|
|
if host.EcVolumes > 0 {
|
|
<div class="mt-1">
|
|
<small class="text-muted">{fmt.Sprintf("%d EC volumes", host.EcVolumes)}</small>
|
|
</div>
|
|
}
|
|
} else {
|
|
<span class="text-muted">-</span>
|
|
}
|
|
</td>
|
|
<td>{formatBytes(host.DiskCapacity)}</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="progress me-2" style="width: 60px; height: 16px;">
|
|
<div class="progress-bar" role="progressbar"
|
|
style={fmt.Sprintf("width: %d%%", calculatePercent64(host.DiskUsage, host.DiskCapacity))}>
|
|
</div>
|
|
</div>
|
|
<small>{formatBytes(host.DiskUsage)}</small>
|
|
</div>
|
|
if host.RemoteSize > 0 {
|
|
<div class="mt-1">
|
|
<small class="text-muted"><i class="bi bi-cloud me-1"></i>{formatBytes(host.RemoteSize)} remote</small>
|
|
</div>
|
|
}
|
|
</td>
|
|
<td>
|
|
if host.PublicURL != "" {
|
|
<button type="button"
|
|
class="btn btn-outline-primary btn-sm"
|
|
title="View Details"
|
|
data-action="view-details"
|
|
data-id={host.ID}
|
|
data-address={host.Address}
|
|
data-public-url={host.PublicURL}
|
|
data-datacenter={host.DataCenter}
|
|
data-rack={host.Rack}
|
|
data-volumes={fmt.Sprintf("%d", host.Volumes)}
|
|
data-max-volumes={fmt.Sprintf("%d", host.MaxVolumes)}
|
|
data-disk-usage={fmt.Sprintf("%d", host.DiskUsage)}
|
|
data-disk-capacity={fmt.Sprintf("%d", host.DiskCapacity)}
|
|
data-remote-size={fmt.Sprintf("%d", host.RemoteSize)}
|
|
data-ec-volumes={fmt.Sprintf("%d", host.EcVolumes)}
|
|
data-ec-shards={fmt.Sprintf("%d", host.EcShards)}
|
|
data-last-heartbeat={host.LastHeartbeat.Format("2006-01-02 15:04:05")}>
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
} else {
|
|
<button type="button"
|
|
class="btn btn-outline-primary btn-sm"
|
|
title="View Details"
|
|
data-action="view-details"
|
|
data-id={host.ID}
|
|
data-address={host.Address}
|
|
data-public-url={host.Address}
|
|
data-datacenter={host.DataCenter}
|
|
data-rack={host.Rack}
|
|
data-volumes={fmt.Sprintf("%d", host.Volumes)}
|
|
data-max-volumes={fmt.Sprintf("%d", host.MaxVolumes)}
|
|
data-disk-usage={fmt.Sprintf("%d", host.DiskUsage)}
|
|
data-disk-capacity={fmt.Sprintf("%d", host.DiskCapacity)}
|
|
data-remote-size={fmt.Sprintf("%d", host.RemoteSize)}
|
|
data-ec-volumes={fmt.Sprintf("%d", host.EcVolumes)}
|
|
data-ec-shards={fmt.Sprintf("%d", host.EcShards)}
|
|
data-last-heartbeat={host.LastHeartbeat.Format("2006-01-02 15:04:05")}>
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
} else {
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-server icon-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No Volume Servers Found</h5>
|
|
<p class="text-muted">No volume servers 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="bi bi-clock me-1"></i>
|
|
Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JavaScript for cluster volume servers functionality -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Handle volume server action buttons
|
|
document.addEventListener('click', function(e) {
|
|
const button = e.target.closest('[data-action]');
|
|
if (!button) return;
|
|
|
|
const action = button.getAttribute('data-action');
|
|
|
|
switch(action) {
|
|
case 'view-details':
|
|
const serverData = {
|
|
id: button.getAttribute('data-id'),
|
|
address: button.getAttribute('data-address'),
|
|
publicUrl: button.getAttribute('data-public-url'),
|
|
datacenter: button.getAttribute('data-datacenter'),
|
|
rack: button.getAttribute('data-rack'),
|
|
volumes: parseInt(button.getAttribute('data-volumes')),
|
|
maxVolumes: parseInt(button.getAttribute('data-max-volumes')),
|
|
diskUsage: parseInt(button.getAttribute('data-disk-usage')),
|
|
diskCapacity: parseInt(button.getAttribute('data-disk-capacity')),
|
|
remoteSize: parseInt(button.getAttribute('data-remote-size')) || 0,
|
|
ecVolumes: parseInt(button.getAttribute('data-ec-volumes')),
|
|
ecShards: parseInt(button.getAttribute('data-ec-shards')),
|
|
lastHeartbeat: button.getAttribute('data-last-heartbeat')
|
|
};
|
|
showVolumeServerDetails(serverData);
|
|
break;
|
|
}
|
|
});
|
|
});
|
|
|
|
function showVolumeServerDetails(server) {
|
|
const volumePercent = server.maxVolumes > 0 ? Math.round((server.volumes / server.maxVolumes) * 100) : 0;
|
|
const diskPercent = server.diskCapacity > 0 ? Math.round((server.diskUsage / server.diskCapacity) * 100) : 0;
|
|
|
|
const modalHtml = '<div class="modal fade" id="volumeServerDetailsModal" tabindex="-1">' +
|
|
'<div class="modal-dialog modal-lg">' +
|
|
'<div class="modal-content">' +
|
|
'<div class="modal-header">' +
|
|
'<h5 class="modal-title"><i class="bi bi-server me-2"></i>Volume Server Details: ' + server.address + '</h5>' +
|
|
'<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
|
|
'</div>' +
|
|
'<div class="modal-body">' +
|
|
'<div class="row">' +
|
|
'<div class="col-md-6">' +
|
|
'<h6 class="text-primary"><i class="bi bi-info-circle me-1"></i>Basic Information</h6>' +
|
|
'<table class="table table-sm">' +
|
|
'<tr><td><strong>Server ID:</strong></td><td><code>' + server.id + '</code></td></tr>' +
|
|
'<tr><td><strong>Address:</strong></td><td>' + server.address + '</td></tr>' +
|
|
'<tr><td><strong>Public URL:</strong></td><td>' + server.publicUrl + '</td></tr>' +
|
|
'<tr><td><strong>Data Center:</strong></td><td><span class="badge bg-light text-dark">' + server.datacenter + '</span></td></tr>' +
|
|
'<tr><td><strong>Rack:</strong></td><td><span class="badge bg-light text-dark">' + server.rack + '</span></td></tr>' +
|
|
'<tr><td><strong>Last Heartbeat:</strong></td><td>' + server.lastHeartbeat + '</td></tr>' +
|
|
'</table>' +
|
|
'</div>' +
|
|
'<div class="col-md-6">' +
|
|
'<h6 class="text-primary"><i class="bi bi-bar-chart me-1"></i>Usage Statistics</h6>' +
|
|
'<table class="table table-sm">' +
|
|
'<tr><td><strong>Volumes:</strong></td><td>' +
|
|
'<div class="d-flex align-items-center">' +
|
|
'<div class="progress me-2" style="width: 100px; height: 20px;">' +
|
|
'<div class="progress-bar" role="progressbar" style="width: ' + volumePercent + '%"></div>' +
|
|
'</div>' +
|
|
'<span>' + server.volumes + '/' + server.maxVolumes + ' (' + volumePercent + '%)</span>' +
|
|
'</div>' +
|
|
'</td></tr>' +
|
|
'<tr><td><strong>Disk Usage:</strong></td><td>' +
|
|
'<div class="d-flex align-items-center">' +
|
|
'<div class="progress me-2" style="width: 100px; height: 20px;">' +
|
|
'<div class="progress-bar" role="progressbar" style="width: ' + diskPercent + '%"></div>' +
|
|
'</div>' +
|
|
'<span>' + formatBytes(server.diskUsage) + '/' + formatBytes(server.diskCapacity) + ' (' + diskPercent + '%)</span>' +
|
|
'</div>' +
|
|
'</td></tr>' +
|
|
'<tr><td><strong>Available Space:</strong></td><td>' + formatBytes(server.diskCapacity - server.diskUsage) + '</td></tr>' +
|
|
(server.remoteSize > 0 ?
|
|
'<tr><td><strong>Remote Data:</strong></td><td><i class="bi bi-cloud me-1"></i>' + formatBytes(server.remoteSize) + '</td></tr>' : '') +
|
|
'</table>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
|
|
// Add EC Shard information if available
|
|
(server.ecShards > 0 ?
|
|
'<div class="row mt-3">' +
|
|
'<div class="col-12">' +
|
|
'<h6 class="text-primary"><i class="bi bi-layers me-1"></i>Erasure Coding Information</h6>' +
|
|
'<table class="table table-sm">' +
|
|
'<tr><td><strong>EC Volumes:</strong></td><td><span class="badge bg-info text-white">' + server.ecVolumes + '</span></td></tr>' +
|
|
'<tr><td><strong>EC Shards:</strong></td><td><span class="badge bg-info text-white">' + server.ecShards + '</span></td></tr>' +
|
|
'</table>' +
|
|
'</div>' +
|
|
'</div>' : '') +
|
|
|
|
'<div class="row mt-3">' +
|
|
'<div class="col-12">' +
|
|
'<h6 class="text-primary"><i class="bi bi-link me-1"></i>Quick Actions</h6>' +
|
|
'<div class="d-grid gap-2 d-md-flex">' +
|
|
'<a href="//' + (server.publicUrl || server.address) + '/ui/index.html" target="_blank" class="btn btn-outline-primary">' +
|
|
'<i class="bi bi-box-arrow-up-right me-1"></i>Open Volume Server UI' +
|
|
'</a>' +
|
|
'<a href="' + (window.__BASE_PATH__ || '') + '/storage/volumes?server=' + encodeURIComponent(server.address) + '" class="btn btn-outline-info">' +
|
|
'<i class="bi bi-database me-1"></i>View Volumes' +
|
|
'</a>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="modal-footer">' +
|
|
'<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// Remove existing modal if present
|
|
const existingModal = document.getElementById('volumeServerDetailsModal');
|
|
if (existingModal) {
|
|
existingModal.remove();
|
|
}
|
|
|
|
// Add modal to body and show
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
const modal = new bootstrap.Modal(document.getElementById('volumeServerDetailsModal'));
|
|
modal.show();
|
|
|
|
// Remove modal when hidden
|
|
document.getElementById('volumeServerDetailsModal').addEventListener('hidden.bs.modal', function() {
|
|
this.remove();
|
|
});
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function exportVolumeServers() {
|
|
// Simple CSV export of volume servers list
|
|
const rows = Array.from(document.querySelectorAll('#hostsTable tbody tr')).map(row => {
|
|
const cells = row.querySelectorAll('td');
|
|
if (cells.length > 1) {
|
|
return {
|
|
id: cells[0].textContent.trim(),
|
|
datacenter: cells[1].textContent.trim(),
|
|
rack: cells[2].textContent.trim(),
|
|
version: cells[3].textContent.trim(),
|
|
volumes: cells[4].textContent.trim(),
|
|
capacity: cells[7].textContent.trim(),
|
|
usage: cells[8].textContent.trim()
|
|
};
|
|
}
|
|
return null;
|
|
}).filter(row => row !== null);
|
|
|
|
const csvContent = "data:text/csv;charset=utf-8," +
|
|
"Server ID,Data Center,Rack,Version,Volumes,Capacity,Usage\n" +
|
|
rows.map(r => '"' + r.id + '","' + r.datacenter + '","' + r.rack + '","' + r.version + '","' + r.volumes + '","' + r.capacity + '","' + r.usage + '"').join("\n");
|
|
|
|
const encodedUri = encodeURI(csvContent);
|
|
const link = document.createElement("a");
|
|
link.setAttribute("href", encodedUri);
|
|
link.setAttribute("download", "volume_servers.csv");
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
</script>
|
|
}
|
|
|
|
|