aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2025-04-07 17:01:33 +0200
committerAleksandr Nogikh <nogikh@google.com>2025-04-08 10:15:40 +0000
commitb133e63a2fee6e55cec2811da78dd5b6aab6440f (patch)
tree73b3ad6c3c8f7031bbfa5b8b3a82cf65fdf6ff30
parentb899e60d5c5004525d9a554d1d5074d062c3fb95 (diff)
syz-cluster: add pagination
Add simple Previous/Next navigation for the list of series. For now, just rely on SQL's LIMIT/OFFSET functionality.
-rw-r--r--syz-cluster/controller/processor_test.go2
-rw-r--r--syz-cluster/dashboard/Dockerfile1
-rw-r--r--syz-cluster/dashboard/handler.go42
-rw-r--r--syz-cluster/dashboard/templates/index.html17
-rw-r--r--syz-cluster/pkg/db/series_repo.go14
-rw-r--r--syz-cluster/pkg/db/series_repo_test.go25
-rw-r--r--syz-cluster/pkg/db/session_repo_test.go2
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"}}
<div class="mx-3">
- <form>
+ <form action="{{.FilterFormURL}}">
<div class="row align-items-center">
<div class="col-auto col-sm-3">
<label for="inputCc">Cc'd</label>
@@ -13,7 +13,7 @@
{{range .Statuses}}
<option value="{{.Key}}" {{if eq .Key $filter.Status}} selected{{end}}>{{.Value}}</option>
{{end}}
- </select>
+ </select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">Submit</button>
@@ -22,6 +22,19 @@
</form>
</div>
+ {{if or .PrevPageURL .NextPageURL}}
+ <nav class="mx-3">
+ <ul class="pagination">
+ <li class="page-item {{if not .PrevPageURL}}disabled{{end}}">
+ <a class="page-link" href="{{.PrevPageURL}}" tabindex="-1">Previous</a>
+ </li>
+ <li class="page-item {{if not .NextPageURL}}disabled{{end}}">
+ <a class="page-link" href="{{.NextPageURL}}">Next</a>
+ </li>
+ </ul>
+ </nav>
+ {{end}}
+
<table class="table">
<thead class="thead-light">
<tr>
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 {