From 3ef496b7ba14f5099f3d09f9da0e931411c2afc0 Mon Sep 17 00:00:00 2001 From: Dmitry Vyukov Date: Fri, 5 Apr 2019 17:59:52 +0200 Subject: dashboard/app: split dashboard per-namespace We now have too many namespaces and bugs. Main page takes infinity to load. Also almost nobody is interested in more than 1 namespace. So split main page per-namespaces. --- dashboard/app/access_test.go | 30 ++++- dashboard/app/admin.html | 4 + dashboard/app/app.yaml | 9 +- dashboard/app/app_test.go | 27 ++++- dashboard/app/bug.html | 6 +- dashboard/app/config.go | 5 + dashboard/app/entities.go | 10 +- dashboard/app/fixed.html | 19 +++ dashboard/app/handler.go | 142 +++++++++++++++++++++- dashboard/app/main.go | 227 ++++++++++++++++++++---------------- dashboard/app/main.html | 17 +-- dashboard/app/static/common.js | 4 +- dashboard/app/static/style.css | 8 +- dashboard/app/templates.html | 35 +++++- dashboard/app/util_test.go | 12 +- docs/akaros/found_bugs.md | 2 +- docs/linux/found_bugs.md | 2 +- docs/linux/reporting_kernel_bugs.md | 2 +- docs/netbsd/README.md | 2 +- docs/netbsd/found_bugs.md | 2 +- docs/openbsd/found_bugs.md | 2 +- docs/syzbot.md | 2 +- pkg/html/generated.go | 6 + 23 files changed, 425 insertions(+), 150 deletions(-) create mode 100644 dashboard/app/fixed.html diff --git a/dashboard/app/access_test.go b/dashboard/app/access_test.go index c331fa242..7765f0c06 100644 --- a/dashboard/app/access_test.go +++ b/dashboard/app/access_test.go @@ -53,10 +53,34 @@ func TestAccess(t *testing.T) { url string // url at which this entity can be requested. } entities := []entity{ + // Main pages. { - // Main page. - level: config.AccessLevel, - url: "/", + level: AccessAdmin, + url: "/admin", + }, + { + level: AccessPublic, + url: "/access-public", + }, + { + level: AccessPublic, + url: "/access-public/fixed", + }, + { + level: AccessUser, + url: "/access-user", + }, + { + level: AccessUser, + url: "/access-user/fixed", + }, + { + level: AccessAdmin, + url: "/access-admin", + }, + { + level: AccessAdmin, + url: "/access-admin/fixed", }, { // Any references to namespace, reporting, links, etc. diff --git a/dashboard/app/admin.html b/dashboard/app/admin.html index 09fd67513..edbdc9f7e 100644 --- a/dashboard/app/admin.html +++ b/dashboard/app/admin.html @@ -26,6 +26,7 @@ Main page. + @@ -36,6 +37,8 @@ Main page. + + {{range $job := $.Jobs}} @@ -74,6 +77,7 @@ Main page. {{end}} +
Recent jobs:
Bug CreatedManager Result
{{$job.BugTitle}}


diff --git a/dashboard/app/app.yaml b/dashboard/app/app.yaml index 7c8d90ab9..0e5ca351f 100644 --- a/dashboard/app/app.yaml +++ b/dashboard/app/app.yaml @@ -19,12 +19,6 @@ handlers: - url: /static static_dir: static secure: always -- url: /(|bug|text|x/.+) - script: _go_app - secure: always -- url: /(api) - script: _go_app - secure: always - url: /(admin|email_poll) script: _go_app login: admin @@ -32,3 +26,6 @@ handlers: - url: /_ah/(mail/.+|bounce) script: _go_app login: admin +- url: /(|api|bug|text|x/.+|.*) + script: _go_app + secure: always diff --git a/dashboard/app/app_test.go b/dashboard/app/app_test.go index 6d0e887a7..8cc3bce80 100644 --- a/dashboard/app/app_test.go +++ b/dashboard/app/app_test.go @@ -7,6 +7,7 @@ package dash import ( "fmt" + "net/http" "strconv" "strings" "testing" @@ -30,6 +31,7 @@ var testConfig = &GlobalConfig{ EmailBlacklist: []string{ "\"Bar\" ", }, + DefaultNamespace: "test1", Namespaces: map[string]*Config{ "test1": { AccessLevel: AccessAdmin, @@ -288,7 +290,7 @@ func TestApp(t *testing.T) { c := NewCtx(t) defer c.Close() - c.expectOK(c.GET("/")) + c.expectOK(c.GET("/test1")) apiClient1 := c.makeClient(client1, key1, false) apiClient2 := c.makeClient(client2, key2, false) @@ -337,6 +339,29 @@ func TestApp(t *testing.T) { }) } +func TestRedirects(t *testing.T) { + c := NewCtx(t) + defer c.Close() + + checkRedirect(c, AccessUser, "/", "/test1") // redirect to default namespace + checkRedirect(c, AccessAdmin, "/", "/admin") + + _, err := c.httpRequest("GET", "/access-user", "", AccessPublic) // not accessible namespace + c.expectForbidden(err) + + _, err = c.httpRequest("GET", "/access-user", "", AccessUser) + c.expectOK(err) +} + +func checkRedirect(c *Ctx, accessLevel AccessLevel, from, to string) { + _, err := c.httpRequest("GET", from, "", accessLevel) + c.expectNE(err, nil) + httpErr, ok := err.(HttpError) + c.expectTrue(ok) + c.expectEQ(httpErr.Code, http.StatusMovedPermanently) + c.expectEQ(httpErr.Headers["Location"], []string{to}) +} + // Test purging of old crashes for bugs with lots of crashes. func TestPurgeOldCrashes(t *testing.T) { if testing.Short() { diff --git a/dashboard/app/bug.html b/dashboard/app/bug.html index ab32d8e54..6bbf30fc7 100644 --- a/dashboard/app/bug.html +++ b/dashboard/app/bug.html @@ -14,7 +14,7 @@ Page with details about a single bug. {{template "header" .Header}} - [{{.Bug.Namespace}}] {{.Bug.Title}}
+ {{.Bug.Title}}
Status: {{if .Bug.ExternalLink}}{{.Bug.Status}}{{else}}{{.Bug.Status}}{{end}}
Reported-by: {{.Bug.CreditEmail}}
{{if .Bug.Commits}} @@ -71,6 +71,7 @@ Page with details about a single bug. + @@ -86,6 +87,8 @@ Page with details about a single bug. {{end}} + + {{range $c := $.Crashes}} @@ -104,6 +107,7 @@ Page with details about a single bug. {{end}} {{end}} +
All crashes ({{.Bug.NumCrashes}}):
Manager TimeMaintainers
{{$c.Manager}}
diff --git a/dashboard/app/config.go b/dashboard/app/config.go index 4b8a70a09..1676bf6ce 100644 --- a/dashboard/app/config.go +++ b/dashboard/app/config.go @@ -33,6 +33,8 @@ type GlobalConfig struct { Clients map[string]string // List of emails blacklisted from issuing test requests. EmailBlacklist []string + // Namespace that is shown by default (no namespace selected yet). + DefaultNamespace string // Per-namespace config. // Namespaces are a mechanism to separate groups of different kernels. // E.g. Debian 4.4 kernels and Ubuntu 4.9 kernels. @@ -191,6 +193,9 @@ func checkConfig(cfg *GlobalConfig) { clientNames := make(map[string]bool) checkClients(clientNames, cfg.Clients) checkConfigAccessLevel(&cfg.AccessLevel, AccessPublic, "global") + if cfg.Namespaces[cfg.DefaultNamespace] == nil { + panic(fmt.Sprintf("default namespace %q is not found", cfg.DefaultNamespace)) + } for ns, cfg := range cfg.Namespaces { checkNamespace(ns, cfg, namespaces, clientNames) } diff --git a/dashboard/app/entities.go b/dashboard/app/entities.go index b1fd89d32..c6d26c476 100644 --- a/dashboard/app/entities.go +++ b/dashboard/app/entities.go @@ -302,16 +302,18 @@ func updateManager(c context.Context, ns, name string, fn func(mgr *Manager, sta return db.RunInTransaction(c, tx, &db.TransactionOptions{Attempts: 10}) } -func loadAllManagers(c context.Context) ([]*Manager, []*db.Key, error) { +func loadAllManagers(c context.Context, ns string) ([]*Manager, []*db.Key, error) { var managers []*Manager - keys, err := db.NewQuery("Manager"). - GetAll(c, &managers) + query := db.NewQuery("Manager") + if ns != "" { + query = query.Filter("Namespace=", ns) + } + keys, err := query.GetAll(c, &managers) if err != nil { return nil, nil, fmt.Errorf("failed to query managers: %v", err) } var result []*Manager var resultKeys []*db.Key - for i, mgr := range managers { if config.Namespaces[mgr.Namespace].Managers[mgr.Name].Decommissioned { continue diff --git a/dashboard/app/fixed.html b/dashboard/app/fixed.html new file mode 100644 index 000000000..4a514583f --- /dev/null +++ b/dashboard/app/fixed.html @@ -0,0 +1,19 @@ +{{/* +Copyright 2019 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. + +Fixed bugs page. +*/}} + + + + + {{template "head" .Header}} + syzbot + + + {{template "header" .Header}} + + {{template "bug_list" .Bugs}} + + diff --git a/dashboard/app/handler.go b/dashboard/app/handler.go index 7fc133bee..8e2d66ad7 100644 --- a/dashboard/app/handler.go +++ b/dashboard/app/handler.go @@ -5,7 +5,12 @@ package dash import ( "bytes" + "encoding/base64" + "encoding/json" + "fmt" "net/http" + "sort" + "strings" "github.com/google/syzkaller/pkg/html" "golang.org/x/net/context" @@ -26,11 +31,12 @@ func handleContext(fn contextHandler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) if err := fn(c, w, r); err != nil { + hdr, _ := commonHeader(c, r, w, "") data := &struct { Header *uiHeader Error string }{ - Header: commonHeader(c, r), + Header: hdr, Error: err.Error(), } if err == ErrAccess { @@ -41,6 +47,10 @@ func handleContext(fn contextHandler) http.Handler { } return } + if redir, ok := err.(ErrRedirect); ok { + http.Redirect(w, r, redir.Error(), http.StatusMovedPermanently) + return + } if _, dontlog := err.(ErrDontLog); !dontlog { log.Errorf(c, "%v", err) } @@ -52,7 +62,10 @@ func handleContext(fn contextHandler) http.Handler { }) } -type ErrDontLog error +type ( + ErrDontLog error + ErrRedirect error +) func handleAuth(fn contextHandler) contextHandler { return func(c context.Context, w http.ResponseWriter, r *http.Request) error { @@ -76,17 +89,136 @@ type uiHeader struct { Admin bool LoginLink string AnalyticsTrackingID string + Subpage string + Namespace string + Namespaces []uiNamespace + Redirects []uiRedirect +} + +type uiNamespace struct { + Name string + Caption string +} + +type uiRedirect struct { + From string + To string +} + +type cookieData struct { + Namespace string `json:"namespace"` } -func commonHeader(c context.Context, r *http.Request) *uiHeader { +func commonHeader(c context.Context, r *http.Request, w http.ResponseWriter, ns string) (*uiHeader, error) { + accessLevel := accessLevel(c, r) + if ns == "" { + ns = strings.ToLower(r.URL.Path) + if ns != "" && ns[0] == '/' { + ns = ns[1:] + } + if pos := strings.IndexByte(ns, '/'); pos != -1 { + ns = ns[:pos] + } + } h := &uiHeader{ - Admin: accessLevel(c, r) == AccessAdmin, + Admin: accessLevel == AccessAdmin, AnalyticsTrackingID: config.AnalyticsTrackingID, } + const adminPage = "admin" + isAdminPage := r.URL.Path == "/"+adminPage + isBugPage := r.URL.Path == "/bug" + found := false + for ns1, cfg := range config.Namespaces { + if accessLevel < cfg.AccessLevel { + if ns1 == ns { + return nil, ErrAccess + } + continue + } + if ns1 == ns { + found = true + } + h.Namespaces = append(h.Namespaces, uiNamespace{ + Name: ns1, + Caption: cfg.DisplayTitle, + }) + // This handles redirects from old URL scheme to new scheme. + // This this should be removed at some point (Apr 5, 2019). + // Also see handling of "fixed" parameter in handleMain. + if isBugPage { + continue + } + h.Redirects = append(h.Redirects, uiRedirect{ + From: "#" + ns1, + To: "/" + ns1, + }) + fragments := []string{"managers", "open", "pending"} + for _, reporting := range cfg.Reporting { + if !reporting.moderation || accessLevel < reporting.AccessLevel { + continue + } + fragments = append(fragments, reporting.Name) + } + for _, frag := range fragments { + h.Redirects = append(h.Redirects, uiRedirect{ + From: "#" + ns1 + "-" + frag, + To: "/" + ns1 + "#" + frag, + }) + } + } + sort.Slice(h.Namespaces, func(i, j int) bool { + return h.Namespaces[i].Caption < h.Namespaces[j].Caption + }) + cookie := decodeCookie(r) + if !found { + ns = config.DefaultNamespace + if cfg := config.Namespaces[cookie.Namespace]; cfg != nil && cfg.AccessLevel <= accessLevel { + ns = cookie.Namespace + } + if accessLevel == AccessAdmin { + ns = adminPage + } + if ns != adminPage || !isAdminPage { + return nil, ErrRedirect(fmt.Errorf("/%v", ns)) + } + } + if ns != adminPage { + h.Namespace = ns + cookie.Namespace = ns + encodeCookie(w, cookie) + } if user.Current(c) == nil { h.LoginLink, _ = user.LoginURL(c, r.URL.String()) } - return h + return h, nil +} + +const cookieName = "syzkaller" + +func decodeCookie(r *http.Request) *cookieData { + cd := new(cookieData) + cookie, err := r.Cookie(cookieName) + if err != nil { + return cd + } + decoded, err := base64.StdEncoding.DecodeString(cookie.Value) + if err != nil { + return cd + } + json.Unmarshal(decoded, cd) + return cd +} + +func encodeCookie(w http.ResponseWriter, cd *cookieData) { + data, err := json.Marshal(cd) + if err != nil { + return + } + cookie := &http.Cookie{ + Name: cookieName, + Value: base64.StdEncoding.EncodeToString(data), + } + http.SetCookie(w, cookie) } var templates = html.CreateGlob("*.html") diff --git a/dashboard/app/main.go b/dashboard/app/main.go index fdf3904fc..88bcfce1e 100644 --- a/dashboard/app/main.go +++ b/dashboard/app/main.go @@ -36,12 +36,25 @@ func initHTTPHandlers() { http.Handle("/x/patch.diff", handlerWrapper(handleTextX(textPatch))) http.Handle("/x/bisect.txt", handlerWrapper(handleTextX(textLog))) http.Handle("/x/error.txt", handlerWrapper(handleTextX(textError))) + for ns := range config.Namespaces { + http.Handle("/"+ns, handlerWrapper(handleMain)) + http.Handle("/"+ns+"/fixed", handlerWrapper(handleFixed)) + } } -type uiMain struct { - Header *uiHeader - Now time.Time - BugNamespaces []*uiBugNamespace +type uiMainPage struct { + Header *uiHeader + Now time.Time + FixedLink string + FixedCount int + Managers []*uiManager + Groups []*uiBugGroup +} + +type uiFixedPage struct { + Header *uiHeader + Now time.Time + Bugs *uiBugGroup } type uiAdminPage struct { @@ -105,15 +118,6 @@ type uiBugPage struct { Crashes []*uiCrash } -type uiBugNamespace struct { - Name string - Caption string - FixedLink string - FixedCount int - Managers []*uiManager - Groups []*uiBugGroup -} - type uiBugGroup struct { Now time.Time Caption string @@ -191,40 +195,63 @@ type uiJob struct { // handleMain serves main page. func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error { - accessLevel := accessLevel(c, r) - var managers []*uiManager - if r.FormValue("fixed") == "" { - var err error - managers, err = loadManagers(c, accessLevel) - if err != nil { - return err - } + if ns := r.FormValue("fixed"); ns != "" { + http.Redirect(w, r, fmt.Sprintf("/%v/fixed", ns), http.StatusMovedPermanently) + return nil } - bugNamespaces, err := fetchBugs(c, r) + hdr, err := commonHeader(c, r, w, "") if err != nil { return err } - for _, ns := range bugNamespaces { - for _, mgr := range managers { - if ns.Name == mgr.Namespace { - ns.Managers = append(ns.Managers, mgr) - } - } + accessLevel := accessLevel(c, r) + managers, err := loadManagers(c, accessLevel, hdr.Namespace) + if err != nil { + return err } - data := &uiMain{ - Header: commonHeader(c, r), - Now: timeNow(c), - BugNamespaces: bugNamespaces, + groups, fixedCount, err := fetchNamespaceBugs(c, accessLevel, hdr.Namespace) + if err != nil { + return err + } + data := &uiMainPage{ + Header: hdr, + Now: timeNow(c), + FixedCount: fixedCount, + FixedLink: fmt.Sprintf("/%v/fixed", hdr.Namespace), + Groups: groups, + Managers: managers, } return serveTemplate(w, "main.html", data) } +func handleFixed(c context.Context, w http.ResponseWriter, r *http.Request) error { + accessLevel := accessLevel(c, r) + hdr, err := commonHeader(c, r, w, "") + if err != nil { + return err + } + hdr.Subpage = "/fixed" + bugs, err := fetchFixedBugs(c, accessLevel, hdr.Namespace) + if err != nil { + return err + } + data := &uiFixedPage{ + Header: hdr, + Now: timeNow(c), + Bugs: bugs, + } + return serveTemplate(w, "fixed.html", data) +} + func handleAdmin(c context.Context, w http.ResponseWriter, r *http.Request) error { accessLevel := accessLevel(c, r) if accessLevel != AccessAdmin { return ErrAccess } - managers, err := loadManagers(c, accessLevel) + hdr, err := commonHeader(c, r, w, "") + if err != nil { + return err + } + managers, err := loadManagers(c, accessLevel, "") if err != nil { return err } @@ -237,7 +264,7 @@ func handleAdmin(c context.Context, w http.ResponseWriter, r *http.Request) erro return err } data := &uiAdminPage{ - Header: commonHeader(c, r), + Header: hdr, Log: errorLog, Managers: managers, Jobs: jobs, @@ -266,6 +293,10 @@ func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error if err := checkAccessLevel(c, r, bug.sanitizeAccess(accessLevel)); err != nil { return err } + hdr, err := commonHeader(c, r, w, bug.Namespace) + if err != nil { + return err + } state, err := loadReportingState(c) if err != nil { return err @@ -326,7 +357,7 @@ func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error } } data := &uiBugPage{ - Header: commonHeader(c, r), + Header: hdr, Now: timeNow(c), Bug: uiBug, BisectCause: bisectCause, @@ -416,47 +447,21 @@ func textFilename(tag string) string { } } -func fetchBugs(c context.Context, r *http.Request) ([]*uiBugNamespace, error) { - state, err := loadReportingState(c) +func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string) ([]*uiBugGroup, int, error) { + var bugs []*Bug + _, err := db.NewQuery("Bug"). + Filter("Namespace=", ns). + GetAll(c, &bugs) if err != nil { - return nil, err - } - accessLevel := accessLevel(c, r) - onlyFixed := r.FormValue("fixed") - var res []*uiBugNamespace - for ns, cfg := range config.Namespaces { - if accessLevel < cfg.AccessLevel { - continue - } - if onlyFixed != "" && onlyFixed != ns { - continue - } - uiNamespace, err := fetchNamespaceBugs(c, accessLevel, ns, state, onlyFixed != "") - if err != nil { - return nil, err - } - res = append(res, uiNamespace) + return nil, 0, err } - sort.Slice(res, func(i, j int) bool { - return res[i].Caption < res[j].Caption - }) - return res, nil -} - -func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string, - state *ReportingState, onlyFixed bool) (*uiBugNamespace, error) { - query := db.NewQuery("Bug").Filter("Namespace=", ns) - if onlyFixed { - query = query.Filter("Status=", BugStatusFixed) - } - var bugs []*Bug - _, err := query.GetAll(c, &bugs) + state, err := loadReportingState(c) if err != nil { - return nil, err + return nil, 0, err } managers, err := managerList(c, ns) if err != nil { - return nil, err + return nil, 0, err } fixedCount := 0 groups := make(map[int][]*uiBug) @@ -465,8 +470,9 @@ func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string, for _, bug := range bugs { if bug.Status == BugStatusFixed { fixedCount++ + continue } - if bug.Status == BugStatusInvalid || bug.Status == BugStatusFixed != onlyFixed { + if bug.Status == BugStatusInvalid { continue } if accessLevel < bug.sanitizeAccess(accessLevel) { @@ -479,10 +485,8 @@ func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string, uiBug := createUIBug(c, bug, state, managers) bugMap[bug.keyHash()] = uiBug id := uiBug.ReportingIndex - if bug.Status == BugStatusFixed { + if len(uiBug.Commits) != 0 { id = -1 - } else if len(uiBug.Commits) != 0 { - id = -2 } groups[id] = append(groups[id], uiBug) } @@ -493,6 +497,7 @@ func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string, } mergeUIBug(c, bug, dup) } + cfg := config.Namespaces[ns] var uiGroups []*uiBugGroup for index, bugs := range groups { sort.Slice(bugs, func(i, j int) bool { @@ -504,27 +509,24 @@ func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string, } return bugs[i].ReportedTime.After(bugs[j].ReportedTime) }) - caption, fragment, showPatch, showPatched := "", "", false, false + caption, fragment, showPatched := "", "", false switch index { case -1: - caption, showPatch, showPatched = "fixed", true, false - case -2: - caption, showPatch, showPatched = "fix pending", false, true - fragment = ns + "-pending" - case len(config.Namespaces[ns].Reporting) - 1: - caption, showPatch, showPatched = "open", false, false - fragment = ns + "-open" + caption, showPatched = "fix pending", true + fragment = "pending" + case len(cfg.Reporting) - 1: + caption, showPatched = "open", false + fragment = "open" default: - reporting := &config.Namespaces[ns].Reporting[index] - caption, showPatch, showPatched = reporting.DisplayTitle, false, false - fragment = ns + "-" + reporting.Name + reporting := &cfg.Reporting[index] + caption, showPatched = reporting.DisplayTitle, false + fragment = reporting.Name } uiGroups = append(uiGroups, &uiBugGroup{ Now: timeNow(c), - Caption: fmt.Sprintf("%v (%v)", caption, len(bugs)), + Caption: caption, Fragment: fragment, Namespace: ns, - ShowPatch: showPatch, ShowPatched: showPatched, ShowIndex: index, Bugs: bugs, @@ -533,19 +535,42 @@ func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string, sort.Slice(uiGroups, func(i, j int) bool { return uiGroups[i].ShowIndex > uiGroups[j].ShowIndex }) - fixedLink := "" - if !onlyFixed { - fixedLink = fmt.Sprintf("?fixed=%v", ns) + return uiGroups, fixedCount, nil +} + +func fetchFixedBugs(c context.Context, accessLevel AccessLevel, ns string) (*uiBugGroup, error) { + var bugs []*Bug + _, err := db.NewQuery("Bug"). + Filter("Namespace=", ns). + Filter("Status=", BugStatusFixed). + GetAll(c, &bugs) + if err != nil { + return nil, err } - cfg := config.Namespaces[ns] - uiNamespace := &uiBugNamespace{ - Name: ns, - Caption: cfg.DisplayTitle, - FixedCount: fixedCount, - FixedLink: fixedLink, - Groups: uiGroups, + state, err := loadReportingState(c) + if err != nil { + return nil, err } - return uiNamespace, nil + managers, err := managerList(c, ns) + if err != nil { + return nil, err + } + res := &uiBugGroup{ + Now: timeNow(c), + Caption: "fixed", + ShowPatch: true, + Namespace: ns, + } + for _, bug := range bugs { + if accessLevel < bug.sanitizeAccess(accessLevel) { + continue + } + res.Bugs = append(res.Bugs, createUIBug(c, bug, state, managers)) + } + sort.Slice(res.Bugs, func(i, j int) bool { + return res.Bugs[i].ClosedTime.After(res.Bugs[j].ClosedTime) + }) + return res, nil } func loadDupsForBug(c context.Context, r *http.Request, bug *Bug, state *ReportingState, managers []string) ( @@ -788,10 +813,10 @@ func makeUIBuild(build *Build) *uiBuild { } } -func loadManagers(c context.Context, accessLevel AccessLevel) ([]*uiManager, error) { +func loadManagers(c context.Context, accessLevel AccessLevel, ns string) ([]*uiManager, error) { now := timeNow(c) date := timeDate(now) - managers, managerKeys, err := loadAllManagers(c) + managers, managerKeys, err := loadAllManagers(c, ns) if err != nil { return nil, err } @@ -877,7 +902,7 @@ func loadRecentJobs(c context.Context) ([]*uiJob, error) { var jobs []*Job keys, err := db.NewQuery("Job"). Order("-Created"). - Limit(40). + Limit(80). GetAll(c, &jobs) if err != nil { return nil, err diff --git a/dashboard/app/main.html b/dashboard/app/main.html index e1fc5a5ec..4c1191a09 100644 --- a/dashboard/app/main.html +++ b/dashboard/app/main.html @@ -14,17 +14,12 @@ Main page. {{template "header" .Header}} - {{range $ns := $.BugNamespaces}} -
-

{{$ns.Caption}}

- {{if $ns.FixedLink}} - fixed bugs ({{$ns.FixedCount}}) - {{end}} - {{template "manager_list" $ns.Managers}} - {{range $group := $ns.Groups}} - {{template "bug_list" $group}} - {{end}} -
+ {{if $.FixedLink}} + fixed bugs ({{$.FixedCount}}) + {{end}} + {{template "manager_list" $.Managers}} + {{range $group := $.Groups}} + {{template "bug_list" $group}} {{end}} diff --git a/dashboard/app/static/common.js b/dashboard/app/static/common.js index d3bd12fe6..f501d61da 100644 --- a/dashboard/app/static/common.js +++ b/dashboard/app/static/common.js @@ -2,7 +2,7 @@ // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. function sortTable(item, colName, conv, desc = false) { - table = item.parentNode.parentNode.parentNode; + table = item.parentNode.parentNode.parentNode.parentNode; rows = table.rows; col = findColumnByName(rows[0].getElementsByTagName("th"), colName); values = []; @@ -18,7 +18,7 @@ function sortTable(item, colName, conv, desc = false) { return 1; }); for (i = 0; i < values.length; i++) - table.appendChild(values[i][1]); + table.tBodies[0].appendChild(values[i][1]); return false; } diff --git a/dashboard/app/static/style.css b/dashboard/app/static/style.css index 9a1b15315..ee5ef5fe1 100644 --- a/dashboard/app/static/style.css +++ b/dashboard/app/static/style.css @@ -35,6 +35,12 @@ table td, table th { overflow: hidden; } +.namespace { + font-weight: bold; + font-size: large; + color: #375EAB; +} + .position_table { border: 0px; margin: 0px; @@ -59,7 +65,7 @@ table td, table th { background: #F4F4F4; } -.list_table tr:nth-child(2n+1) { +.list_table tr:nth-child(2n) { background: #F4F4F4; } diff --git a/dashboard/app/templates.html b/dashboard/app/templates.html index 5747aa722..8c95831c9 100644 --- a/dashboard/app/templates.html +++ b/dashboard/app/templates.html @@ -6,6 +6,15 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the {{/* Common page head part, invoked with *uiHeader */}} {{define "head"}} + {{if .Redirects}} + + {{end}} {{if .AnalyticsTrackingID}} @@ -24,7 +33,14 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the
-

syzbot

+

syzbot

+