internal/relui: simplify rendering of homepage

This refactors the homepage rendering to use a nested template for each
task row. This will help simplify the template as we add more complex
layout to the outputs of workflows and tasks.

Updates golang/go#51797
Updates golang/go#40279
For golang/go#53382

Change-Id: I85a86b82bdc79c7fb4e837d884af922c7028295d
Reviewed-on: https://go-review.googlesource.com/c/build/+/412176
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Alex Rakoczy <alex@golang.org>
diff --git a/internal/relui/templates/home.html b/internal/relui/templates/home.html
index d5534f1..e160560 100644
--- a/internal/relui/templates/home.html
+++ b/internal/relui/templates/home.html
@@ -3,6 +3,8 @@
     Use of this source code is governed by a BSD-style
     license that can be found in the LICENSE file.
 -->
+{{template "layout" .}}
+
 {{define "content"}}
   <section class="Workflows">
     <div class="Workflows-header">
@@ -10,7 +12,9 @@
       <a href="{{baseLink "/workflows/new"}}" class="Button">New</a>
     </div>
     <ul class="WorkflowList">
-      {{range $workflow := .Workflows}}
+      {{range $wfid := .WorkflowIDs}}
+        {{$detail := index $.WorkflowDetails $wfid}}
+        {{$workflow := $detail.Workflow}}
         <li class="WorkflowList-item">
           <h3 class="WorkflowList-title">
             {{$workflow.Name.String}}
@@ -19,8 +23,8 @@
             </span>
             {{if not (or $workflow.Finished $workflow.Error)}}
               <div class="WorkflowList-titleStop">
-                <form action="{{baseLink (printf "/workflows/%s/stop" $workflow.ID) }}" method="post">
-                  <input type="hidden" id="workflow.id" name="workflow.id" value="{{$workflow.ID}}" />
+                <form action="{{baseLink (printf "/workflows/%s/stop" $wfid)}}" method="post">
+                  <input type="hidden" id="workflow.id" name="workflow.id" value="{{$wfid}}" />
                   <input name="workflow.stop" class="Button Button--red" type="submit" value="STOP" onclick="return this.form.reportValidity() && confirm('This will stop the workflow and all in-flight tasks.\n\nAre you sure you want to proceed?')" />
                 </form>
               </div>
@@ -60,87 +64,7 @@
             </tbody>
           </table>
           <h4 class="WorkflowList-sectionTitle">Tasks</h4>
