internal/relui: improve task result formatting
Format most result types, somewhat. This implementation should be
reusable when formatting workflow outputs in the future.
For golang/go#53382
Change-Id: I4734cdefff85a37cb1047a6689411139f2ebfbc1
Reviewed-on: https://go-review.googlesource.com/c/build/+/412677
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Run-TryBot: Alex Rakoczy <alex@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/relui/static/styles.css b/internal/relui/static/styles.css
index b2de074..56a0469 100644
--- a/internal/relui/static/styles.css
+++ b/internal/relui/static/styles.css
@@ -215,6 +215,7 @@
font-size: 0.8125rem;
margin: 0;
padding: 1rem 0;
+ vertical-align: top;
}
.TaskList-itemLogLine {
font-family: monospace;
@@ -235,6 +236,51 @@
font-size: 0.8125rem;
font-weight: bold;
}
+.TaskList-itemActions {
+ width: 12.8125rem;
+}
+.TaskList-itemResult {
+ width: 5rem;
+}
+.TaskList-itemResultDetail {
+ border: 0.0625rem solid #ccc;
+ border-top: 0;
+ max-width: 17.8125rem;
+ vertical-align: top;
+ width: 17.8125rem;
+}
+.TaskList-itemResultDetailList {
+ margin: 0;
+}
+.TaskList-itemResultTerm {
+ background: #fafafa;
+ border-top: 0.0625rem solid #ccc;
+ font-size: 0.75rem;
+ padding: 0.1875rem 0.25rem 0;
+}
+.TaskList-itemResultTerm:nth-of-type(even) {
+ background: #f8f8f8;
+}
+.TaskList-itemResultDefinition {
+ background: #fafafa;
+ font-size: 0.875rem;
+ margin: 0;
+ min-height: 1.875rem;
+ padding: 0.5rem 0.375rem;
+}
+.TaskList-itemResultDefinition:nth-of-type(even) {
+ background: #f8f8f8;
+}
+.TaskList-itemResultDefinition--string {
+ display: block;
+ -webkit-line-clamp: 3;
+ max-height: 7.5rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.TaskList-itemResultArtifactSize {
+ float: right;
+}
.Button {
background: #375eab;
border: none;
diff --git a/internal/relui/templates/task_list.html b/internal/relui/templates/task_list.html
index 428fa36..c198478 100644
--- a/internal/relui/templates/task_list.html
+++ b/internal/relui/templates/task_list.html
@@ -18,6 +18,7 @@
</tr>
</thead>
{{range $task := .Tasks}}
+ {{ $resultDetail := unmarshalResultDetail $task.Result.String }}
<tbody>
<tr class="TaskList-item TaskList-itemSummary TaskList-expandableItem">
<td class="TaskList-itemCol TaskList-itemExpand">
@@ -53,7 +54,7 @@
{{$task.UpdatedAt.UTC.Format "Mon Jan _2 2006 15:04:05"}}
</td>
<td class="TaskList-itemCol TaskList-itemResult">
- {{$task.Result.String}}
+ {{ $resultDetail.Kind }}
</td>
<td class="TaskList-itemCol TaskList-itemAction">
{{if $task.Error.Valid}}
@@ -75,7 +76,7 @@
</td>
</tr>
<tr class="TaskList-itemLogsRow">
- <td class="TaskList-itemLogs" colspan="7">
+ <td class="TaskList-itemLogs" colspan="5">
{{if $task.Error.Valid}}
<div class="TaskList-itemLogLine TaskList-itemLogLineError">
{{- $task.Error.Value -}}
@@ -83,12 +84,76 @@
{{end}}
{{range $log := index $.TaskLogs $task.Name}}
<div class="TaskList-itemLogLine">
- {{- $log.CreatedAt.UTC.Format "2006/01/02 15:04:05"}} {{$log.Body -}}
+ {{- printf "%s %s" ($log.CreatedAt.UTC.Format "2006/01/02 15:04:05") $log.Body -}}
</div>
{{end}}
+ {{if $task.Result.Valid}}
+ <div class="TaskList-itemLogLine">
+ {{- $task.Result.String -}}
+ </div>
+ {{end}}
+ </td>
+ <td class="TaskList-itemResultDetail" colspan="2">
+ {{template "itemResult" $resultDetail}}
</td>
</tr>
</tbody>
{{end}}
</table>
{{end}}
+
+{{define "itemResult"}}
+ {{$resultDetail := .}}
+ {{if eq $resultDetail.Kind "Artifact"}}
+ <dl class="TaskList-itemResultDetailList">
+ <dt class="TaskList-itemResultTerm">Name</dt>
+ <dd class="TaskList-itemResultDefinition">
+ {{$resultDetail.Artifact.Target.Name}}
+ </dd>
+ <dt class="TaskList-itemResultTerm">Filename</dt>
+ <dd class="TaskList-itemResultDefinition">
+ {{$resultDetail.Artifact.Filename}}
+ </dd>
+ <dt class="TaskList-itemResultTerm">ScratchPath</dt>
+ <dd class="TaskList-itemResultDefinition">
+ {{$resultDetail.Artifact.ScratchPath}}
+ </dd>
+ <dt class="TaskList-itemResultTerm">StagingPath</dt>
+ <dd class="TaskList-itemResultDefinition">
+ {{$resultDetail.Artifact.StagingPath}}
+ </dd>
+ </dl>
+ {{else if eq $resultDetail.Kind "Artifacts"}}
+ <dt class="TaskList-itemResultTerm">Filenames</dt>
+ <dd class="TaskList-itemResultDefinition">
+ {{range $artifact := $resultDetail.Artifacts}}
+ {{$artifact.Filename}} <span class="TaskList-itemResultArtifactSize">{{prettySize $artifact.Size}}</span>
+ {{end}}
+ </dd>
+ {{else if eq $resultDetail.Kind "Outputs"}}
+ {{range $key, $value := $resultDetail.Outputs}}
+ <dt class="TaskList-itemResultTerm">
+ {{$key}}
+ </dt>
+ <dd class="TaskList-itemResultDefinition">
+ {{template "itemResult" $value}}
+ </dd>
+ {{end}}
+ {{else if eq $resultDetail.Kind "JSON"}}
+ {{range $key, $value := $resultDetail.JSON}}
+ <dt class="TaskList-itemResultTerm">
+ {{$key}}
+ </dt>
+ <dd class="TaskList-itemResultDefinition">
+ {{$value}}
+ </dd>
+ {{end}}
+ {{else if eq $resultDetail.Kind "String"}}
+ <dt class="TaskList-itemResultTerm">
+ String
+ </dt>
+ <dd class="TaskList-itemResultDefinition TaskList-itemResultDefinition--string">
+ {{$resultDetail.String}}
+ </dd>
+ {{end}}
+{{end}}
diff --git a/internal/relui/web.go b/internal/relui/web.go
index 8c765c1..b44de81 100644
--- a/internal/relui/web.go
+++ b/internal/relui/web.go
@@ -19,6 +19,7 @@
"net/http"
"net/url"
"path"
+ "reflect"
"strings"
"time"
@@ -77,8 +78,10 @@
header: header,
}
helpers := map[string]interface{}{
- "baseLink": s.BaseLink,
- "hasPrefix": strings.HasPrefix,
+ "baseLink": s.BaseLink,
+ "hasPrefix": strings.HasPrefix,
+ "prettySize": prettySize,
+ "unmarshalResultDetail": unmarshalResultDetail,
}
s.templates = template.Must(template.New("").Funcs(helpers).ParseFS(templates, "templates/*.html"))
s.homeTmpl = s.mustLookup("home.html")
@@ -353,3 +356,66 @@
}
http.Redirect(w, r, s.BaseLink("/"), http.StatusSeeOther)
}
+
+// resultDetail contains unmarshalled results from a workflow task, or
+// workflow output. Only one field is expected to be populated.
+//
+// The UI implementation uses Kind to determine which result type to
+// render.
+type resultDetail struct {
+ Artifact artifact
+ Artifacts []artifact
+ Outputs map[string]*resultDetail
+ JSON map[string]interface{}
+ String string
+ Unknown interface{}
+}
+
+func (r *resultDetail) Kind() string {
+ v := reflect.ValueOf(r)
+ if v.IsZero() {
+ return ""
+ }
+ v = v.Elem()
+ for i := 0; i < v.NumField(); i++ {
+ if v.Field(i).IsZero() {
+ continue
+ }
+ return v.Type().Field(i).Name
+ }
+ return ""
+}
+
+func (r *resultDetail) UnmarshalJSON(result []byte) error {
+ v := reflect.ValueOf(r).Elem()
+ for i := 0; i < v.NumField(); i++ {
+ f := v.Field(i)
+ if err := json.Unmarshal(result, f.Addr().Interface()); err == nil {
+ if f.IsZero() {
+ continue
+ }
+ return nil
+ }
+ }
+ return errors.New("unknown result type")
+}
+
+func unmarshalResultDetail(result string) *resultDetail {
+ ret := new(resultDetail)
+ if err := json.Unmarshal([]byte(result), &ret); err != nil {
+ ret.String = err.Error()
+ }
+ return ret
+}
+
+func prettySize(size int) string {
+ const mb = 1 << 20
+ if size == 0 {
+ return ""
+ }
+ if size < mb {
+ // All Go releases are >1mb, but handle this case anyway.
+ return fmt.Sprintf("%v bytes", size)
+ }
+ return fmt.Sprintf("%.0fMiB", float64(size)/mb)
+}
diff --git a/internal/relui/web_test.go b/internal/relui/web_test.go
index 7ce6d9a..c3c72f0 100644
--- a/internal/relui/web_test.go
+++ b/internal/relui/web_test.go
@@ -753,3 +753,60 @@
})
}
}
+
+func TestResultDetail(t *testing.T) {
+ cases := []struct {
+ desc string
+ input string
+ want *resultDetail
+ wantKind string
+ }{
+ {
+ desc: "string",
+ input: `"hello"`,
+ want: &resultDetail{String: "hello"},
+ wantKind: "String",
+ },
+ {
+ desc: "nested json string",
+ input: `{"SomeOutput": "hello"}`,
+ want: &resultDetail{Outputs: map[string]*resultDetail{"SomeOutput": {String: "hello"}}},
+ wantKind: "Outputs",
+ },
+ {
+ desc: "nested json complex",
+ input: `{"SomeOutput": {"Filename": "go.exe"}}`,
+ want: &resultDetail{Outputs: map[string]*resultDetail{"SomeOutput": {Artifact: artifact{Filename: "go.exe"}}}},
+ wantKind: "Outputs",
+ },
+ {
+ desc: "nested json slice",
+ input: `{"SomeOutput": [{"Filename": "go.exe"}]}`,
+ want: &resultDetail{Outputs: map[string]*resultDetail{"SomeOutput": {Artifacts: []artifact{{Filename: "go.exe"}}}}},
+ wantKind: "Outputs",
+ },
+ {
+ desc: "nested json output",
+ input: `{"SomeOutput": {"OtherOutput": "go.exe"}}`,
+ want: &resultDetail{Outputs: map[string]*resultDetail{"SomeOutput": {Outputs: map[string]*resultDetail{"OtherOutput": {String: "go.exe"}}}}},
+ wantKind: "Outputs",
+ },
+ {
+ desc: "null json",
+ input: `null`,
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.desc, func(t *testing.T) {
+ got := unmarshalResultDetail(c.input)
+
+ if got.Kind() != c.wantKind {
+ t.Errorf("got.Kind() = %q, wanted %q", got.Kind(), c.wantKind)
+ }
+ if diff := cmp.Diff(c.want, got); diff != "" {
+ t.Errorf("unmarshalResultDetail mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+
+}