feat(panel): show snapshot disk usage in a wider time-machine modal

Incus's snapshot API already returns each snapshot's size, so the overview
now renders a Size column (via a newly exported HumanBytes) and the time
machine modal is bumped from 420px to the wide layout. The per-row value is
greeted only when snapshots exist; the header ships in the template.

Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
bluebluesoda committed 2026-08-31 21:54:12 +08:00
1 parent f79ccb09f0
commit 4bc161c379
4 files changed
+28 -8

No files matched your search

+4
View File
@@ -212,6 +212,10 @@ func resourceStateName(state int) string {
}
}
// HumanBytes renders a byte count as a short human string (e.g. "184 MiB").
// Exportable because the panel shows snapshot sizes.
func HumanBytes(b int64) string { return humanBytes(b) }
// humanBytes renders a byte count as a short human string (e.g. "184 MiB").
func humanBytes(b int64) string {
const unit = 1024
+4 -3
View File
@@ -114,6 +114,7 @@ type domainRow struct {
type snapshotRow struct {
Name string
CreatedAt string // UTC RFC3339; rendered in the browser's timezone
Size string // human-readable disk usage, e.g. "184 MiB"; may be empty
}
// sshKeyRow is one public key shown in the SSH-key management panel.
@@ -368,9 +369,9 @@ func (s *Server) buildData(u *db.User, msg, errMsg string) pageData {
// Snapshots come from Incus (one list call). A failure is non-fatal: the
// page still renders, and the snapshot modal shows the empty state.
if snaps, err := s.mgr.SnapshotList(u.Name); err == nil {
for _, sn := range snaps {
d.Snapshots = append(d.Snapshots, snapshotRow{Name: sn.Name, CreatedAt: sn.CreatedAt})
}
for _, sn := range snaps {
d.Snapshots = append(d.Snapshots, snapshotRow{Name: sn.Name, CreatedAt: sn.CreatedAt, Size: mgr.HumanBytes(sn.Size)})
}
}
d.SnapshotLimit = s.mgr.SnapshotLimit()
keys, _ := s.db.ListSSHKeys(u.ID)
+17 -3
View File
@@ -1019,9 +1019,23 @@ func TestOverviewSnapshotModal(t *testing.T) {
t.Errorf("overview missing %q", want)
}
}
// Size column must NOT be rendered.
if strings.Contains(body, ">Size<") || strings.Contains(body, ">大小<") {
t.Error("snapshot size column should not be rendered")
// With the mocked manager the snapshot list on this page is empty, so the
// table would not be rendered here. Verify the shipped template carries the
// size column header and a per-row size cell; the table body is guarded by
// {{if .Snapshots}}, so this checks the template source itself.
src, err := tmplFS.ReadFile("templates/overview.html")
if err != nil {
t.Fatal(err)
}
tpl := string(src)
// The header is a language-conditional template:
// {{if eq .Lang "zh"}}大小{{else}}Size{{end}}</th>
// Check the whole conditional so the language branch change doesn't pass.
if !strings.Contains(tpl, "大小{{else}}Size{{end}}</th>") && !strings.Contains(tpl, "Size{{else}}") {
t.Error("snapshot size column header should be present in overview.html")
}
if !strings.Contains(tpl, ".Size") {
t.Error("snapshot size cell should be rendered per row in overview.html")
}
// The snapshot create button must be inside the modal.
iModal := strings.Index(body, `id="snapModal"`)
+3 -2
View File
@@ -411,7 +411,7 @@ footer a{color:var(--muted);text-decoration:underline}
</div>
<div class="overlay" id="snapModal">
<div class="modal">
<div class="modal modal-wide">
<h3>{{if eq .Lang "zh"}}时光机{{else}}Time machine{{end}}</h3>
<p class="muted">{{if eq .Lang "zh"}}时光机只能回到过去,不能前往未来。恢复到某个检查点会丢失当前数据,以及在此之后创建的所有检查点。每容器最多 {{.SnapshotLimit}} 个检查点。{{else}}A time machine only goes back in time. Restoring to a checkpoint loses the current data and every checkpoint created after it. Up to {{.SnapshotLimit}} per container.{{end}}</p>
<div class="btnrow">
@@ -419,12 +419,13 @@ footer a{color:var(--muted);text-decoration:underline}
</div>
{{if .Snapshots}}
<table class="domtable">
<thead><tr><th>{{if eq .Lang "zh"}}检查点{{else}}Checkpoint{{end}}</th><th>{{if eq .Lang "zh"}}创建时间(本地时区){{else}}Created (local){{end}}</th><th></th></tr></thead>
<thead><tr><th>{{if eq .Lang "zh"}}检查点{{else}}Checkpoint{{end}}</th><th>{{if eq .Lang "zh"}}创建时间(本地时区){{else}}Created (local){{end}}</th><th>{{if eq .Lang "zh"}}大小{{else}}Size{{end}}</th><th></th></tr></thead>
<tbody>
{{range .Snapshots}}
<tr>
<td><code>{{.Name}}</code></td>
<td><span class="snapts" data-ts="{{.CreatedAt}}">{{.CreatedAt}}</span></td>
<td><span class="muted">{{.Size}}</span></td>
<td>
<button type="button" class="btn accent" onclick="restoreSnap('{{.Name}}')">{{if eq $.Lang "zh"}}恢复{{else}}Restore{{end}}</button>
<button type="button" class="btn danger" onclick="delSnap('{{.Name}}')">{{if eq $.Lang "zh"}}删除{{else}}Delete{{end}}</button>