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:
Chris Lu
2026-01-03 22:45:48 -08:00
committed by GitHub
parent 0647bc24d5
commit 63b2fe0d76
3 changed files with 275 additions and 165 deletions

View File

@@ -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,
}

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<title>SeaweedFS {{ .Version }}</title>
<link rel="stylesheet" href="/seaweedfsstatic/bootstrap/3.3.1/css/bootstrap.min.css">
@@ -26,6 +27,7 @@
}
</style>
</head>
<body>
<div class="container">
<div class="page-header">
@@ -71,24 +73,26 @@
</tr>
<tr>
<th>Weekly # ReadRequests</th>
<td><span class="inlinesparkline-day">{{ .Counters.ReadRequests.WeekCounter.ToList | join }}</span>
<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><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><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><span class="inlinesparkline-second">{{ .Counters.ReadRequests.MinuteCounter.ToList | join
}}</span>
</td>
</tr>
{{ range $key, $val := .Stats }}
@@ -187,7 +191,7 @@
<td>{{ .Collection }}</td>
<td>{{ bytesToHumanReadable .Size }}</td>
<td>
{{ range .ShardDetails }}
{{ range .Shards }}
<span class="label label-info" style="margin-right: 5px;">
{{ .ShardId }}: {{ bytesToHumanReadable .Size }}
</span>
@@ -202,4 +206,5 @@
{{ end }}
</div>
</body>
</html>

View 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'")
}
}