mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-17 12:46:59 +00:00
* admin: count each chunk once in the dashboard total The dashboard summed file_count from every node's volume list, so a chunk was counted once per replica and deleted chunks were never subtracted. Reuse the collection aggregation, which dedupes replicas and EC shard holders and nets out tombstones. * admin: the dashboard card counts chunks, so name it that Volumes store chunks, and a file is split into one or more of them, so the 'Total Files' card always read far higher than the number of files in the filer. Rename it to 'Total Chunks' and say so in the tooltip. * admin: collections pages count chunks once and say so The collections list and detail pages summed file_count straight off the topology, so replicas multiplied the count, tombstones stayed in it, and the detail page ignored EC volumes entirely. Take the numbers from the shared collection aggregation and label them chunks. * admin: dedupe replica chunk counts per volume instead of dividing Dividing each replica's live count by the copy count truncated a chunk per odd-sized volume, and reported half the count while a volume's second replica had not checked in yet. Replicas mirror each other's needles and deletes, so keep the fullest report per volume id. * admin: fix the collections CSV export column mapping The exporter read chunks from the EC-volume cell and shifted size and disk types with it. Read every column the table actually has.
528 lines
27 KiB
Templ
528 lines
27 KiB
Templ
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
)
|
|
|
|
templ Admin(data dash.AdminData) {
|
|
<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="fas fa-tachometer-alt me-2"></i>Dashboard
|
|
</h1>
|
|
<div class="btn-toolbar mb-2 mb-md-0">
|
|
<div class="btn-group me-2">
|
|
<a href={ dash.PUrl(ctx, "/object-store/buckets") } class="btn btn-sm btn-primary">
|
|
<i class="fas fa-cube me-1"></i>Object Store Buckets
|
|
</a>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="dashboard-content">
|
|
<!-- Summary Cards — each carries an inline sparkline of recent history
|
|
from the admin's own snapshots (no Prometheus required). -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-3 col-md-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">
|
|
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>
|
|
@templ.Raw(data.Trends.Volumes)
|
|
</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" title="Chunks stored in volumes. A file is split into one or more chunks, so this is not the number of files.">
|
|
Total Chunks
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{formatNumber(data.TotalChunks)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-cubes fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
@templ.Raw(data.Trends.Chunks)
|
|
</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 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-hdd fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
@templ.Raw(data.Trends.DiskUsed)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
Volume Size Limit
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d MB", data.VolumeSizeLimitMB)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-cogs fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Second Row for EC Shards Information -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-3 col-md-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">
|
|
EC Volumes
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalEcVolumes)}
|
|
</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-3 col-md-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">
|
|
EC Shards
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalEcShards)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-puzzle-piece fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
@templ.Raw(data.Trends.EcShards)
|
|
</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">
|
|
Active Tasks
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{ data.Trends.TasksValue }
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-tasks fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
@templ.Raw(data.Trends.Tasks)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
Workers
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{ data.Trends.WorkersValue }
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-users-cog fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
@templ.Raw(data.Trends.Workers)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mount Clients -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-plug me-2"></i>Mount Clients
|
|
</h6>
|
|
<a id="mountClientsDetailsLink" href="/cluster/mount-clients" class="btn btn-sm btn-outline-primary">
|
|
View Details <i class="fas fa-arrow-right ms-1"></i>
|
|
</a>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<i class="fas fa-plug fa-2x text-gray-300 me-3"></i>
|
|
<div>
|
|
<div class="h4 mb-0 font-weight-bold text-gray-800">{fmt.Sprintf("%d", data.TotalMountClients)}</div>
|
|
<div class="text-xs text-muted text-uppercase">Connected FUSE/VFS mount clients</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function(){var l=document.getElementById('mountClientsDetailsLink'); if(l){l.href=(window.__BASE_PATH__||'')+'/cluster/mount-clients';}})();
|
|
</script>
|
|
|
|
<!-- Master Nodes Status -->
|
|
<div class="row mb-4">
|
|
<div class="col-lg-4">
|
|
<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-server me-2"></i>Master Nodes
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered" width="100%" cellspacing="0">
|
|
<thead>
|
|
<tr>
|
|
<th>Address</th>
|
|
<th>Role</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, master := range data.MasterNodes {
|
|
<tr>
|
|
<td>
|
|
<a href={templ.SafeURL(fmt.Sprintf("//%s/ui/index.html", master.Address))} target="_blank">
|
|
{master.Address}
|
|
<i class="fas fa-external-link-alt ms-1 text-muted"></i>
|
|
</a>
|
|
</td>
|
|
<td>
|
|
if master.IsLeader {
|
|
<span class="badge bg-primary">Leader</span>
|
|
} else {
|
|
<span class="badge bg-secondary">Follower</span>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Health -->
|
|
<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-chart-pie me-2"></i>Cluster
|
|
</h6>
|
|
</div>
|
|
<div class="card-body text-center">
|
|
<div class="row">
|
|
<div class="col">
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<h5>{fmt.Sprintf("%d", len(data.MasterNodes))}</h5>
|
|
<small class="text-muted">Masters</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<h5>{fmt.Sprintf("%d", len(data.VolumeServers))}</h5>
|
|
<small class="text-muted">Volume Servers</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<h5>{fmt.Sprintf("%d", len(data.FilerNodes))}</h5>
|
|
<small class="text-muted">Filers</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<h5>{fmt.Sprintf("%d", len(data.S3Nodes))}</h5>
|
|
<small class="text-muted">S3 Servers</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<h5>{fmt.Sprintf("%d", len(data.MessageBrokers))}</h5>
|
|
<small class="text-muted">Message Brokers</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Volume Servers -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-database me-2"></i>Volume Servers
|
|
</h6>
|
|
<div class="dropdown no-arrow">
|
|
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i>
|
|
</a>
|
|
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in">
|
|
<div class="dropdown-header">Actions:</div>
|
|
<a class="dropdown-item" href={ dash.PUrl(ctx, "/volumes") }>View Details</a>
|
|
<a class="dropdown-item" href={ dash.PUrl(ctx, "/cluster") }>Topology View</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover" width="100%" cellspacing="0">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Address</th>
|
|
<th>Data Center</th>
|
|
<th>Rack</th>
|
|
<th>Volumes</th>
|
|
<th>EC Shards</th>
|
|
<th>Capacity</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, vs := range data.VolumeServers {
|
|
<tr>
|
|
<td>{vs.ID}</td>
|
|
<td>
|
|
<a href={templ.SafeURL(fmt.Sprintf("//%s/ui/index.html", vs.GetDisplayAddress()))} target="_blank">
|
|
{vs.GetDisplayAddress()}
|
|
<i class="fas fa-external-link-alt ms-1 text-muted"></i>
|
|
</a>
|
|
</td>
|
|
<td>{vs.DataCenter}</td>
|
|
<td>{vs.Rack}</td>
|
|
<td>
|
|
<div class="progress" style="height: 20px;">
|
|
<div class="progress-bar" role="progressbar"
|
|
style={fmt.Sprintf("width: %d%%", calculatePercent(vs.Volumes, vs.MaxVolumes))}>
|
|
{fmt.Sprintf("%d/%d", vs.Volumes, vs.MaxVolumes)}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
if vs.EcShards > 0 {
|
|
<span class="badge bg-info text-white me-1">{fmt.Sprintf("%d", vs.EcShards)}</span>
|
|
if vs.EcVolumes > 0 {
|
|
<small class="text-muted">({fmt.Sprintf("%d vol", vs.EcVolumes)})</small>
|
|
}
|
|
} else {
|
|
<span class="text-muted">-</span>
|
|
}
|
|
</td>
|
|
<td>{formatBytes(vs.DiskUsage)} / {formatBytes(vs.DiskCapacity)}</td>
|
|
</tr>
|
|
}
|
|
if len(data.VolumeServers) == 0 {
|
|
<tr>
|
|
<td colspan="7" class="text-center text-muted py-4">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
No volume servers found
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filer Nodes -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-folder me-2"></i>Filer Nodes
|
|
</h6>
|
|
<div class="dropdown no-arrow">
|
|
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i>
|
|
</a>
|
|
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in">
|
|
<div class="dropdown-header">Actions:</div>
|
|
<a class="dropdown-item" href={ dash.PUrl(ctx, "/filer") }>File Browser</a>
|
|
<a class="dropdown-item" href={ dash.PUrl(ctx, "/cluster") }>Topology View</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover" width="100%" cellspacing="0">
|
|
<thead>
|
|
<tr>
|
|
<th>Address</th>
|
|
<th>Data Center</th>
|
|
<th>Rack</th>
|
|
<th>Last Updated</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, filer := range data.FilerNodes {
|
|
<tr>
|
|
<td>
|
|
<a href={templ.SafeURL(fmt.Sprintf("//%s", filer.Address))} target="_blank">
|
|
{filer.Address}
|
|
<i class="fas fa-external-link-alt ms-1 text-muted"></i>
|
|
</a>
|
|
</td>
|
|
<td>{filer.DataCenter}</td>
|
|
<td>{filer.Rack}</td>
|
|
<td>{filer.LastUpdated.Format("2006-01-02 15:04:05")}</td>
|
|
</tr>
|
|
}
|
|
if len(data.FilerNodes) == 0 {
|
|
<tr>
|
|
<td colspan="4" class="text-center text-muted py-4">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
No filer nodes found
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- S3 Servers -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-cloud me-2"></i>S3 Servers
|
|
</h6>
|
|
<div class="dropdown no-arrow">
|
|
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i>
|
|
</a>
|
|
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in">
|
|
<div class="dropdown-header">Actions:</div>
|
|
<a class="dropdown-item" href={ dash.PUrl(ctx, "/cluster/s3") }>View Details</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover" width="100%" cellspacing="0">
|
|
<thead>
|
|
<tr>
|
|
<th>Address</th>
|
|
<th>Data Center</th>
|
|
<th>Last Updated</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, s3Node := range data.S3Nodes {
|
|
<tr>
|
|
<td>{s3Node.Address}</td>
|
|
<td>{s3Node.DataCenter}</td>
|
|
<td>{s3Node.LastUpdated.Format("2006-01-02 15:04:05")}</td>
|
|
</tr>
|
|
}
|
|
if len(data.S3Nodes) == 0 {
|
|
<tr>
|
|
<td colspan="3" class="text-center text-muted py-4">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
No S3 servers found
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</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>
|
|
</div>
|
|
} |