feat: master/agent phase 2 — jobs and manual update (#7) #20
@@ -23,6 +23,9 @@ import (
|
||||
//go:embed templates/*.html static/*
|
||||
var assets embed.FS
|
||||
|
||||
// serviceJobsLimit is how many jobs the service page lists.
|
||||
const serviceJobsLimit = 20
|
||||
|
||||
var agentNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
|
||||
|
||||
// Store is the storage the UI reads and writes. *store.Store satisfies it.
|
||||
@@ -203,7 +206,7 @@ func (s *Server) serviceDetail(r *http.Request, id int64) (pageData, error) {
|
||||
} else if err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.Jobs, err = s.store.ListJobs(r.Context(), model.JobFilter{ServiceID: id, Limit: 20})
|
||||
data.Jobs, err = s.store.ListJobs(r.Context(), model.JobFilter{ServiceID: id, Limit: serviceJobsLimit})
|
||||
return data, err
|
||||
}
|
||||
|
||||
@@ -225,7 +228,7 @@ func (s *Server) serviceJobsFragment(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jobs, err := s.store.ListJobs(r.Context(), model.JobFilter{ServiceID: id, Limit: 20})
|
||||
jobs, err := s.store.ListJobs(r.Context(), model.JobFilter{ServiceID: id, Limit: serviceJobsLimit})
|
||||
if err != nil {
|
||||
s.fail(w, err)
|
||||
return
|
||||
|
||||
@@ -482,6 +482,19 @@ func TestJobFragment_RereadsFromStore(t *testing.T) {
|
||||
assert.Contains(t, body, "step 2")
|
||||
}
|
||||
|
||||
func TestJobFragment_PollsOnlyWhileActive(t *testing.T) {
|
||||
srv, store, _, _, _ := newTestEnv(t)
|
||||
store.jobs = []model.Job{
|
||||
{ID: 42, ServiceID: 1, Status: model.JobRunning, CreatedAt: time.Now()},
|
||||
{ID: 43, ServiceID: 1, Status: model.JobSucceeded, CreatedAt: time.Now()},
|
||||
}
|
||||
_, body := get(t, srv, "/fragments/jobs/42")
|
||||
assert.Contains(t, body, `hx-trigger="sse:job-42, every 2s"`)
|
||||
_, body = get(t, srv, "/fragments/jobs/43")
|
||||
assert.Contains(t, body, `hx-trigger="sse:job-43"`)
|
||||
assert.NotContains(t, body, "every 2s")
|
||||
}
|
||||
|
||||
func TestJob_NotFound(t *testing.T) {
|
||||
srv, _, _, _ := newTestServer(t)
|
||||
for _, path := range []string{"/jobs/99", "/fragments/jobs/99"} {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{{define "content"}}
|
||||
<h1>Job #{{.Job.ID}}</h1>
|
||||
<p><a href="/services/{{.Job.ServiceID}}">{{.Job.ServiceName}}</a></p>
|
||||
<div id="job" hx-get="/fragments/jobs/{{.Job.ID}}" hx-trigger="sse:job-{{.Job.ID}}" hx-swap="innerHTML">
|
||||
{{template "job" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* The wrapper is part of the fragment and swapped as outerHTML, so a finished job drops the polling timer. The timer recovers from a dropped SSE signal. */}}
|
||||
{{define "job"}}
|
||||
<div id="job" hx-get="/fragments/jobs/{{.Job.ID}}" hx-trigger="sse:job-{{.Job.ID}}{{if .Job.Active}}, every 2s{{end}}" hx-swap="outerHTML">
|
||||
<table class="details">
|
||||
<tr><th>Status</th><td class="status status-job-{{.Job.Status}}">{{.Job.Status}}</td></tr>
|
||||
<tr><th>From</th><td><code>{{shortRef .Job.FromRef}}</code></td></tr>
|
||||
@@ -18,4 +18,5 @@
|
||||
{{if .Job.Error}}<p class="error">{{.Job.Error}}</p>{{end}}
|
||||
{{if .Job.OutputTruncated}}<p class="muted">Earlier output truncated.</p>{{end}}
|
||||
<pre class="log">{{.Job.Output}}</pre>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user