fix: EC UI template error when viewing shard details (#7955)
* fix: EC UI template error when viewing shard details Fixed field name mismatch in volume.html where it was using .ShardDetails instead of .Shards. Added a robust type conversion wrapper in templates.go to handle int64 to uint64 conversion for bytesToHumanReadable. Added regression test to ensure future stability. * refactor: improve bytesToHumanReadable and test robustness - Handled more integer types (uint32, int32, uint) in bytesToHumanReadable. - Improved volume_test.go to verify both shards are formatted correctly. * refactor: add bounds checking to bytesToHumanReadable Added checks for negative values in signed integer types to avoid incorrect formatting when converting to uint64. Addressed feedback from coderabbitai.
This commit is contained in:
@@ -3,16 +3,45 @@ package volume_server_ui
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
func percentFrom(total uint64, part_of uint64) string {
|
||||
return fmt.Sprintf("%.2f", (float64(part_of)/float64(total))*100)
|
||||
}
|
||||
|
||||
func bytesToHumanReadable(b interface{}) string {
|
||||
switch v := b.(type) {
|
||||
case uint64:
|
||||
return util.BytesToHumanReadable(v)
|
||||
case int64:
|
||||
if v < 0 {
|
||||
return fmt.Sprintf("%d B", v)
|
||||
}
|
||||
return util.BytesToHumanReadable(uint64(v))
|
||||
case int:
|
||||
if v < 0 {
|
||||
return fmt.Sprintf("%d B", v)
|
||||
}
|
||||
return util.BytesToHumanReadable(uint64(v))
|
||||
case uint32:
|
||||
return util.BytesToHumanReadable(uint64(v))
|
||||
case int32:
|
||||
if v < 0 {
|
||||
return fmt.Sprintf("%d B", v)
|
||||
}
|
||||
return util.BytesToHumanReadable(uint64(v))
|
||||
case uint:
|
||||
return util.BytesToHumanReadable(uint64(v))
|
||||
default:
|
||||
return fmt.Sprintf("%v", b)
|
||||
}
|
||||
}
|
||||
|
||||
func join(data []int64) string {
|
||||
var ret []string
|
||||
for _, d := range data {
|
||||
@@ -23,7 +52,7 @@ func join(data []int64) string {
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"join": join,
|
||||
"bytesToHumanReadable": util.BytesToHumanReadable,
|
||||
"bytesToHumanReadable": bytesToHumanReadable,
|
||||
"percentFrom": percentFrom,
|
||||
"isNotEmpty": util.IsNotEmpty,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>SeaweedFS {{ .Version }}</title>
|
||||
<link rel="stylesheet" href="/seaweedfsstatic/bootstrap/3.3.1/css/bootstrap.min.css">
|
||||
<script type="text/javascript" src="/seaweedfsstatic/javascript/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript"
|
||||
src="/seaweedfsstatic/javascript/jquery-sparklines/2.1.2/jquery.sparkline.min.js"></script>
|
||||
src="/seaweedfsstatic/javascript/jquery-sparklines/2.1.2/jquery.sparkline.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
var periods = ['second', 'minute', 'hour', 'day'];
|
||||
@@ -26,180 +27,184 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<a href="https://github.com/seaweedfs/seaweedfs"><img src="/seaweedfsstatic/seaweed50x50.png"></img></a>
|
||||
SeaweedFS <small>{{ .Version }}</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h2>Disk Stats</h2>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<a href="https://github.com/seaweedfs/seaweedfs"><img src="/seaweedfsstatic/seaweed50x50.png"></img></a>
|
||||
SeaweedFS <small>{{ .Version }}</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h2>Disk Stats</h2>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
<th>Disk</th>
|
||||
<th>Total</th>
|
||||
<th>Free</th>
|
||||
<th>Usage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .DiskStatuses }}
|
||||
<tr>
|
||||
<td>{{ .Dir }}</td>
|
||||
<td>{{ .DiskType }}</td>
|
||||
<td>{{ bytesToHumanReadable .All }}</td>
|
||||
<td>{{ bytesToHumanReadable .Free }}</td>
|
||||
<td>{{ percentFrom .All .Used}}%</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<h2>System Stats</h2>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr>
|
||||
<th>Masters</th>
|
||||
<td>{{.Masters}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Weekly # ReadRequests</th>
|
||||
<td><span class="inlinesparkline-day">{{ .Counters.ReadRequests.WeekCounter.ToList | join
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Daily # ReadRequests</th>
|
||||
<td><span class="inlinesparkline-hour">{{ .Counters.ReadRequests.DayCounter.ToList | join
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Hourly # ReadRequests</th>
|
||||
<td><span class="inlinesparkline-minute">{{ .Counters.ReadRequests.HourCounter.ToList | join
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Last Minute # ReadRequests</th>
|
||||
<td><span class="inlinesparkline-second">{{ .Counters.ReadRequests.MinuteCounter.ToList | join
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{{ range $key, $val := .Stats }}
|
||||
<tr>
|
||||
<th>{{ $key }}</th>
|
||||
<td>{{ $val }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h2>Volumes</h2>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
<th>Disk</th>
|
||||
<th>Total</th>
|
||||
<th>Free</th>
|
||||
<th>Usage</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Collection</th>
|
||||
<th>Disk</th>
|
||||
<th>Data Size</th>
|
||||
<th>Files</th>
|
||||
<th>Trash</th>
|
||||
<th>TTL</th>
|
||||
<th>ReadOnly</th>
|
||||
<th>Version</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .DiskStatuses }}
|
||||
<tr>
|
||||
<td>{{ .Dir }}</td>
|
||||
<td>{{ .DiskType }}</td>
|
||||
<td>{{ bytesToHumanReadable .All }}</td>
|
||||
<td>{{ bytesToHumanReadable .Free }}</td>
|
||||
<td>{{ percentFrom .All .Used}}%</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .Volumes }}
|
||||
<tr>
|
||||
<td><code>{{ .Id }}</code></td>
|
||||
<td>{{ .Collection }}</td>
|
||||
<td>{{ .DiskType }}</td>
|
||||
<td>{{ bytesToHumanReadable .Size }}</td>
|
||||
<td>{{ .FileCount }}</td>
|
||||
<td>{{ .DeleteCount }} / {{bytesToHumanReadable .DeletedByteCount}}</td>
|
||||
<td>{{ .Ttl }}</td>
|
||||
<td>{{ .ReadOnly }}</td>
|
||||
<td>{{ .Version }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<h2>System Stats</h2>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr>
|
||||
<th>Masters</th>
|
||||
<td>{{.Masters}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Weekly # ReadRequests</th>
|
||||
<td><span class="inlinesparkline-day">{{ .Counters.ReadRequests.WeekCounter.ToList | join }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Daily # ReadRequests</th>
|
||||
<td><span class="inlinesparkline-hour">{{ .Counters.ReadRequests.DayCounter.ToList | join }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Hourly # ReadRequests</th>
|
||||
<td><span
|
||||
class="inlinesparkline-minute">{{ .Counters.ReadRequests.HourCounter.ToList | join }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Last Minute # ReadRequests</th>
|
||||
<td><span
|
||||
class="inlinesparkline-second">{{ .Counters.ReadRequests.MinuteCounter.ToList | join }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{{ range $key, $val := .Stats }}
|
||||
<tr>
|
||||
<th>{{ $key }}</th>
|
||||
<td>{{ $val }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ if isNotEmpty .RemoteVolumes }}
|
||||
<div class="row">
|
||||
<h2>Remote Volumes</h2>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Collection</th>
|
||||
<th>Size</th>
|
||||
<th>Files</th>
|
||||
<th>Trash</th>
|
||||
<th>Remote</th>
|
||||
<th>Key</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .RemoteVolumes }}
|
||||
<tr>
|
||||
<td><code>{{ .Id }}</code></td>
|
||||
<td>{{ .Collection }}</td>
|
||||
<td>{{ bytesToHumanReadable .Size }}</td>
|
||||
<td>{{ .FileCount }}</td>
|
||||
<td>{{ .DeleteCount }} / {{bytesToHumanReadable .DeletedByteCount}}</td>
|
||||
<td>{{ .RemoteStorageName }}</td>
|
||||
<td>{{ .RemoteStorageKey }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="row">
|
||||
<h2>Volumes</h2>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Collection</th>
|
||||
<th>Disk</th>
|
||||
<th>Data Size</th>
|
||||
<th>Files</th>
|
||||
<th>Trash</th>
|
||||
<th>TTL</th>
|
||||
<th>ReadOnly</th>
|
||||
<th>Version</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Volumes }}
|
||||
<tr>
|
||||
<td><code>{{ .Id }}</code></td>
|
||||
<td>{{ .Collection }}</td>
|
||||
<td>{{ .DiskType }}</td>
|
||||
<td>{{ bytesToHumanReadable .Size }}</td>
|
||||
<td>{{ .FileCount }}</td>
|
||||
<td>{{ .DeleteCount }} / {{bytesToHumanReadable .DeletedByteCount}}</td>
|
||||
<td>{{ .Ttl }}</td>
|
||||
<td>{{ .ReadOnly }}</td>
|
||||
<td>{{ .Version }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{ if isNotEmpty .RemoteVolumes }}
|
||||
<div class="row">
|
||||
<h2>Remote Volumes</h2>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Collection</th>
|
||||
<th>Size</th>
|
||||
<th>Files</th>
|
||||
<th>Trash</th>
|
||||
<th>Remote</th>
|
||||
<th>Key</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .RemoteVolumes }}
|
||||
<tr>
|
||||
<td><code>{{ .Id }}</code></td>
|
||||
<td>{{ .Collection }}</td>
|
||||
<td>{{ bytesToHumanReadable .Size }}</td>
|
||||
<td>{{ .FileCount }}</td>
|
||||
<td>{{ .DeleteCount }} / {{bytesToHumanReadable .DeletedByteCount}}</td>
|
||||
<td>{{ .RemoteStorageName }}</td>
|
||||
<td>{{ .RemoteStorageKey }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if isNotEmpty .EcVolumes }}
|
||||
<div class="row">
|
||||
<h2>Erasure Coding Shards</h2>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Collection</th>
|
||||
<th>Total Size</th>
|
||||
<th>Shard Details</th>
|
||||
<th>CreatedAt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .EcVolumes }}
|
||||
<tr>
|
||||
<td><code>{{ .VolumeId }}</code></td>
|
||||
<td>{{ .Collection }}</td>
|
||||
<td>{{ bytesToHumanReadable .Size }}</td>
|
||||
<td>
|
||||
{{ range .ShardDetails }}
|
||||
<span class="label label-info" style="margin-right: 5px;">
|
||||
{{ .ShardId }}: {{ bytesToHumanReadable .Size }}
|
||||
</span>
|
||||
{{ if isNotEmpty .EcVolumes }}
|
||||
<div class="row">
|
||||
<h2>Erasure Coding Shards</h2>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Collection</th>
|
||||
<th>Total Size</th>
|
||||
<th>Shard Details</th>
|
||||
<th>CreatedAt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .EcVolumes }}
|
||||
<tr>
|
||||
<td><code>{{ .VolumeId }}</code></td>
|
||||
<td>{{ .Collection }}</td>
|
||||
<td>{{ bytesToHumanReadable .Size }}</td>
|
||||
<td>
|
||||
{{ range .Shards }}
|
||||
<span class="label label-info" style="margin-right: 5px;">
|
||||
{{ .ShardId }}: {{ bytesToHumanReadable .Size }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>{{ .CreatedAt.Format "2006-01-02 15:04" }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>{{ .CreatedAt.Format "2006-01-02 15:04" }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
76
weed/server/volume_server_ui/volume_test.go
Normal file
76
weed/server/volume_server_ui/volume_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package volume_server_ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/stats"
|
||||
)
|
||||
|
||||
func TestStatusTpl(t *testing.T) {
|
||||
args := struct {
|
||||
Version string
|
||||
Masters []string
|
||||
Volumes interface{}
|
||||
EcVolumes interface{}
|
||||
RemoteVolumes interface{}
|
||||
DiskStatuses interface{}
|
||||
Stats interface{}
|
||||
Counters *stats.ServerStats
|
||||
}{
|
||||
Version: "3.59",
|
||||
Masters: []string{"localhost:9333"},
|
||||
EcVolumes: []interface{}{
|
||||
struct {
|
||||
VolumeId uint32
|
||||
Collection string
|
||||
Size uint64
|
||||
Shards []interface{}
|
||||
CreatedAt time.Time
|
||||
}{
|
||||
VolumeId: 1,
|
||||
Collection: "ectest",
|
||||
Size: 8 * 1024 * 1024,
|
||||
Shards: []interface{}{
|
||||
struct {
|
||||
ShardId uint8
|
||||
Size int64
|
||||
}{
|
||||
ShardId: 4,
|
||||
Size: 1024 * 1024,
|
||||
},
|
||||
struct {
|
||||
ShardId uint8
|
||||
Size uint32
|
||||
}{
|
||||
ShardId: 6,
|
||||
Size: 1024 * 1024,
|
||||
},
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
Counters: stats.NewServerStats(),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := StatusTpl.Execute(&buf, args); err != nil {
|
||||
t.Logf("output: %s", buf.String())
|
||||
t.Fatalf("template execution error: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Contains(buf.Bytes(), []byte("8.00 MiB")) {
|
||||
t.Errorf("output does not contain formatted volume size '8.00 MiB'")
|
||||
}
|
||||
if bytes.Count(buf.Bytes(), []byte("1.00 MiB")) != 2 {
|
||||
t.Errorf("expected two shards of size '1.00 MiB', but they were not found or not formatted correctly")
|
||||
}
|
||||
|
||||
if !bytes.Contains(buf.Bytes(), []byte("Erasure Coding Shards")) {
|
||||
t.Errorf("output does not contain 'Erasure Coding Shards'")
|
||||
}
|
||||
if !bytes.Contains(buf.Bytes(), []byte("ectest")) {
|
||||
t.Errorf("output does not contain 'ectest'")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user