ui: split /hosts/{id} into host page + /runs/{runID} run page
Host page owns host metadata, full runs table with per-row stage strip, in-flight banner, and empty-state CTA. Run page owns pipeline, active step, logs, sub-steps, spec diffs, and hold banner with a breadcrumb back to the host. Dashboard tile reverts to host-only. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,11 @@ package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"vetting/internal/model"
|
||||
"vetting/internal/store"
|
||||
)
|
||||
|
||||
func TestHumanAgoFrom(t *testing.T) {
|
||||
@@ -98,30 +96,20 @@ func TestHostTile_DisabledStartWhenOffline(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHostTile_MiniRunView asserts the tile renders a step-list entry
|
||||
// for every canonical stage, colours the dots according to the mixed
|
||||
// stage states in the fixture, and surfaces the run id + duration in
|
||||
// the meta row. This is the contract the dashboard leans on: the
|
||||
// operator should be able to read run health across all tiles without
|
||||
// drilling into any of them.
|
||||
func TestHostTile_MiniRunView(t *testing.T) {
|
||||
// TestHostTile_NoStageStrip: the tile no longer carries the Phase 3
|
||||
// per-stage mini run-view — the runs-table on /hosts/{id} owns the
|
||||
// stage-strip now. Guards against the regression that would bring
|
||||
// `tile-steplist` / `tile-step-name` / `tile-run-duration` back.
|
||||
func TestHostTile_NoStageStrip(t *testing.T) {
|
||||
now := time.Now()
|
||||
started := now.Add(-3 * time.Minute)
|
||||
latest := &model.Run{
|
||||
ID: 17,
|
||||
State: model.StateSMART,
|
||||
StartedAt: started,
|
||||
}
|
||||
// Mixed states: first two stages passed, SMART running, rest pending.
|
||||
stages := []model.Stage{
|
||||
{Name: "Inventory", State: model.StagePassed},
|
||||
{Name: "SpecValidate", State: model.StagePassed},
|
||||
{Name: "SMART", State: model.StageRunning},
|
||||
StartedAt: now.Add(-3 * time.Minute),
|
||||
}
|
||||
data := TileData{
|
||||
Host: model.Host{ID: 42, Name: "tile-test", MAC: "aa:bb:cc:dd:ee:ff"},
|
||||
Latest: latest,
|
||||
Stages: stages,
|
||||
LastSeenAt: &now,
|
||||
}
|
||||
var buf strings.Builder
|
||||
@@ -129,66 +117,18 @@ func TestHostTile_MiniRunView(t *testing.T) {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
html := buf.String()
|
||||
|
||||
// Step list exists and contains every canonical stage name so the
|
||||
// operator reads a full 9-dot strip regardless of how far the run got.
|
||||
if !strings.Contains(html, `<ol class="tile-steplist">`) {
|
||||
t.Fatalf("tile missing step list: %s", html)
|
||||
}
|
||||
for _, s := range store.DefaultStageOrder {
|
||||
want := fmt.Sprintf(`<span class="tile-step-name">%s</span>`, s)
|
||||
if !strings.Contains(html, want) {
|
||||
t.Fatalf("tile missing step name %q: %s", s, html)
|
||||
for _, dropped := range []string{
|
||||
`tile-steplist`,
|
||||
`tile-step-name`,
|
||||
`tile-step-dot`,
|
||||
`tile-run-id`,
|
||||
`tile-run-duration`,
|
||||
`tile-meta-row`,
|
||||
} {
|
||||
if strings.Contains(html, dropped) {
|
||||
t.Errorf("host tile leaked dropped class %q: %s", dropped, html)
|
||||
}
|
||||
}
|
||||
// Colours: the two passed stages got passed dots; SMART got a running
|
||||
// dot; CPUStress (no fixture row) falls back to pending.
|
||||
mustContain := []string{
|
||||
`stage-dot stage-dot-sm stage-dot-passed`,
|
||||
`stage-dot stage-dot-sm stage-dot-running`,
|
||||
`stage-dot stage-dot-sm stage-dot-pending`,
|
||||
}
|
||||
for _, c := range mustContain {
|
||||
if !strings.Contains(html, c) {
|
||||
t.Fatalf("tile missing expected dot classes %q: %s", c, html)
|
||||
}
|
||||
}
|
||||
// Meta row: run id + a duration string (minutes for a 3m-old run).
|
||||
if !strings.Contains(html, `#17`) {
|
||||
t.Fatalf("tile missing run id #17: %s", html)
|
||||
}
|
||||
if !strings.Contains(html, `class="tile-run-duration"`) {
|
||||
t.Fatalf("tile missing duration element: %s", html)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHostTile_GhostSteplist: a never-run host still gets a 9-dot
|
||||
// ghost strip (all pending). Keeps the tile height stable so the
|
||||
// dashboard grid doesn't reflow as hosts gain their first run.
|
||||
func TestHostTile_GhostSteplist(t *testing.T) {
|
||||
now := time.Now()
|
||||
data := TileData{
|
||||
Host: model.Host{ID: 1, Name: "fresh", MAC: "aa:bb:cc:dd:ee:01"},
|
||||
LastSeenAt: &now,
|
||||
}
|
||||
var buf strings.Builder
|
||||
if err := HostTile(data).Render(context.Background(), &buf); err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
html := buf.String()
|
||||
for _, s := range store.DefaultStageOrder {
|
||||
want := fmt.Sprintf(`<span class="tile-step-name">%s</span>`, s)
|
||||
if !strings.Contains(html, want) {
|
||||
t.Fatalf("ghost tile missing stage %q: %s", s, html)
|
||||
}
|
||||
}
|
||||
if strings.Contains(html, `stage-dot-passed`) || strings.Contains(html, `stage-dot-running`) || strings.Contains(html, `stage-dot-failed`) {
|
||||
t.Fatalf("ghost tile should have only pending dots: %s", html)
|
||||
}
|
||||
// No run → no meta row (suppresses "#0 · 0s" when no run exists).
|
||||
if strings.Contains(html, `class="tile-run-id"`) {
|
||||
t.Fatalf("ghost tile should omit run id: %s", html)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastSeenLabelAndClass(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user