internal/reviews: add filter box to display of open changes

This adds a new form input field to the top of the display of open changes.
The new field may be used as a filter expression that selects
the changes to display. For example, entering "Ian Lance Taylor"
will show all CLs with a field (typically a DisplayName field)
matching "Ian Lance Taylor". Or "NOT Predicates.Name:hasUnresolvedComments"
will show all CLs that do not have the hasUnresolvedComments predicate
(that is, will show all CLs for which all comments are resolved).

Change-Id: Id29ee69a5228bca9d5730e71929be65a3fba9127
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/650018
Reviewed-by: Ian Lance Taylor <iant@google.com>
Auto-Submit: Ian Lance Taylor <iant@google.com>
Commit-Queue: Ian Lance Taylor <iant@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/gaby/reviews.go b/internal/gaby/reviews.go
index 9629715..ef5958e 100644
--- a/internal/gaby/reviews.go
+++ b/internal/gaby/reviews.go
@@ -16,5 +16,5 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	c.Display(w, r)
+	c.Display(reviewsID.Endpoint(), w, r)
 }
diff --git a/internal/goreviews/changes.go b/internal/goreviews/changes.go
index 2863c98..fb6bb03 100644
--- a/internal/goreviews/changes.go
+++ b/internal/goreviews/changes.go
@@ -54,10 +54,10 @@
 const displayDoc template.HTML = `Excluding those marked WIP, having hashtags "wait-author", "wait-release", "wait-issue", or description containing "DO NOT REVIEW".`
 
 // Display is an HTTP handler that displays the dashboard.
-func (ch *Changes) Display(w http.ResponseWriter, r *http.Request) {
+func (ch *Changes) Display(endpoint string, w http.ResponseWriter, r *http.Request) {
 	ch.mu.Lock()
 	cps := ch.cps
 	ch.mu.Unlock()
 
-	reviews.Display(ch.slog, displayDoc, cps, w, r)
+	reviews.Display(ch.slog, displayDoc, endpoint, cps, w, r)
 }
diff --git a/internal/reviews/display.go b/internal/reviews/display.go
index 08f2e47..5974ab1 100644
--- a/internal/reviews/display.go
+++ b/internal/reviews/display.go
@@ -5,34 +5,72 @@
 package reviews
 
 import (
+	"errors"
+	"fmt"
 	"html/template"
 	"io"
 	"log/slog"
 	"net/http"
+	"strings"
+
+	"golang.org/x/oscar/internal/filter"
 )
 
 // Display is an HTTP handler function that displays the
 // changes to review.
-func Display(lg *slog.Logger, doc template.HTML, cps []ChangePreds, w http.ResponseWriter, r *http.Request) {
+func Display(lg *slog.Logger, doc template.HTML, endpoint string, cps []ChangePreds, w http.ResponseWriter, r *http.Request) {
 	if len(cps) == 0 {
 		io.WriteString(w, reportNoData)
 		return
 	}
 
-	sc := make([]CP, len(cps))
-	for i, v := range cps {
-		sc[i] = CP{v}
+	userFilter := r.FormValue("filter")
+	filterFn, err := makeFilter(userFilter)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("invalid filter: %v", err), http.StatusBadRequest)
+		return
+	}
+
+	sc := make([]CP, 0, len(cps))
+	for _, v := range cps {
+		if filterFn != nil && !filterFn(v) {
+			continue
+		}
+
+		sc = append(sc, CP{v})
 	}
 
 	data := &displayType{
-		Doc:     doc,
-		Changes: sc,
+		Endpoint: endpoint,
+		Doc:      doc,
+		Filter:   userFilter,
+		Changes:  sc,
 	}
 	if err := displayTemplate.Execute(w, data); err != nil {
 		lg.Error("template execution failed", "err", err)
 	}
 }
 
+// makeFilter turns the user-specific filter string into a filter function.
+// This returns nil if there is nothing to filter.
+func makeFilter(s string) (func(ChangePreds) bool, error) {
+	if s == "" {
+		return nil, nil
+	}
+	expr, err := filter.ParseFilter(s)
+	if err != nil {
+		return nil, err
+	}
+	ev, problems := filter.Evaluator[ChangePreds](expr, nil)
+	if len(problems) > 0 {
+		return nil, errors.New(strings.Join(problems, "\n"))
+	}
+	fn := func(cp ChangePreds) bool {
+		return ev(cp)
+	}
+	return fn, nil
+}
+
 // reportNoData is the HTML that we report if we have not finished
 // applying predicates to changes.
 const reportNoData = `
@@ -56,8 +94,10 @@
 
 // displayType is the type expected by displayHTML
 type displayType struct {
-	Doc     template.HTML
-	Changes []CP
+	Endpoint string        // Relative URL being served.
+	Doc      template.HTML // Documentation string.
+	Filter   string        // Filter value.
+	Changes  []CP          // List of changes with predicates.
 }
 
 // CP is the type we pass to the HTML template displayTemplate.
@@ -202,6 +242,14 @@
         <a href="https://go.googlesource.com/oscar/+/master/internal/goreviews/display.go">Source code</a>
       </div>
     </header>
+  <div class="filter">
+    <form id="form" action="{{.Endpoint}}" method="GET">
+      <span>
+        <label for="filter">Filter</label>
+        <input id="filter" type="text" size=75 name="filter" value="{{.Filter}}"/>
+      </span>
+    </form>
+  </div>
   {{range $change := .Changes}}
     <div class="row">
       <span class="date">{{.FormattedLastUpdate}}</span>