-          <table class="TaskList">
-            <thead>
-              <tr class="TaskList-item TaskList-itemHeader">
-                <th class="TaskList-itemHeaderCol TaskList-itemExpand"></th>
-                <th class="TaskList-itemHeaderCol TaskList-itemState">State</th>
-                <th class="TaskList-itemHeaderCol TaskList-itemName">Name</th>
-                <th class="TaskList-itemHeaderCol TaskList-itemStarted">Started</th>
-                <th class="TaskList-itemHeaderCol TaskList-itemUpdated">Updated</th>
-                <th class="TaskList-itemHeaderCol TaskList-itemResult">Result</th>
-                <th class="TaskList-itemHeaderCol TaskList-itemActions">Actions</th>
-              </tr>
-            </thead>
-            {{$tasks := index $.WorkflowTasks $workflow.ID}}
-            {{range $task := $tasks}}
-              <tbody>
-                <tr class="TaskList-item TaskList-itemSummary TaskList-expandableItem">
-                  <td class="TaskList-itemCol TaskList-itemExpand">
-                    <span class="TaskList-itemExpandClosed">
-                      <img class="TaskList-itemExpandControl" alt="unfold more" src="{{baseLink "/static/images/chevron_right_black_24dp.svg"}}" />
-                    </span>
-                    <span class="TaskList-ItemExpandOpened">
-                      <img class="TaskList-itemExpandControl" alt="unfold less" src="{{baseLink "/static/images/expand_more_black_24dp.svg"}}" />
-                    </span>
-                  </td>
-                  <td class="TaskList-itemCol TaskList-itemState">
-                    {{if $task.Error.Valid}}
-                      <img class="TaskList-itemStateIcon" alt="error" src="{{baseLink "/static/images/error_red_24dp.svg"}}" />
-                    {{else if $task.Finished}}
-                      <img class="TaskList-itemStateIcon" alt="finished" src="{{baseLink "/static/images/check_circle_green_24dp.svg"}}" />
-                    {{else}}
-                      <img class="TaskList-itemStateIcon" alt="pending" src="{{baseLink "/static/images/pending_yellow_24dp.svg"}}" />
-                    {{end}}
-                  </td>
-                  <td class="TaskList-itemCol TaskList-itemName">
-                    {{$task.Name}}
-                  </td>
-                  <td class="TaskList-itemCol TaskList-itemStarted">
-                    {{$task.CreatedAt.UTC.Format "Mon Jan _2 2006 15:04:05"}}
-                  </td>
-                  <td class="TaskList-itemCol TaskList-itemUpdated">
-                    {{$task.UpdatedAt.UTC.Format "Mon Jan _2 2006 15:04:05"}}
-                  </td>
-                  <td class="TaskList-itemCol TaskList-itemResult">
-                    {{$task.Result.String}}
-                  </td>
-                  <td class="TaskList-itemCol TaskList-itemAction">
-                    {{if $task.Error.Valid}}
-                    <div class="TaskList-retryTask">
-                      <form action="{{baseLink (printf "/workflows/%s/tasks/%s/retry" $workflow.ID $task.Name) }}" method="post">
-                        <input type="hidden" id="workflow.id" name="workflow.id" value="{{$workflow.ID}}" />
-                        <input class="Button Button--small" name="task.reset" type="submit" value="Retry" onclick="return this.form.reportValidity() && confirm('This will retry the task and clear workflow errors.\n\nReady to proceed?')" />
-                      </form>
-                    </div>
-                    {{end}}
-                    {{if and (not $task.Finished) (hasPrefix $task.Name "APPROVE-")}}
-                    <div class="TaskList-approveTask">
-                      <form action="{{baseLink (printf "/workflows/%s/tasks/%s/approve" $workflow.ID $task.Name) }}" method="post">
-                        <input type="hidden" id="workflow.id" name="workflow.id" value="{{$workflow.ID}}" />
-                        <input class="Button Button--small" name="task.approve" type="submit" value="Approve" onclick="return this.form.reportValidity() && confirm('This will mark the task approved and resume the workflow.\n\nReady to proceed?')" />
-                      </form>
-                    </div>
-                    {{end}}
-                  </td>
-                </tr>
-                <tr class="TaskList-itemLogsRow">
-                  <td class="TaskList-itemLogs" colspan="7">
-                    {{if $task.Error.Valid}}
-                      <div class="TaskList-itemLogLine TaskList-itemLogLineError">
-                        {{- $task.Error.Value -}}
-                      </div>
-                    {{end}}
-                    {{range $log := $.Logs $workflow.ID  $task.Name}}
-                      <div class="TaskList-itemLogLine">
-                        {{- $log.CreatedAt.UTC.Format "2006/01/02 15:04:05"}} {{$log.Body -}}
-                      </div>
-                    {{end}}
-                  </td>
-                </tr>
-              </tbody>
-            {{end}}
-          </table>
+          {{template "task_list" $detail}}
         </li>
       {{end}}
     </ul>
diff --git a/internal/relui/templates/layout.html b/internal/relui/templates/layout.html
index 9cc777e..a8be183 100644
--- a/internal/relui/templates/layout.html
+++ b/internal/relui/templates/layout.html
@@ -3,6 +3,7 @@
     Use of this source code is governed by a BSD-style
     license that can be found in the LICENSE file.
 -->
+{{define "layout"}}
 <!DOCTYPE html>
 <html lang="en">
   <title>{{.SiteHeader.Title}}</title>
@@ -16,7 +17,8 @@
       </div>
     </header>
     <main class="Site-content">
-      {{template "content" .}}
+      {{block "content" .}}{{end}}
     </main>
   </body>
 </html>
+{{end}}
diff --git a/internal/relui/templates/new_workflow.html b/internal/relui/templates/new_workflow.html
index 4810a8f..c0a0a44 100644
--- a/internal/relui/templates/new_workflow.html
+++ b/internal/relui/templates/new_workflow.html
@@ -3,6 +3,8 @@
     Use of this source code is governed by a BSD-style
     license that can be found in the LICENSE file.
 -->
+{{template "layout" .}}
+
 {{define "content"}}
   <section class="NewWorkflow">
     <h2>New Go Release</h2>
