internal/gaby: add filter to action log page
Hook up the new internal/filter package to the action log page
to allow filtering.
Fixes golang/oscar#53.
Change-Id: Ie40f791c430e2f79dc78140d3f4be5e1c9c1e380
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/630355
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
diff --git a/internal/filter/eval.go b/internal/filter/eval.go
index 8806290..501efa5 100644
--- a/internal/filter/eval.go
+++ b/internal/filter/eval.go
@@ -213,7 +213,7 @@
}
// function returns an evaluator function for a [functionExpr].
-func (ev *eval[T]) function(e *functionExpr) func(T) bool {
+func (ev *eval[T]) function(*functionExpr) func(T) bool {
ev.msgs = append(ev.msgs, "function not implemented")
return ev.alwaysFalse()
}
diff --git a/internal/gaby/actionlog.go b/internal/gaby/actionlog.go
index 3880fa6..1852df0 100644
--- a/internal/gaby/actionlog.go
+++ b/internal/gaby/actionlog.go
@@ -10,11 +10,13 @@
"fmt"
"net/http"
"strconv"
+ "strings"
"time"
"github.com/google/safehtml"
"github.com/google/safehtml/template"
"golang.org/x/oscar/internal/actions"
+ "golang.org/x/oscar/internal/filter"
"golang.org/x/oscar/internal/storage"
)
@@ -24,6 +26,7 @@
Start, End endpoint
StartTime, EndTime string // formatted times that the endpoints describe
+ Filter string
Entries []*actions.Entry
}
@@ -48,6 +51,7 @@
var page actionLogPage
// Fill in the endpoint values from the form on the page.
+ page.Filter = r.FormValue("filter")
page.Start.formValues(r, "start")
page.End.formValues(r, "end")
@@ -66,7 +70,11 @@
// Retrieve and display entries if something was set.
if r.FormValue("start") != "" {
- page.Entries = g.actionsBetween(startTime, endTime)
+ filter, err := newFilter(page.Filter)
+ if err != nil {
+ return nil, http.StatusBadRequest, fmt.Errorf("invalid filter: %v", err)
+ }
+ page.Entries = g.actionsBetween(startTime, endTime, filter)
for _, e := range page.Entries {
e.Created = e.Created.In(loc)
e.Done = e.Done.In(loc)
@@ -206,18 +214,40 @@
}
// actionsBetween returns the action entries between start and end, inclusive.
-func (g *Gaby) actionsBetween(start, end time.Time) []*actions.Entry {
+func (g *Gaby) actionsBetween(start, end time.Time, filter func(*actions.Entry) bool) []*actions.Entry {
var es []*actions.Entry
// Scan entries created in [start, end].
for e := range actions.ScanAfter(g.slog, g.db, start.Add(-time.Nanosecond), nil) {
if e.Created.After(end) {
break
}
- es = append(es, e)
+ if filter(e) {
+ es = append(es, e)
+ }
}
return es
}
+func newFilter(s string) (func(*actions.Entry) bool, error) {
+ if s == "" {
+ return func(*actions.Entry) bool { return true }, nil
+ }
+ expr, err := filter.ParseFilter(s)
+ if err != nil {
+ return nil, err
+ }
+ ev, problems := filter.Evaluator[actions.Entry](expr, nil)
+ if len(problems) > 0 {
+ return nil, errors.New(strings.Join(problems, "\n"))
+ }
+ return func(e *actions.Entry) bool {
+ if e == nil {
+ return false
+ }
+ return ev(*e)
+ }, nil
+}
+
// fmtTime formats a time for display on the action log page.
func fmtTime(t time.Time) string {
if t.IsZero() {
diff --git a/internal/gaby/actionlog_test.go b/internal/gaby/actionlog_test.go
index a4833bf..9666ed2 100644
--- a/internal/gaby/actionlog_test.go
+++ b/internal/gaby/actionlog_test.go
@@ -6,6 +6,7 @@
import (
"context"
+ "slices"
"strings"
"testing"
"time"
@@ -142,8 +143,67 @@
time.Sleep(100 * time.Millisecond)
before(db, []byte{2}, nil, false)
- got := g.actionsBetween(start, end)
+ got := g.actionsBetween(start, end, func(*actions.Entry) bool { return true })
if len(got) != 1 {
t.Errorf("got %d entries, want 1", len(got))
}
}
+
+func TestActionFilter(t *testing.T) {
+ entries := []*actions.Entry{
+ {Kind: "a", Error: ""},
+ {Kind: "b", Error: ""},
+ {Kind: "a", Error: "a bad error"},
+ {Kind: "a", Error: "meh"},
+ {Kind: "b", Error: "bad"},
+ {Kind: "a", ApprovalRequired: true},
+ {Kind: "b", ApprovalRequired: true},
+ }
+
+ applyFilter := func(f func(*actions.Entry) bool) []*actions.Entry {
+ var res []*actions.Entry
+ for _, e := range entries {
+ if f(e) {
+ res = append(res, e)
+ }
+ }
+ return res
+ }
+
+ for _, tc := range []struct {
+ in string
+ want func(*actions.Entry) bool
+ }{
+ {"", func(*actions.Entry) bool { return true }},
+ {
+ "Kind=a",
+ func(e *actions.Entry) bool { return e.Kind == "a" },
+ },
+ {
+ "Kind=a Error:bad",
+ func(e *actions.Entry) bool {
+ return e.Kind == "a" && strings.Contains(e.Error, "bad")
+ },
+ },
+ {
+ "ApprovalRequired=true",
+ func(e *actions.Entry) bool { return e.ApprovalRequired },
+ },
+ {
+ "ApprovalRequired=true OR Kind=b",
+ func(e *actions.Entry) bool {
+ return e.ApprovalRequired || e.Kind == "b"
+ },
+ },
+ } {
+ gotf, err := newFilter(tc.in)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := applyFilter(gotf)
+ want := applyFilter(tc.want)
+ if !slices.Equal(got, want) {
+ t.Errorf("%q:\ngot %v\nwant %v", tc.in, got, want)
+ }
+ }
+}
diff --git a/internal/gaby/tmpl/actionlog.tmpl b/internal/gaby/tmpl/actionlog.tmpl
index e4dd08d..7e600ba 100644
--- a/internal/gaby/tmpl/actionlog.tmpl
+++ b/internal/gaby/tmpl/actionlog.tmpl
@@ -33,6 +33,16 @@
<script>
document.getElementById("form-tz").setAttribute("value", timezone);
</script>
+ <span>
+ <label for="filter">Filter</label>
+ <input id="filter" type="text" size=75 name="filter" value="{{.Filter}}"/>
+ </span>
+ <div style="font-size: smaller; margin-top: 0.5rem; margin-bottom: 1rem">
+ Examples: <code>Kind:Fixer</code>, <code>RequiresApproval=true</code><br/>
+ Case matters. Boolean fields must be compared with <code>=true</code> or <code>=false</code>.
+ OR, AND and NOT must be in all caps.<br/>
+ See <a href="https://google.aip.dev/160">AIP 160</a> for more.
+ </div>
<table>
<tr>
<td>