aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2023-05-09 18:25:02 +0200
committerAleksandr Nogikh <wp32pw@gmail.com>2023-05-10 09:02:53 +0200
commitdfd5a9acc134f1ea849a49efee7dd7f50c836e75 (patch)
treeac41d021e09d5b8ac2c70938d59432092415e798
parent1964022bd4ae3c35688b98f6a4db45076c7d002c (diff)
dashboard: support filtering over multiple labels
For each label, allow only one value to be specified. At the same time, allow multiple different labels (subsystem, origin, prio, etc) be specified together.
-rw-r--r--dashboard/app/main.go74
-rw-r--r--dashboard/app/main_test.go48
-rw-r--r--dashboard/app/templates.html11
-rw-r--r--pkg/html/html.go29
-rw-r--r--pkg/html/html_test.go49
5 files changed, 183 insertions, 28 deletions
diff --git a/dashboard/app/main.go b/dashboard/app/main.go
index d63856455..db19e2c62 100644
--- a/dashboard/app/main.go
+++ b/dashboard/app/main.go
@@ -82,15 +82,15 @@ type uiMainPage struct {
type uiBugFilter struct {
Filter *userBugFilter
- DropURL func(string) string
+ DropURL func(string, string) string
}
func makeUIBugFilter(c context.Context, filter *userBugFilter) *uiBugFilter {
url := getCurrentURL(c)
return &uiBugFilter{
Filter: filter,
- DropURL: func(name string) string {
- return html.AmendURL(url, name, "")
+ DropURL: func(name, value string) string {
+ return html.DropParam(url, name, value)
},
}
}
@@ -386,17 +386,20 @@ type uiJob struct {
type userBugFilter struct {
Manager string // show bugs that happened on the manager
OnlyManager string // show bugs that happened ONLY on the manager
- Label string // TODO: support multiple.
+ Labels []string
NoSubsystem bool
}
-func MakeBugFilter(r *http.Request) *userBugFilter {
+func MakeBugFilter(r *http.Request) (*userBugFilter, error) {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
return &userBugFilter{
NoSubsystem: r.FormValue("no_subsystem") != "",
Manager: r.FormValue("manager"),
OnlyManager: r.FormValue("only_manager"),
- Label: r.FormValue("label"),
- }
+ Labels: r.Form["label"],
+ }, nil
}
func (filter *userBugFilter) MatchManagerName(name string) bool {
@@ -427,9 +430,11 @@ func (filter *userBugFilter) MatchBug(bug *Bug) bool {
if filter.NoSubsystem && len(bug.LabelValues(SubsystemLabel)) > 0 {
return false
}
- if filter.Label != "" {
- label, value := splitLabel(filter.Label)
- return bug.HasLabel(label, value)
+ for _, rawLabel := range filter.Labels {
+ label, value := splitLabel(rawLabel)
+ if !bug.HasLabel(label, value) {
+ return false
+ }
}
return true
}
@@ -443,7 +448,7 @@ func (filter *userBugFilter) Any() bool {
if filter == nil {
return false
}
- return filter.Label != "" || filter.OnlyManager != "" || filter.Manager != "" || filter.NoSubsystem
+ return len(filter.Labels) > 0 || filter.OnlyManager != "" || filter.Manager != "" || filter.NoSubsystem
}
// handleMain serves main page.
@@ -453,7 +458,10 @@ func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error
return err
}
accessLevel := accessLevel(c, r)
- filter := MakeBugFilter(r)
+ filter, err := MakeBugFilter(r)
+ if err != nil {
+ return fmt.Errorf("%w: failed to parse URL parameters", ErrClientBadRequest)
+ }
managers, err := loadManagers(c, accessLevel, hdr.Namespace, filter)
if err != nil {
return err
@@ -516,10 +524,12 @@ func handleSubsystemPage(c context.Context, w http.ResponseWriter, r *http.Reque
}
groups, err := fetchNamespaceBugs(c, accessLevel(c, r),
hdr.Namespace, &userBugFilter{
- Label: BugLabel{
- Label: SubsystemLabel,
- Value: subsystem.Name,
- }.String(),
+ Labels: []string{
+ BugLabel{
+ Label: SubsystemLabel,
+ Value: subsystem.Name,
+ }.String(),
+ },
})
if err != nil {
return err
@@ -584,7 +594,10 @@ func handleTerminalBugList(c context.Context, w http.ResponseWriter, r *http.Req
return err
}
hdr.Subpage = typ.Subpage
- typ.Filter = MakeBugFilter(r)
+ typ.Filter, err = MakeBugFilter(r)
+ if err != nil {
+ return fmt.Errorf("%w: failed to parse URL parameters", ErrClientBadRequest)
+ }
extraBugs := []*Bug{}
if typ.Status == BugStatusFixed {
// Mix in bugs that have pending fixes.
@@ -873,8 +886,9 @@ func makeBugLabelUI(c context.Context, bug *Bug, entry BugLabel) *uiBugLabel {
if !strings.HasPrefix(url, "/"+bug.Namespace) {
link = fmt.Sprintf("/%s", bug.Namespace)
}
- link = html.AmendURL(link, "label", filterValue)
-
+ link = html.TransformURL(link, "label", func(oldLabels []string) []string {
+ return mergeLabelSet(oldLabels, entry.String())
+ })
ret := &uiBugLabel{
Name: filterValue,
Link: link,
@@ -892,6 +906,23 @@ func makeBugLabelUI(c context.Context, bug *Bug, entry BugLabel) *uiBugLabel {
return ret
}
+func mergeLabelSet(oldLabels []string, newLabel string) []string {
+ // Leave only one label for each type.
+ labelsMap := map[BugLabelType]string{}
+ for _, rawLabel := range append(oldLabels, newLabel) {
+ label, value := splitLabel(rawLabel)
+ labelsMap[label] = value
+ }
+ var ret []string
+ for label, value := range labelsMap {
+ ret = append(ret, BugLabel{
+ Label: label,
+ Value: value,
+ }.String())
+ }
+ return ret
+}
+
func getBugDiscussionsUI(c context.Context, bug *Bug) ([]*uiBugDiscussion, error) {
// TODO: also include dup bug discussions.
// TODO: limit the number of DiscussionReminder type entries, e.g. all with
@@ -1386,8 +1417,9 @@ func fetchTerminalBugs(c context.Context, accessLevel AccessLevel,
func applyBugFilter(query *db.Query, filter *userBugFilter) *db.Query {
manager := filter.ManagerName()
- label, value := splitLabel(filter.Label)
- if label != EmptyLabel {
+ if len(filter.Labels) > 0 {
+ // Take just the first one.
+ label, value := splitLabel(filter.Labels[0])
query = query.Filter("Labels.Label=", string(label))
query = query.Filter("Labels.Value=", value)
} else if manager != "" {
diff --git a/dashboard/app/main_test.go b/dashboard/app/main_test.go
index be1b3e847..fff94d7f9 100644
--- a/dashboard/app/main_test.go
+++ b/dashboard/app/main_test.go
@@ -247,3 +247,51 @@ func TestSubsystemPage(t *testing.T) {
assert.Contains(t, string(reply), crash1.Title)
assert.NotContains(t, string(reply), crash2.Title)
}
+
+func TestMultiLabelFilter(t *testing.T) {
+ c := NewCtx(t)
+ defer c.Close()
+
+ client := c.makeClient(clientPublicEmail, keyPublicEmail, true)
+ mailingList := config.Namespaces["access-public-email"].Reporting[0].Config.(*EmailConfig).Email
+
+ build1 := testBuild(1)
+ build1.Manager = "manager-name-123"
+ client.UploadBuild(build1)
+
+ crash1 := testCrash(build1, 1)
+ crash1.GuiltyFiles = []string{"a.c"}
+ crash1.Title = "crash-with-subsystem-A"
+ client.ReportCrash(crash1)
+ c.pollEmailBug()
+
+ crash2 := testCrash(build1, 2)
+ crash2.GuiltyFiles = []string{"a.c"}
+ crash2.Title = "prio-crash-subsystem-A"
+ client.ReportCrash(crash2)
+
+ c.incomingEmail(c.pollEmailBug().Sender, "#syz set prio: low\n",
+ EmailOptFrom("test@requester.com"), EmailOptCC([]string{mailingList}))
+
+ // The normal main page.
+ reply, err := c.AuthGET(AccessAdmin, "/access-public-email")
+ c.expectOK(err)
+ assert.Contains(t, string(reply), build1.Manager)
+ assert.NotContains(t, string(reply), "Applied filters")
+
+ reply, err = c.AuthGET(AccessAdmin, "/access-public-email?label=subsystems:subsystemA")
+ c.expectOK(err)
+ assert.Contains(t, string(reply), "Applied filters") // we're seeing a prompt to disable the filter
+ assert.Contains(t, string(reply), crash1.Title)
+ assert.Contains(t, string(reply), crash2.Title)
+
+ // Test filters together.
+ reply, err = c.AuthGET(AccessAdmin, "/access-public-email?label=subsystems:subsystemA&&label=prio:low")
+ c.expectOK(err)
+ assert.NotContains(t, string(reply), crash1.Title)
+ assert.Contains(t, string(reply), crash2.Title)
+
+ // Ensure we provide links that drop labels.
+ assert.NotContains(t, string(reply), "/access-public-email?label=subsystems:subsystemA\"")
+ assert.NotContains(t, string(reply), "/access-public-email?label=prop:low\"")
+}
diff --git a/dashboard/app/templates.html b/dashboard/app/templates.html
index 044f0fd42..001eec25e 100644
--- a/dashboard/app/templates.html
+++ b/dashboard/app/templates.html
@@ -94,16 +94,17 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the
{{if .Filter.Any}}
<b>Applied filters: </b>
{{if .Filter.Manager}}
- Manager={{.Filter.Manager}} ({{link (call .DropURL "manager") "drop"}})
+ Manager={{.Filter.Manager}} ({{link (call .DropURL "manager" "") "drop"}})
{{end}}
{{if .Filter.OnlyManager}}
- Only Manager={{.Filter.OnlyManager}} ({{link (call .DropURL "only_manager") "drop"}})
+ Only Manager={{.Filter.OnlyManager}} ({{link (call .DropURL "only_manager" "") "drop"}})
{{end}}
{{if .Filter.NoSubsystem}}
- NoSubsystem={{.Filter.NoSubsystem}} ({{link (call .DropURL "no_subsystem") "drop"}})
+ NoSubsystem={{.Filter.NoSubsystem}} ({{link (call .DropURL "no_subsystem" "") "drop"}})
{{end}}
- {{if .Filter.Label}}
- Label={{.Filter.Label}} ({{link (call .DropURL "label") "drop"}})
+ {{$drop := .DropURL}}
+ {{range .Filter.Labels}}
+ Label={{.}} ({{link (call $drop "label" .) "drop"}})
{{end}}
<br>
{{end}}
diff --git a/pkg/html/html.go b/pkg/html/html.go
index 72c2956e0..930fdd923 100644
--- a/pkg/html/html.go
+++ b/pkg/html/html.go
@@ -194,6 +194,30 @@ func commitLink(repo, commit string) string {
}
func AmendURL(baseURL, key, value string) string {
+ return TransformURL(baseURL, key, func(_ []string) []string {
+ if value == "" {
+ return nil
+ }
+ return []string{value}
+ })
+}
+
+func DropParam(baseURL, key, value string) string {
+ return TransformURL(baseURL, key, func(oldValues []string) []string {
+ if value == "" {
+ return nil
+ }
+ var newValues []string
+ for _, iterVal := range oldValues {
+ if iterVal != value {
+ newValues = append(newValues, iterVal)
+ }
+ }
+ return newValues
+ })
+}
+
+func TransformURL(baseURL, key string, f func([]string) []string) string {
if baseURL == "" {
return ""
}
@@ -202,10 +226,11 @@ func AmendURL(baseURL, key, value string) string {
return ""
}
values := parsed.Query()
- if value == "" {
+ ret := f(values[key])
+ if len(ret) == 0 {
values.Del(key)
} else {
- values.Set(key, value)
+ values[key] = ret
}
parsed.RawQuery = values.Encode()
return parsed.String()
diff --git a/pkg/html/html_test.go b/pkg/html/html_test.go
new file mode 100644
index 000000000..d824e0246
--- /dev/null
+++ b/pkg/html/html_test.go
@@ -0,0 +1,49 @@
+// Copyright 2023 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package html
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDropParam(t *testing.T) {
+ tests := []struct {
+ in string
+ key string
+ value string
+ out string
+ }{
+ {
+ in: `/upstream?first=a&second=b`,
+ key: `first`,
+ value: ``,
+ out: `/upstream?second=b`,
+ },
+ {
+ in: `/upstream?first=a&first=b&second=c`,
+ key: `second`,
+ value: ``,
+ out: `/upstream?first=a&first=b`,
+ },
+ {
+ in: `/upstream?first=a&first=b&second=c`,
+ key: `first`,
+ value: ``,
+ out: `/upstream?second=c`,
+ },
+ {
+ in: `/upstream?first=a&first=b&second=c`,
+ key: `first`,
+ value: `b`,
+ out: `/upstream?first=a&second=c`,
+ },
+ }
+
+ for _, test := range tests {
+ got := DropParam(test.in, test.key, test.value)
+ assert.Equal(t, test.out, got)
+ }
+}