mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat: add pagination to getWorkspaces (#4521)
This commit is contained in:
@@ -695,6 +695,19 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
||||
workspaces = append(workspaces, workspace)
|
||||
}
|
||||
|
||||
if arg.Offset > 0 {
|
||||
if int(arg.Offset) > len(workspaces) {
|
||||
return []database.Workspace{}, nil
|
||||
}
|
||||
workspaces = workspaces[arg.Offset:]
|
||||
}
|
||||
if arg.Limit > 0 {
|
||||
if int(arg.Limit) > len(workspaces) {
|
||||
return workspaces, nil
|
||||
}
|
||||
workspaces = workspaces[:arg.Limit]
|
||||
}
|
||||
|
||||
return workspaces, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lib/pq"
|
||||
|
||||
@@ -164,8 +165,11 @@ type workspaceQuerier interface {
|
||||
// This code is copied from `GetWorkspaces` and adds the authorized filter WHERE
|
||||
// clause.
|
||||
func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, authorizedFilter rbac.AuthorizeFilter) ([]Workspace, error) {
|
||||
// In order to properly use ORDER BY, OFFSET, and LIMIT, we need to inject the
|
||||
// authorizedFilter between the end of the where clause and those statements.
|
||||
filter := strings.Replace(getWorkspaces, "-- @authorize_filter", fmt.Sprintf(" AND %s", authorizedFilter.SQLString(rbac.NoACLConfig())), 1)
|
||||
// The name comment is for metric tracking
|
||||
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s AND %s", getWorkspaces, authorizedFilter.SQLString(rbac.NoACLConfig()))
|
||||
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s", filter)
|
||||
rows, err := q.db.QueryContext(ctx, query,
|
||||
arg.Deleted,
|
||||
arg.Status,
|
||||
@@ -174,6 +178,8 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
||||
arg.TemplateName,
|
||||
pq.Array(arg.TemplateIds),
|
||||
arg.Name,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get authorized workspaces: %w", err)
|
||||
|
||||
@@ -5553,6 +5553,17 @@ WHERE
|
||||
name ILIKE '%' || $7 || '%'
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
|
||||
-- @authorize_filter
|
||||
ORDER BY
|
||||
last_used_at DESC
|
||||
LIMIT
|
||||
CASE
|
||||
WHEN $9 :: integer > 0 THEN
|
||||
$9
|
||||
END
|
||||
OFFSET
|
||||
$8
|
||||
`
|
||||
|
||||
type GetWorkspacesParams struct {
|
||||
@@ -5563,6 +5574,8 @@ type GetWorkspacesParams struct {
|
||||
TemplateName string `db:"template_name" json:"template_name"`
|
||||
TemplateIds []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Offset int32 `db:"offset_" json:"offset_"`
|
||||
Limit int32 `db:"limit_" json:"limit_"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]Workspace, error) {
|
||||
@@ -5574,6 +5587,8 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
||||
arg.TemplateName,
|
||||
pq.Array(arg.TemplateIds),
|
||||
arg.Name,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -132,6 +132,17 @@ WHERE
|
||||
name ILIKE '%' || @name || '%'
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
|
||||
-- @authorize_filter
|
||||
ORDER BY
|
||||
last_used_at DESC
|
||||
LIMIT
|
||||
CASE
|
||||
WHEN @limit_ :: integer > 0 THEN
|
||||
@limit_
|
||||
END
|
||||
OFFSET
|
||||
@offset_
|
||||
;
|
||||
|
||||
-- name: GetWorkspaceByOwnerIDAndName :one
|
||||
|
||||
+16
-10
@@ -98,8 +98,13 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
page, ok := parsePagination(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
queryStr := r.URL.Query().Get("q")
|
||||
filter, errs := workspaceSearchQuery(queryStr)
|
||||
filter, errs := workspaceSearchQuery(queryStr, page)
|
||||
if len(errs) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid workspace search query.",
|
||||
@@ -1072,11 +1077,15 @@ func validWorkspaceSchedule(s *string, min time.Duration) (sql.NullString, error
|
||||
|
||||
// workspaceSearchQuery takes a query string and returns the workspace filter.
|
||||
// It also can return the list of validation errors to return to the api.
|
||||
func workspaceSearchQuery(query string) (database.GetWorkspacesParams, []codersdk.ValidationError) {
|
||||
func workspaceSearchQuery(query string, page codersdk.Pagination) (database.GetWorkspacesParams, []codersdk.ValidationError) {
|
||||
filter := database.GetWorkspacesParams{
|
||||
Offset: int32(page.Offset),
|
||||
Limit: int32(page.Limit),
|
||||
}
|
||||
searchParams := make(url.Values)
|
||||
if query == "" {
|
||||
// No filter
|
||||
return database.GetWorkspacesParams{}, nil
|
||||
return filter, nil
|
||||
}
|
||||
query = strings.ToLower(query)
|
||||
// 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
|
||||
// Using the query param parser here just returns consistent errors with
|
||||
// other parsing.
|
||||
parser := httpapi.NewQueryParamParser()
|
||||
filter := database.GetWorkspacesParams{
|
||||
Deleted: false,
|
||||
OwnerUsername: parser.String(searchParams, "", "owner"),
|
||||
TemplateName: parser.String(searchParams, "", "template"),
|
||||
Name: parser.String(searchParams, "", "name"),
|
||||
Status: parser.String(searchParams, "", "status"),
|
||||
}
|
||||
filter.OwnerUsername = parser.String(searchParams, "", "owner")
|
||||
filter.TemplateName = parser.String(searchParams, "", "template")
|
||||
filter.Name = parser.String(searchParams, "", "name")
|
||||
filter.Status = parser.String(searchParams, "", "status")
|
||||
|
||||
return filter, parser.Errors
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -135,7 +136,7 @@ func TestSearchWorkspace(t *testing.T) {
|
||||
c := c
|
||||
t.Run(c.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
values, errs := workspaceSearchQuery(c.Query)
|
||||
values, errs := workspaceSearchQuery(c.Query, codersdk.Pagination{})
|
||||
if c.ExpectedErrorContains != "" {
|
||||
require.True(t, len(errs) > 0, "expect some errors")
|
||||
var s strings.Builder
|
||||
|
||||
@@ -781,6 +781,47 @@ func TestWorkspaceFilterManual(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestOffsetLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
|
||||
// empty finds all workspaces
|
||||
ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 3)
|
||||
|
||||
// offset 1 finds 2 workspaces
|
||||
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Offset: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 2)
|
||||
|
||||
// offset 1 limit 1 finds 1 workspace
|
||||
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Offset: 1,
|
||||
Limit: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 1)
|
||||
|
||||
// offset 3 finds no workspaces
|
||||
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Offset: 3,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ws, 0)
|
||||
}
|
||||
|
||||
func TestPostWorkspaceBuild(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NoTemplateVersion", func(t *testing.T) {
|
||||
|
||||
@@ -254,6 +254,10 @@ type WorkspaceFilter struct {
|
||||
Name string `json:"name,omitempty" typescript:"-"`
|
||||
// Status is a workspace status, which is really the status of the latest build
|
||||
Status string `json:"status,omitempty" typescript:"-"`
|
||||
// Offset is the number of workspaces to skip before returning results.
|
||||
Offset int `json:"offset,omitempty" typescript:"-"`
|
||||
// Limit is a limit on the number of workspaces returned.
|
||||
Limit int `json:"limit,omitempty" typescript:"-"`
|
||||
// FilterQuery supports a raw filter query string
|
||||
FilterQuery string `json:"q,omitempty"`
|
||||
}
|
||||
@@ -290,7 +294,11 @@ func (f WorkspaceFilter) asRequestOption() RequestOption {
|
||||
|
||||
// Workspaces returns all workspaces the authenticated user has access to.
|
||||
func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) ([]Workspace, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaces", nil, filter.asRequestOption())
|
||||
page := Pagination{
|
||||
Offset: filter.Offset,
|
||||
Limit: filter.Limit,
|
||||
}
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaces", nil, filter.asRequestOption(), page.asRequestOption())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user