diff --git a/internal/relui/templates/task_list.html b/internal/relui/templates/task_list.html
new file mode 100644
index 0000000..428fa36
--- /dev/null
+++ b/internal/relui/templates/task_list.html
@@ -0,0 +1,94 @@
+<!--
+    Copyright 2022 The Go Authors. All rights reserved.
+    Use of this source code is governed by a BSD-style
+    license that can be found in the LICENSE file.
+-->
+{{define "task_list"}}
+  {{$workflow := .Workflow}}
+  <table class="TaskList">
+    <thead>
+      <tr class="TaskList-item TaskList-itemHeader">
+        <th class="TaskList-itemHeaderCol TaskList-itemExpand"></th>
+        <th class="TaskList-itemHeaderCol TaskList-itemState">State</th>
+        <th class="TaskList-itemHeaderCol TaskList-itemName">Name</th>
+        <th class="TaskList-itemHeaderCol TaskList-itemStarted">Started</th>
+        <th class="TaskList-itemHeaderCol TaskList-itemUpdated">Updated</th>
+        <th class="TaskList-itemHeaderCol TaskList-itemResult">Result</th>
+        <th class="TaskList-itemHeaderCol TaskList-itemActions">Actions</th>
+      </tr>
+    </thead>
+    {{range $task := .Tasks}}
+      <tbody>
+        <tr class="TaskList-item TaskList-itemSummary TaskList-expandableItem">
+          <td class="TaskList-itemCol TaskList-itemExpand">
+            <span class="TaskList-itemExpandClosed">
+              <img class="TaskList-itemExpandControl" alt="unfold more" src="{{baseLink "/static/images/chevron_right_black_24dp.svg"}}" />
+            </span>
+            <span class="TaskList-ItemExpandOpened">
+              <img class="TaskList-itemExpandControl" alt="unfold less" src="{{baseLink "/static/images/expand_more_black_24dp.svg"}}" />
+            </span>
+          </td>
+          <td class="TaskList-itemCol TaskList-itemState">
+            {{if $task.Error.Valid}}
+              <img class="TaskList-itemStateIcon" alt="error" src="{{baseLink "/static/images/error_red_24dp.svg"}}" />
+            {{else if $task.Finished}}
+              <img
+                class="TaskList-itemStateIcon"
+                alt="finished"
+                src="{{baseLink "/static/images/check_circle_green_24dp.svg"}}" />
+            {{else}}
+              <img
+                class="TaskList-itemStateIcon"
+                alt="pending"
+                src="{{baseLink "/static/images/pending_yellow_24dp.svg"}}" />
+            {{end}}
+          </td>
+          <td class="TaskList-itemCol TaskList-itemName">
+            {{$task.Name}}
+          </td>
+          <td class="TaskList-itemCol TaskList-itemStarted">
+            {{$task.CreatedAt.UTC.Format "Mon Jan _2 2006 15:04:05"}}
+          </td>
+          <td class="TaskList-itemCol TaskList-itemUpdated">
+            {{$task.UpdatedAt.UTC.Format "Mon Jan _2 2006 15:04:05"}}
+          </td>
+          <td class="TaskList-itemCol TaskList-itemResult">
+            {{$task.Result.String}}
+          </td>
+          <td class="TaskList-itemCol TaskList-itemAction">
+            {{if $task.Error.Valid}}
+              <div class="TaskList-retryTask">
+                <form action="{{baseLink (printf "/workflows/%s/tasks/%s/retry" $workflow.ID $task.Name)}}" method="post">
+                  <input type="hidden" id="workflow.id" name="workflow.id" value="{{$workflow.ID}}" />
+                  <input class="Button Button--small" name="task.reset" type="submit" value="Retry" onclick="return this.form.reportValidity() && confirm('This will retry the task and clear workflow errors.\n\nReady to proceed?')" />
+                </form>
+              </div>
+            {{end}}
+            {{if and (not $task.Finished) (hasPrefix $task.Name "APPROVE-")}}
+              <div class="TaskList-approveTask">
+                <form action="{{baseLink (printf "/workflows/%s/tasks/%s/approve" $workflow.ID $task.Name)}}" method="post">
+                  <input type="hidden" id="workflow.id" name="workflow.id" value="{{$workflow.ID}}" />
+                  <input class="Button Button--small" name="task.approve" type="submit" value="Approve" onclick="return this.form.reportValidity() && confirm('This will mark the task approved and resume the workflow.\n\nReady to proceed?')" />
+                </form>
+              </div>
+            {{end}}
+          </td>
+        </tr>
+        <tr class="TaskList-itemLogsRow">
+          <td class="TaskList-itemLogs" colspan="7">
+            {{if $task.Error.Valid}}
+              <div class="TaskList-itemLogLine TaskList-itemLogLineError">
+                {{- $task.Error.Value -}}
+              </div>
+            {{end}}
+            {{range $log := index $.TaskLogs $task.Name}}
+              <div class="TaskList-itemLogLine">
+                {{- $log.CreatedAt.UTC.Format "2006/01/02 15:04:05"}} {{$log.Body -}}
+              </div>
+            {{end}}
+          </td>
+        </tr>
+      </tbody>
+    {{end}}
+  </table>
+{{end}}
diff --git a/internal/relui/web.go b/internal/relui/web.go
index c6acad4..8c765c1 100644
--- a/internal/relui/web.go
+++ b/internal/relui/web.go
@@ -59,6 +59,7 @@
 	// mux used if baseURL is set
 	bm *http.ServeMux
 
