From d83517f67ba638abed1d76541068413e60142194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Thu, 26 Dec 2024 18:41:29 +0100 Subject: [PATCH] Refresh task view dynamically with Javascript This is more efficient, responsive, and user friendly. --- acid.go | 231 ++++++++++++++++++++++++++++++++++++----------- terminal.go | 12 ++- terminal_test.go | 32 ++++++- 3 files changed, 220 insertions(+), 55 deletions(-) diff --git a/acid.go b/acid.go index cdf9007..62687f4 100644 --- a/acid.go +++ b/acid.go @@ -263,53 +263,182 @@ var templateTask = template.Must(template.New("tasks").Parse(` Task {{.ID}} -{{if .IsRunning}} - -{{end}}

Tasks » {{.ID}}

-{{if .Created}} + +{{if .Created -}}
Created
-
{{.CreatedAgo}} ago
-{{end}} -{{if .Changed}} +
{{.CreatedAgo}} ago
+{{end -}} +{{if .Changed -}}
Changed
-
{{.ChangedAgo}} ago
-{{end}} +
{{.ChangedAgo}} ago
+{{end -}}
Project
-
{{.FullName}}
+
{{.FullName}}
Commit
-
{{.Hash}}
+
{{.Hash}}
Runner
-
{{.RunnerName}}
+
{{.RunnerName}}
State
-
{{.State}}{{if .Detail}} ({{.Detail}}){{end}}
+
{{.State}}{{if .Detail}} ({{.Detail}}){{end}}
Notified
-
{{.Notified}}
-{{if .Duration}} +
{{.Notified}}
Duration
-
{{.Duration}}
-{{end}} +
{{if .Duration}}{{.Duration}}{{else}}—{{end}}
-{{if .RunLog}} -

Runner log

-
{{printf "%s" .RunLog}}
-{{end}} -{{if .TaskLog}} -

Task log

-
{{printf "%s" .TaskLog}}
-{{end}} -{{if .DeployLog}} -

Deploy log

-
{{printf "%s" .DeployLog}}
-{{end}} - + +

Runner log

+
{{printf "%s" .RunLog}}
+

Task log

+
{{printf "%s" .TaskLog}}
+

Deploy log

+
{{printf "%s" .DeployLog}}
+ +{{if .IsRunning -}} + +{{end -}} + `)) +// handlerTask serves as the data for JSON encoding and the task HTML template. +// It needs to explicitly include many convenience method results. +type handlerTask struct { + Task + IsRunning bool + + Created *string // Task.Created?.String() + Changed *string // Task.Changed?.String() + CreatedAgo string // Task.CreatedAgo() + ChangedAgo string // Task.ChangedAgo() + Duration *string // Task.Duration?.String() + State string // Task.State.String() + RunLog string + RunLogTop int + TaskLog string + TaskLogTop int + DeployLog string + DeployLogTop int +} + +func toNilableString[T fmt.Stringer](stringer *T) *string { + if stringer == nil { + return nil + } + s := (*stringer).String() + return &s +} + +func newHandlerTask(task Task) handlerTask { + return handlerTask{ + Task: task, + RunLog: string(task.RunLog), + TaskLog: string(task.TaskLog), + DeployLog: string(task.DeployLog), + + Created: toNilableString(task.Created()), + Changed: toNilableString(task.Changed()), + CreatedAgo: task.CreatedAgo(), + ChangedAgo: task.ChangedAgo(), + Duration: toNilableString(task.Duration()), + State: task.State.String(), + } +} + +func (ht *handlerTask) updateFromRunning( + rt *RunningTask, lastRun, lastTask, lastDeploy int) { + ht.IsRunning = true + ht.Task.DurationSeconds = rt.elapsed() + ht.Duration = toNilableString(ht.Task.Duration()) + + rt.RunLog.Lock() + defer rt.RunLog.Unlock() + rt.TaskLog.Lock() + defer rt.TaskLog.Unlock() + rt.DeployLog.Lock() + defer rt.DeployLog.Unlock() + + ht.RunLog, ht.RunLogTop = rt.RunLog.SerializeUpdates(lastRun) + ht.TaskLog, ht.TaskLogTop = rt.TaskLog.SerializeUpdates(lastTask) + ht.DeployLog, ht.DeployLogTop = rt.DeployLog.SerializeUpdates(lastDeploy) +} + func handleTask(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.PathValue("id")) if err != nil { @@ -330,36 +459,32 @@ func handleTask(w http.ResponseWriter, r *http.Request) { return } - task := struct { - Task - IsRunning bool - }{Task: tasks[0]} + // These are intended for running tasks, + // so don't reprocess DB logs, which would only help the last update. + q := r.URL.Query() + lastRun, _ := strconv.Atoi(q.Get("run")) + lastTask, _ := strconv.Atoi(q.Get("task")) + lastDeploy, _ := strconv.Atoi(q.Get("deploy")) + + task := newHandlerTask(tasks[0]) func() { gRunningMutex.Lock() defer gRunningMutex.Unlock() - rt, ok := gRunning[task.ID] - task.IsRunning = ok - if !ok { - return + if rt, ok := gRunning[task.ID]; ok { + task.updateFromRunning( + rt, int(lastRun), int(lastTask), int(lastDeploy)) } - - task.DurationSeconds = rt.elapsed() - - rt.RunLog.Lock() - defer rt.RunLog.Unlock() - rt.TaskLog.Lock() - defer rt.TaskLog.Unlock() - rt.DeployLog.Lock() - defer rt.DeployLog.Unlock() - - task.RunLog = rt.RunLog.Serialize(0) - task.TaskLog = rt.TaskLog.Serialize(0) - task.DeployLog = rt.DeployLog.Serialize(0) }() - if err := templateTask.Execute(w, &task); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + if q.Has("json") { + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(&task) + } else { + err = templateTask.Execute(w, &task) + } + if err != nil { + log.Println(err) } } diff --git a/terminal.go b/terminal.go index 4660c1b..55eabb5 100644 --- a/terminal.go +++ b/terminal.go @@ -47,6 +47,16 @@ func (tw *terminalWriter) log(format string, v ...interface{}) { } } +// SerializeUpdates returns an update block for a client with a given last line, +// and the index of the first line in the update block. +func (tw *terminalWriter) SerializeUpdates(last int) (string, int) { + if last < 0 || last >= len(tw.lines) { + return "", last + } + top := tw.lines[last].updateGroup + return string(tw.Serialize(top)), top +} + func (tw *terminalWriter) Serialize(top int) []byte { var b bytes.Buffer for i := top; i < len(tw.lines); i++ { @@ -104,7 +114,7 @@ func (tw *terminalWriter) processPrint(r rune) { // Refresh update trackers, if necessary. if tw.lines[len(tw.lines)-1].updateGroup > tw.line { for i := tw.line; i < len(tw.lines); i++ { - tw.lines[i].updateGroup = tw.line + tw.lines[i].updateGroup = min(tw.lines[i].updateGroup, tw.line) } } diff --git a/terminal_test.go b/terminal_test.go index bec14bf..b0686ed 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "slices" + "testing" +) // This could be way more extensive, but we're not aiming for perfection. var tests = []struct { @@ -68,3 +71,30 @@ Loop: } } } + +func TestTerminalUpdateGroups(t *testing.T) { + tw := terminalWriter{} + collect := func() (have []int) { + for _, line := range tw.lines { + have = append(have, line.updateGroup) + } + return + } + + // 0: A 0 0 0 + // 1: B X 1 1 1 + // 2: C Y 1 2 1 1 + // 3: Z 2 3 2 + // 4: 3 4 + tw.Write([]byte("A\nB\nC\x1b[FX\nY\nZ")) + have, want := collect(), []int{0, 1, 1, 3} + if !slices.Equal(want, have) { + t.Errorf("update groups: %+v; want: %+v", have, want) + } + + tw.Write([]byte("\x1b[F1\n2\n3")) + have, want = collect(), []int{0, 1, 1, 2, 4} + if !slices.Equal(want, have) { + t.Errorf("update groups: %+v; want: %+v", have, want) + } +}