Files
seaweedfs/weed/admin/view/app/admin.templ
T
Chris Lu d47cc45b1f admin: fold dashboard sparklines into the existing cards (de-dup) (#9964)
admin: fold dashboard sparklines into the existing cards

The trend sparklines added in #9957 lived in a separate "Cluster Trends"
row that duplicated the existing summary cards (Volumes, Files, Disk Used,
EC Shards). Remove that row and instead render each sparkline inside the
matching summary card, so every headline number shows its recent trend
without duplication. The two maintenance metrics that have no existing
card — Active Tasks and Workers — now fill the previously-empty columns of
the EC row (also with sparklines).

DashboardTrends changes from a Cards slice to named per-card sparkline
SVGs (+ current values for the two maintenance cards). Drops the now-unused
trendBytes helper (disk size keeps using the existing formatBytes).
2026-06-14 14:17:43 -07:00

500 lines
25 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">
Total Files
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{formatNumber(data.TotalFiles)}
</div>
</div>
<div class="col-auto">
<i class="fas fa-file fa-2x text-gray-300"></i>
</div>
</div>
@templ.Raw(data.Trends.Files)
</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>
<!-- 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>
}