| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- package app
- import (
- "fmt"
- "github.com/seaweedfs/seaweedfs/weed/admin/dash"
- )
- templ EcVolumeDetails(data dash.EcVolumeDetailsData) {
- <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-th-large me-2"></i>EC 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/ec-shards" class="text-decoration-none">EC Volumes</a></li>
- <li class="breadcrumb-item active" aria-current="page">Volume {fmt.Sprintf("%d", data.VolumeID)}</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>
- <!-- EC Volume Summary -->
- <div class="row mb-4">
- <div class="col-md-6">
- <div class="card">
- <div class="card-header">
- <h5 class="card-title mb-0">
- <i class="fas fa-info-circle me-2"></i>Volume Information
- </h5>
- </div>
- <div class="card-body">
- <table class="table table-borderless">
- <tr>
- <td><strong>Volume ID:</strong></td>
- <td>{fmt.Sprintf("%d", data.VolumeID)}</td>
- </tr>
- <tr>
- <td><strong>Collection:</strong></td>
- <td>
- if data.Collection != "" {
- <span class="badge bg-info">{data.Collection}</span>
- } else {
- <span class="text-muted">default</span>
- }
- </td>
- </tr>
- <tr>
- <td><strong>Status:</strong></td>
- <td>
- if data.IsComplete {
- <span class="badge bg-success">
- <i class="fas fa-check me-1"></i>Complete ({data.TotalShards}/14 shards)
- </span>
- } else {
- <span class="badge bg-warning">
- <i class="fas fa-exclamation-triangle me-1"></i>Incomplete ({data.TotalShards}/14 shards)
- </span>
- }
- </td>
- </tr>
- if !data.IsComplete {
- <tr>
- <td><strong>Missing Shards:</strong></td>
- <td>
- for i, shardID := range data.MissingShards {
- if i > 0 {
- <span>, </span>
- }
- <span class="badge bg-danger">{fmt.Sprintf("%02d", shardID)}</span>
- }
- </td>
- </tr>
- }
- <tr>
- <td><strong>Data Centers:</strong></td>
- <td>
- for i, dc := range data.DataCenters {
- if i > 0 {
- <span>, </span>
- }
- <span class="badge bg-primary">{dc}</span>
- }
- </td>
- </tr>
- <tr>
- <td><strong>Servers:</strong></td>
- <td>
- <span class="text-muted">{fmt.Sprintf("%d servers", len(data.Servers))}</span>
- </td>
- </tr>
- <tr>
- <td><strong>Last Updated:</strong></td>
- <td>
- <span class="text-muted">{data.LastUpdated.Format("2006-01-02 15:04:05")}</span>
- </td>
- </tr>
- </table>
- </div>
- </div>
- </div>
-
- <div class="col-md-6">
- <div class="card">
- <div class="card-header">
- <h5 class="card-title mb-0">
- <i class="fas fa-chart-pie me-2"></i>Shard Distribution
- </h5>
- </div>
- <div class="card-body">
- <div class="row text-center">
- <div class="col-4">
- <div class="border rounded p-3">
- <h3 class="text-primary mb-1">{fmt.Sprintf("%d", data.TotalShards)}</h3>
- <small class="text-muted">Total Shards</small>
- </div>
- </div>
- <div class="col-4">
- <div class="border rounded p-3">
- <h3 class="text-success mb-1">{fmt.Sprintf("%d", len(data.DataCenters))}</h3>
- <small class="text-muted">Data Centers</small>
- </div>
- </div>
- <div class="col-4">
- <div class="border rounded p-3">
- <h3 class="text-info mb-1">{fmt.Sprintf("%d", len(data.Servers))}</h3>
- <small class="text-muted">Servers</small>
- </div>
- </div>
- </div>
-
- <!-- Shard Distribution Visualization -->
- <div class="mt-3">
- <h6>Present Shards:</h6>
- <div class="d-flex flex-wrap gap-1">
- for _, shard := range data.Shards {
- <span class="badge bg-success me-1 mb-1">{fmt.Sprintf("%02d", shard.ShardID)}</span>
- }
- </div>
- if len(data.MissingShards) > 0 {
- <h6 class="mt-2">Missing Shards:</h6>
- <div class="d-flex flex-wrap gap-1">
- for _, shardID := range data.MissingShards {
- <span class="badge bg-secondary me-1 mb-1">{fmt.Sprintf("%02d", shardID)}</span>
- }
- </div>
- }
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- Shard Details Table -->
- <div class="card">
- <div class="card-header">
- <h5 class="card-title mb-0">
- <i class="fas fa-list me-2"></i>Shard Details
- </h5>
- </div>
- <div class="card-body">
- if len(data.Shards) > 0 {
- <div class="table-responsive">
- <table class="table table-striped table-hover">
- <thead>
- <tr>
- <th>
- <a href="#" onclick="sortBy('shard_id')" class="text-dark text-decoration-none">
- Shard ID
- if data.SortBy == "shard_id" {
- if data.SortOrder == "asc" {
- <i class="fas fa-sort-up ms-1"></i>
- } else {
- <i class="fas fa-sort-down ms-1"></i>
- }
- } else {
- <i class="fas fa-sort ms-1 text-muted"></i>
- }
- </a>
- </th>
- <th>
- <a href="#" onclick="sortBy('server')" class="text-dark text-decoration-none">
- Server
- if data.SortBy == "server" {
- if data.SortOrder == "asc" {
- <i class="fas fa-sort-up ms-1"></i>
- } else {
- <i class="fas fa-sort-down ms-1"></i>
- }
- } else {
- <i class="fas fa-sort ms-1 text-muted"></i>
- }
- </a>
- </th>
- <th>
- <a href="#" onclick="sortBy('data_center')" class="text-dark text-decoration-none">
- Data Center
- if data.SortBy == "data_center" {
- if data.SortOrder == "asc" {
- <i class="fas fa-sort-up ms-1"></i>
- } else {
- <i class="fas fa-sort-down ms-1"></i>
- }
- } else {
- <i class="fas fa-sort ms-1 text-muted"></i>
- }
- </a>
- </th>
- <th>
- <a href="#" onclick="sortBy('rack')" class="text-dark text-decoration-none">
- Rack
- if data.SortBy == "rack" {
- if data.SortOrder == "asc" {
- <i class="fas fa-sort-up ms-1"></i>
- } else {
- <i class="fas fa-sort-down ms-1"></i>
- }
- } else {
- <i class="fas fa-sort ms-1 text-muted"></i>
- }
- </a>
- </th>
- <th class="text-dark">Disk Type</th>
- <th class="text-dark">Shard Size</th>
- <th class="text-dark">Actions</th>
- </tr>
- </thead>
- <tbody>
- for _, shard := range data.Shards {
- <tr>
- <td>
- <span class="badge bg-primary">{fmt.Sprintf("%02d", shard.ShardID)}</span>
- </td>
- <td>
- <a href={ templ.URL("/cluster/volume-servers/" + shard.Server) } class="text-primary text-decoration-none">
- <code class="small">{shard.Server}</code>
- </a>
- </td>
- <td>
- <span class="badge bg-primary text-white">{shard.DataCenter}</span>
- </td>
- <td>
- <span class="badge bg-secondary text-white">{shard.Rack}</span>
- </td>
- <td>
- <span class="text-dark">{shard.DiskType}</span>
- </td>
- <td>
- <span class="text-success">{bytesToHumanReadableUint64(shard.Size)}</span>
- </td>
- <td>
- <a href={ templ.SafeURL(fmt.Sprintf("http://%s/ui/index.html", shard.Server)) } target="_blank" class="btn btn-sm btn-primary">
- <i class="fas fa-external-link-alt me-1"></i>Volume Server
- </a>
- </td>
- </tr>
- }
- </tbody>
- </table>
- </div>
- } else {
- <div class="text-center py-4">
- <i class="fas fa-exclamation-triangle fa-3x text-warning mb-3"></i>
- <h5>No EC shards found</h5>
- <p class="text-muted">This volume may not be EC encoded yet.</p>
- </div>
- }
- </div>
- </div>
-
- <script>
- // Sorting functionality
- function sortBy(field) {
- const currentSort = new URLSearchParams(window.location.search).get('sort_by');
- const currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';
-
- let newOrder = 'asc';
- if (currentSort === field && currentOrder === 'asc') {
- newOrder = 'desc';
- }
-
- const url = new URL(window.location);
- url.searchParams.set('sort_by', field);
- url.searchParams.set('sort_order', newOrder);
- window.location.href = url.toString();
- }
- </script>
- }
- // Helper function to convert bytes to human readable format (uint64 version)
- func bytesToHumanReadableUint64(bytes uint64) string {
- const unit = 1024
- if bytes < unit {
- return fmt.Sprintf("%dB", bytes)
- }
- div, exp := uint64(unit), 0
- for n := bytes / unit; n >= unit; n /= unit {
- div *= unit
- exp++
- }
- return fmt.Sprintf("%.1f%cB", float64(bytes)/float64(div), "KMGTPE"[exp])
- }
|