+	templates       *template.Template
 	homeTmpl        *template.Template
 	newWorkflowTmpl *template.Template
 }
@@ -79,9 +80,9 @@
 		"baseLink":  s.BaseLink,
 		"hasPrefix": strings.HasPrefix,
 	}
-	layout := template.Must(template.New("layout.html").Funcs(helpers).ParseFS(templates, "templates/layout.html"))
-	s.homeTmpl = template.Must(template.Must(layout.Clone()).Funcs(helpers).ParseFS(templates, "templates/home.html"))
-	s.newWorkflowTmpl = template.Must(template.Must(layout.Clone()).Funcs(helpers).ParseFS(templates, "templates/new_workflow.html"))
+	s.templates = template.Must(template.New("").Funcs(helpers).ParseFS(templates, "templates/*.html"))
+	s.homeTmpl = s.mustLookup("home.html")
+	s.newWorkflowTmpl = s.mustLookup("new_workflow.html")
 	s.m.POST("/workflows/:id/stop", s.stopWorkflowHandler)
 	s.m.POST("/workflows/:id/tasks/:name/retry", s.retryTaskHandler)
 	s.m.POST("/workflows/:id/tasks/:name/approve", s.approveTaskHandler)
@@ -98,6 +99,14 @@
 	return s
 }
 
+func (s *Server) mustLookup(name string) *template.Template {
+	t := template.Must(template.Must(s.templates.Clone()).ParseFS(templates, path.Join("templates", name))).Lookup(name)
+	if t == nil {
+		panic(fmt.Errorf("template %q not found", name))
+	}
+	return t
+}
+
 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if s.bm != nil {
 		s.bm.ServeHTTP(w, r)
@@ -124,19 +133,18 @@
 	return u.String()
 }
 
-type homeResponse struct {
-	SiteHeader    SiteHeader
-	Workflows     []db.Workflow
-	WorkflowTasks map[uuid.UUID][]db.Task
-	TaskLogs      map[uuid.UUID]map[string][]db.TaskLog
+type workflowDetail struct {
+	Workflow db.Workflow
+	Tasks    []db.Task
+	// TaskLogs is a map of all logs for a db.Task, keyed on
+	// (db.Task).Name
+	TaskLogs map[string][]db.TaskLog
 }
 
-func (h *homeResponse) Logs(workflow uuid.UUID, task string) []db.TaskLog {
-	t := h.TaskLogs[workflow]
-	if t == nil {
-		return nil
-	}
-	return t[task]
+type homeResponse struct {
+	SiteHeader      SiteHeader
+	WorkflowIDs     []uuid.UUID
+	WorkflowDetails map[uuid.UUID]*workflowDetail
 }
 
 func (h *homeResponse) WorkflowParams(wf db.Workflow) map[string]string {
@@ -172,22 +180,31 @@
 	if err != nil {
 		return nil, err
 	}
-	wfTasks := make(map[uuid.UUID][]db.Task, len(ws))
+	hr := &homeResponse{
+		SiteHeader:      s.header,
+		WorkflowDetails: make(map[uuid.UUID]*workflowDetail),
+	}
+	for _, w := range ws {
+		hr.WorkflowIDs = append(hr.WorkflowIDs, w.ID)
+		hr.WorkflowDetails[w.ID] = &workflowDetail{Workflow: w}
+	}
 	for _, t := range tasks {
-		wfTasks[t.WorkflowID] = append(wfTasks[t.WorkflowID], t)
+		wd := hr.WorkflowDetails[t.WorkflowID]
+		wd.Tasks = append(hr.WorkflowDetails[t.WorkflowID].Tasks, t)
+		wd.TaskLogs = make(map[string][]db.TaskLog)
 	}
 	tlogs, err := q.TaskLogs(ctx)
 	if err != nil {
 		return nil, err
 	}
-	wftlogs := make(map[uuid.UUID]map[string][]db.TaskLog)
 	for _, l := range tlogs {
-		if wftlogs[l.WorkflowID] == nil {
-			wftlogs[l.WorkflowID] = make(map[string][]db.TaskLog)
+		wd := hr.WorkflowDetails[l.WorkflowID]
+		if wd.TaskLogs == nil {
+			wd.TaskLogs = make(map[string][]db.TaskLog)
 		}
-		wftlogs[l.WorkflowID][l.TaskName] = append(wftlogs[l.WorkflowID][l.TaskName], l)
+		wd.TaskLogs[l.TaskName] = append(wd.TaskLogs[l.TaskName], l)
 	}
-	return &homeResponse{SiteHeader: s.header, Workflows: ws, WorkflowTasks: wfTasks, TaskLogs: wftlogs}, nil
+	return hr, nil
 }
 
 type newWorkflowResponse struct {