From b133e63a2fee6e55cec2811da78dd5b6aab6440f Mon Sep 17 00:00:00 2001 From: Aleksandr Nogikh Date: Mon, 7 Apr 2025 17:01:33 +0200 Subject: syz-cluster: add pagination Add simple Previous/Next navigation for the list of series. For now, just rely on SQL's LIMIT/OFFSET functionality. --- syz-cluster/controller/processor_test.go | 2 +- syz-cluster/dashboard/Dockerfile | 1 + syz-cluster/dashboard/handler.go | 42 ++++++++++++++++++++++++++++-- syz-cluster/dashboard/templates/index.html | 17 ++++++++++-- syz-cluster/pkg/db/series_repo.go | 14 +++++++--- syz-cluster/pkg/db/series_repo_test.go | 25 +++++++++++++----- syz-cluster/pkg/db/session_repo_test.go | 2 +- 7 files changed, 86 insertions(+), 17 deletions(-) diff --git a/syz-cluster/controller/processor_test.go b/syz-cluster/controller/processor_test.go index d5e5438bf..f6f6abbbe 100644 --- a/syz-cluster/controller/processor_test.go +++ b/syz-cluster/controller/processor_test.go @@ -87,7 +87,7 @@ func awaitFinishedSessions(t *testing.T, seriesRepo *db.SeriesRepository, wantFi for i := 0; i < int(deadline/interval); i++ { time.Sleep(interval) - list, err := seriesRepo.ListLatest(context.Background(), db.SeriesFilter{}, time.Time{}, 0) + list, err := seriesRepo.ListLatest(context.Background(), db.SeriesFilter{}, time.Time{}) assert.NoError(t, err) withFinishedSeries := 0 for _, item := range list { diff --git a/syz-cluster/dashboard/Dockerfile b/syz-cluster/dashboard/Dockerfile index 53925a53b..62d540cd8 100644 --- a/syz-cluster/dashboard/Dockerfile +++ b/syz-cluster/dashboard/Dockerfile @@ -7,6 +7,7 @@ COPY go.mod ./ COPY go.sum ./ RUN go mod download COPY pkg/gcs/ pkg/gcs/ +COPY pkg/html/urlutil/ pkg/html/urlutil/ # Build the tool. COPY syz-cluster/dashboard/ syz-cluster/dashboard/ diff --git a/syz-cluster/dashboard/handler.go b/syz-cluster/dashboard/handler.go index 50c1966f8..318705e7f 100644 --- a/syz-cluster/dashboard/handler.go +++ b/syz-cluster/dashboard/handler.go @@ -11,8 +11,10 @@ import ( "io" "io/fs" "net/http" + "strconv" "time" + "github.com/google/syzkaller/pkg/html/urlutil" "github.com/google/syzkaller/syz-cluster/pkg/app" "github.com/google/syzkaller/syz-cluster/pkg/blob" "github.com/google/syzkaller/syz-cluster/pkg/db" @@ -85,12 +87,26 @@ func (h *dashboardHandler) seriesList(w http.ResponseWriter, r *http.Request) er List []*db.SeriesWithSession Filter db.SeriesFilter Statuses []statusOption + // This is very primitive, but better than nothing. + FilterFormURL string + PrevPageURL string + NextPageURL string } + const perPage = 100 + offset, err := h.getOffset(r) + if err != nil { + return err + } + baseURL := r.URL.RequestURI() data := MainPageData{ Filter: db.SeriesFilter{ Cc: r.FormValue("cc"), Status: db.SessionStatus(r.FormValue("status")), + Limit: perPage, + Offset: offset, }, + // If the filters are changed, the old offset value is irrelevant. + FilterFormURL: urlutil.DropParam(baseURL, "offset", ""), Statuses: []statusOption{ {db.SessionStatusAny, "any"}, {db.SessionStatusWaiting, "waiting"}, @@ -98,14 +114,36 @@ func (h *dashboardHandler) seriesList(w http.ResponseWriter, r *http.Request) er {db.SessionStatusFinished, "finished"}, }, } - var err error - data.List, err = h.seriesRepo.ListLatest(r.Context(), data.Filter, time.Time{}, 0) + + data.List, err = h.seriesRepo.ListLatest(r.Context(), data.Filter, time.Time{}) if err != nil { return fmt.Errorf("failed to query the list: %w", err) } + if data.Filter.Offset > 0 { + data.PrevPageURL = urlutil.SetParam(baseURL, "offset", + fmt.Sprintf("%d", max(0, data.Filter.Offset-perPage))) + } + // TODO: this is not strictly correct (we also need to check whether there actually more rows). + // But let's tolerate it for now. + if len(data.List) == data.Filter.Limit { + data.NextPageURL = urlutil.SetParam(baseURL, "offset", + fmt.Sprintf("%d", data.Filter.Offset+len(data.List))) + } return h.templates["index.html"].ExecuteTemplate(w, "base.html", data) } +func (h *dashboardHandler) getOffset(r *http.Request) (int, error) { + val := r.FormValue("offset") + if val == "" { + return 0, nil + } + i, err := strconv.Atoi(val) + if err != nil || i < 0 { + return 0, fmt.Errorf("%w: invalid offset value", errBadRequest) + } + return i, nil +} + func (h *dashboardHandler) seriesInfo(w http.ResponseWriter, r *http.Request) error { type SessionTest struct { *db.FullSessionTest diff --git a/syz-cluster/dashboard/templates/index.html b/syz-cluster/dashboard/templates/index.html index 98d021a1b..f51e5d34f 100644 --- a/syz-cluster/dashboard/templates/index.html +++ b/syz-cluster/dashboard/templates/index.html @@ -1,6 +1,6 @@ {{define "content"}}
-
+
@@ -13,7 +13,7 @@ {{range .Statuses}} {{end}} - +
@@ -22,6 +22,19 @@
+ {{if or .PrevPageURL .NextPageURL}} + + {{end}} + diff --git a/syz-cluster/pkg/db/series_repo.go b/syz-cluster/pkg/db/series_repo.go index b21b82889..554a32b00 100644 --- a/syz-cluster/pkg/db/series_repo.go +++ b/syz-cluster/pkg/db/series_repo.go @@ -134,11 +134,13 @@ type SeriesWithSession struct { type SeriesFilter struct { Cc string Status SessionStatus + Limit int + Offset int } // ListLatest() returns the list of series ordered by the decreasing PublishedAt value. func (repo *SeriesRepository) ListLatest(ctx context.Context, filter SeriesFilter, - maxPublishedAt time.Time, limit int) ([]*SeriesWithSession, error) { + maxPublishedAt time.Time) ([]*SeriesWithSession, error) { ro := repo.client.ReadOnlyTransaction() defer ro.Close() @@ -171,10 +173,14 @@ func (repo *SeriesRepository) ListLatest(ctx context.Context, filter SeriesFilte } stmt.SQL += ")" } - stmt.SQL += " ORDER BY PublishedAt DESC" - if limit > 0 { + stmt.SQL += " ORDER BY PublishedAt DESC, ID" + if filter.Limit > 0 { stmt.SQL += " LIMIT @limit" - stmt.Params["limit"] = limit + stmt.Params["limit"] = filter.Limit + } + if filter.Offset > 0 { + stmt.SQL += " OFFSET @offset" + stmt.Params["offset"] = filter.Offset } iter := ro.Query(ctx, stmt) defer iter.Stop() diff --git a/syz-cluster/pkg/db/series_repo_test.go b/syz-cluster/pkg/db/series_repo_test.go index 222014113..694893514 100644 --- a/syz-cluster/pkg/db/series_repo_test.go +++ b/syz-cluster/pkg/db/series_repo_test.go @@ -84,22 +84,34 @@ func TestSeriesRepositoryList(t *testing.T) { }) t.Run("all", func(t *testing.T) { - list, err := repo.ListLatest(ctx, SeriesFilter{}, time.Time{}, 0) + list, err := repo.ListLatest(ctx, SeriesFilter{}, time.Time{}) assert.NoError(t, err) assert.Len(t, list, 3) }) t.Run("with_limit", func(t *testing.T) { - list, err := repo.ListLatest(ctx, SeriesFilter{}, time.Time{}, 2) + list, err := repo.ListLatest(ctx, SeriesFilter{ + Limit: 2, + }, time.Time{}) assert.NoError(t, err) assert.Len(t, list, 2) assert.Equal(t, "Series 3", list[0].Series.Title) assert.Equal(t, "Series 2", list[1].Series.Title) }) + t.Run("with_offset", func(t *testing.T) { + list, err := repo.ListLatest(ctx, SeriesFilter{ + Limit: 1, + Offset: 1, + }, time.Time{}) + assert.NoError(t, err) + assert.Len(t, list, 1) + assert.Equal(t, "Series 2", list[0].Series.Title) + }) + t.Run("with_from", func(t *testing.T) { // Skips the latest series. - list, err := repo.ListLatest(ctx, SeriesFilter{}, time.Date(2020, time.January, 1, 3, 0, 0, 0, time.UTC), 0) + list, err := repo.ListLatest(ctx, SeriesFilter{}, time.Date(2020, time.January, 1, 3, 0, 0, 0, time.UTC)) assert.NoError(t, err) assert.Len(t, list, 2) assert.Equal(t, "Series 2", list[0].Series.Title) @@ -107,8 +119,7 @@ func TestSeriesRepositoryList(t *testing.T) { }) t.Run("filter_by_cc", func(t *testing.T) { - list, err := repo.ListLatest(ctx, - SeriesFilter{Cc: "a"}, time.Time{}, 0) + list, err := repo.ListLatest(ctx, SeriesFilter{Cc: "a"}, time.Time{}) assert.NoError(t, err) assert.Len(t, list, 2) }) @@ -125,7 +136,7 @@ func TestSeriesRepositoryList(t *testing.T) { assert.NoError(t, err) t.Run("filter_status_waiting", func(t *testing.T) { - list, err := repo.ListLatest(ctx, SeriesFilter{Status: SessionStatusWaiting}, time.Time{}, 0) + list, err := repo.ListLatest(ctx, SeriesFilter{Status: SessionStatusWaiting}, time.Time{}) assert.NoError(t, err) assert.Len(t, list, 1) }) @@ -134,7 +145,7 @@ func TestSeriesRepositoryList(t *testing.T) { assert.NoError(t, err) t.Run("filter_status_in_progress", func(t *testing.T) { - list, err := repo.ListLatest(ctx, SeriesFilter{Status: SessionStatusInProgress}, time.Time{}, 0) + list, err := repo.ListLatest(ctx, SeriesFilter{Status: SessionStatusInProgress}, time.Time{}) assert.NoError(t, err) assert.Len(t, list, 1) }) diff --git a/syz-cluster/pkg/db/session_repo_test.go b/syz-cluster/pkg/db/session_repo_test.go index c6f79f975..eb688510a 100644 --- a/syz-cluster/pkg/db/session_repo_test.go +++ b/syz-cluster/pkg/db/session_repo_test.go @@ -20,7 +20,7 @@ func TestSeriesInsertSession(t *testing.T) { assert.NoError(t, err) withSession := func(need int) { - list, err := seriesRepo.ListLatest(ctx, SeriesFilter{}, time.Time{}, 10) + list, err := seriesRepo.ListLatest(ctx, SeriesFilter{Limit: 10}, time.Time{}) assert.NoError(t, err) var cnt int for _, item := range list { -- cgit mrf-deployment