Skip to content

Commit 459ee4e

Browse files
authored
feat: add pagination to getWorkspaces (coder#4521)
1 parent 574e5d3 commit 459ee4e

File tree

8 files changed

+114
-13
lines changed

8 files changed

+114
-13
lines changed

coderd/database/databasefake/databasefake.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,19 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
695695
workspaces = append(workspaces, workspace)
696696
}
697697

698+
if arg.Offset > 0 {
699+
if int(arg.Offset) > len(workspaces) {
700+
return []database.Workspace{}, nil
701+
}
702+
workspaces = workspaces[arg.Offset:]
703+
}
704+
if arg.Limit > 0 {
705+
if int(arg.Limit) > len(workspaces) {
706+
return workspaces, nil
707+
}
708+
workspaces = workspaces[:arg.Limit]
709+
}
710+
698711
return workspaces, nil
699712
}
700713

coderd/database/modelqueries.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"strings"
78

89
"github.com/lib/pq"
910

@@ -164,8 +165,11 @@ type workspaceQuerier interface {
164165
// This code is copied from `GetWorkspaces` and adds the authorized filter WHERE
165166
// clause.
166167
func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, authorizedFilter rbac.AuthorizeFilter) ([]Workspace, error) {
168+
// In order to properly use ORDER BY, OFFSET, and LIMIT, we need to inject the
169+
// authorizedFilter between the end of the where clause and those statements.
170+
filter := strings.Replace(getWorkspaces, "-- @authorize_filter", fmt.Sprintf(" AND %s", authorizedFilter.SQLString(rbac.NoACLConfig())), 1)
167171
// The name comment is for metric tracking
168-
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s AND %s", getWorkspaces, authorizedFilter.SQLString(rbac.NoACLConfig()))
172+
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s", filter)
169173
rows, err := q.db.QueryContext(ctx, query,
170174
arg.Deleted,
171175
arg.Status,
@@ -174,6 +178,8 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
174178
arg.TemplateName,
175179
pq.Array(arg.TemplateIds),
176180
arg.Name,
181+
arg.Offset,
182+
arg.Limit,
177183
)
178184
if err != nil {
179185
return nil, xerrors.Errorf("get authorized workspaces: %w", err)

coderd/database/queries.sql.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaces.sql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,17 @@ WHERE
132132
name ILIKE '%' || @name || '%'
133133
ELSE true
134134
END
135+
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
136+
-- @authorize_filter
137+
ORDER BY
138+
last_used_at DESC
139+
LIMIT
140+
CASE
141+
WHEN @limit_ :: integer > 0 THEN
142+
@limit_
143+
END
144+
OFFSET
145+
@offset_
135146
;
136147

137148
-- name: GetWorkspaceByOwnerIDAndName :one

coderd/workspaces.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,13 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
9898
ctx := r.Context()
9999
apiKey := httpmw.APIKey(r)
100100

101+
page, ok := parsePagination(rw, r)
102+
if !ok {
103+
return
104+
}
105+
101106
queryStr := r.URL.Query().Get("q")
102-
filter, errs := workspaceSearchQuery(queryStr)
107+
filter, errs := workspaceSearchQuery(queryStr, page)
103108
if len(errs) > 0 {
104109
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
105110
Message: "Invalid workspace search query.",
@@ -1072,11 +1077,15 @@ func validWorkspaceSchedule(s *string, min time.Duration) (sql.NullString, error
10721077

10731078
// workspaceSearchQuery takes a query string and returns the workspace filter.
10741079
// It also can return the list of validation errors to return to the api.
1075-
func workspaceSearchQuery(query string) (database.GetWorkspacesParams, []codersdk.ValidationError) {
1080+
func workspaceSearchQuery(query string, page codersdk.Pagination) (database.GetWorkspacesParams, []codersdk.ValidationError) {
1081+
filter := database.GetWorkspacesParams{
1082+
Offset: int32(page.Offset),
1083+
Limit: int32(page.Limit),
1084+
}
10761085
searchParams := make(url.Values)
10771086
if query == "" {
10781087
// No filter
1079-
return database.GetWorkspacesParams{}, nil
1088+
return filter, nil
10801089
}
10811090
query = strings.ToLower(query)
10821091
// Because we do this in 2 passes, we want to maintain quotes on the first
@@ -1112,13 +1121,10 @@ func workspaceSearchQuery(query string) (database.GetWorkspacesParams, []codersd
11121121
// Using the query param parser here just returns consistent errors with
11131122
// other parsing.
11141123
parser := httpapi.NewQueryParamParser()
1115-
filter := database.GetWorkspacesParams{
1116-
Deleted: false,
1117-
OwnerUsername: parser.String(searchParams, "", "owner"),
1118-
TemplateName: parser.String(searchParams, "", "template"),
1119-
Name: parser.String(searchParams, "", "name"),
1120-
Status: parser.String(searchParams, "", "status"),
1121-
}
1124+
filter.OwnerUsername = parser.String(searchParams, "", "owner")
1125+
filter.TemplateName = parser.String(searchParams, "", "template")
1126+
filter.Name = parser.String(searchParams, "", "name")
1127+
filter.Status = parser.String(searchParams, "", "status")
11221128

11231129
return filter, parser.Errors
11241130
}

coderd/workspaces_internal_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/coder/coder/coderd/database"
9+
"github.com/coder/coder/codersdk"
910

1011
"github.com/stretchr/testify/require"
1112
)
@@ -135,7 +136,7 @@ func TestSearchWorkspace(t *testing.T) {
135136
c := c
136137
t.Run(c.Name, func(t *testing.T) {
137138
t.Parallel()
138-
values, errs := workspaceSearchQuery(c.Query)
139+
values, errs := workspaceSearchQuery(c.Query, codersdk.Pagination{})
139140
if c.ExpectedErrorContains != "" {
140141
require.True(t, len(errs) > 0, "expect some errors")
141142
var s strings.Builder

coderd/workspaces_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,47 @@ func TestWorkspaceFilterManual(t *testing.T) {
781781
})
782782
}
783783

784+
func TestOffsetLimit(t *testing.T) {
785+
t.Parallel()
786+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
787+
defer cancel()
788+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
789+
user := coderdtest.CreateFirstUser(t, client)
790+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
791+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
792+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
793+
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
794+
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
795+
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
796+
797+
// empty finds all workspaces
798+
ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
799+
require.NoError(t, err)
800+
require.Len(t, ws, 3)
801+
802+
// offset 1 finds 2 workspaces
803+
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
804+
Offset: 1,
805+
})
806+
require.NoError(t, err)
807+
require.Len(t, ws, 2)
808+
809+
// offset 1 limit 1 finds 1 workspace
810+
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
811+
Offset: 1,
812+
Limit: 1,
813+
})
814+
require.NoError(t, err)
815+
require.Len(t, ws, 1)
816+
817+
// offset 3 finds no workspaces
818+
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
819+
Offset: 3,
820+
})
821+
require.NoError(t, err)
822+
require.Len(t, ws, 0)
823+
}
824+
784825
func TestPostWorkspaceBuild(t *testing.T) {
785826
t.Parallel()
786827
t.Run("NoTemplateVersion", func(t *testing.T) {

codersdk/workspaces.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ type WorkspaceFilter struct {
254254
Name string `json:"name,omitempty" typescript:"-"`
255255
// Status is a workspace status, which is really the status of the latest build
256256
Status string `json:"status,omitempty" typescript:"-"`
257+
// Offset is the number of workspaces to skip before returning results.
258+
Offset int `json:"offset,omitempty" typescript:"-"`
259+
// Limit is a limit on the number of workspaces returned.
260+
Limit int `json:"limit,omitempty" typescript:"-"`
257261
// FilterQuery supports a raw filter query string
258262
FilterQuery string `json:"q,omitempty"`
259263
}
@@ -290,7 +294,11 @@ func (f WorkspaceFilter) asRequestOption() RequestOption {
290294

291295
// Workspaces returns all workspaces the authenticated user has access to.
292296
func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) ([]Workspace, error) {
293-
res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaces", nil, filter.asRequestOption())
297+
page := Pagination{
298+
Offset: filter.Offset,
299+
Limit: filter.Limit,
300+
}
301+
res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaces", nil, filter.asRequestOption(), page.asRequestOption())
294302
if err != nil {
295303
return nil, err
296304
}

0 commit comments

Comments
 (0)