diff --git a/cli/list.go b/cli/list.go index 09e15f2e663..ff473be2b48 100644 --- a/cli/list.go +++ b/cli/list.go @@ -178,3 +178,7 @@ func QueryConvertWorkspaces[T any](ctx context.Context, client *codersdk.Client, } return converted, nil } + +// pageLimit is the number of rows requested per page when the CLI fetches a +// list endpoint to exhaustion. +const pageLimit = codersdk.MaxPaginationLimit diff --git a/cli/templatepull.go b/cli/templatepull.go index 322a11d8e36..18de4f00fbc 100644 --- a/cli/templatepull.go +++ b/cli/templatepull.go @@ -62,7 +62,7 @@ func (r *RootCmd) templatePull() *serpent.Command { { // Determine the latest template version and compare with the // active version. If they aren't the same, warn the user. - versions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{ + versions, err := allTemplateVersions(ctx, client, codersdk.TemplateVersionsByTemplateRequest{ TemplateID: template.ID, }) if err != nil { diff --git a/cli/templateversions.go b/cli/templateversions.go index 30d4a1ca82b..11590d1b675 100644 --- a/cli/templateversions.go +++ b/cli/templateversions.go @@ -1,6 +1,7 @@ package cli import ( + "context" "fmt" "strings" "time" @@ -14,6 +15,24 @@ import ( "github.com/coder/serpent" ) +// allTemplateVersions fetches template versions page by page until a page +// returns fewer rows than requested, which marks the end of the result set. +func allTemplateVersions(ctx context.Context, client *codersdk.Client, req codersdk.TemplateVersionsByTemplateRequest) ([]codersdk.TemplateVersion, error) { + req.Limit = pageLimit + var versions []codersdk.TemplateVersion + for { + page, err := client.TemplateVersionsByTemplate(ctx, req) + if err != nil { + return nil, err + } + versions = append(versions, page...) + if len(page) < pageLimit { + return versions, nil + } + req.Offset += len(page) + } +} + func (r *RootCmd) templateVersions() *serpent.Command { cmd := &serpent.Command{ Use: "versions", @@ -110,7 +129,7 @@ func (r *RootCmd) templateVersionsList() *serpent.Command { IncludeArchived: includeArchived.Value(), } - versions, err := client.TemplateVersionsByTemplate(inv.Context(), req) + versions, err := allTemplateVersions(inv.Context(), client, req) if err != nil { return xerrors.Errorf("get template versions by template: %w", err) } diff --git a/cli/userlist.go b/cli/userlist.go index c8a6740a935..83c3dc1df44 100644 --- a/cli/userlist.go +++ b/cli/userlist.go @@ -48,12 +48,23 @@ func (r *RootCmd) userList() *serpent.Command { req.Search = fmt.Sprintf("github_com_user_id:%d", githubUserID) } - res, err := client.Users(inv.Context(), req) - if err != nil { - return err + // Fetch pages until a page returns fewer rows than requested, + // which marks the end of the result set. + req.Limit = pageLimit + users := []codersdk.User{} + for { + res, err := client.Users(inv.Context(), req) + if err != nil { + return err + } + users = append(users, res.Users...) + if len(res.Users) < pageLimit { + break + } + req.Offset += len(res.Users) } - out, err := formatter.Format(inv.Context(), res.Users) + out, err := formatter.Format(inv.Context(), users) if err != nil { return err } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 526fe4b48fa..af2c11df80a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2078,8 +2078,7 @@ const docTemplate = `{ "type": "integer", "description": "Page limit", "name": "limit", - "in": "query", - "required": true + "in": "query" }, { "type": "integer", @@ -2239,8 +2238,7 @@ const docTemplate = `{ "type": "integer", "description": "Page limit", "name": "limit", - "in": "query", - "required": true + "in": "query" }, { "type": "integer", @@ -5854,7 +5852,7 @@ const docTemplate = `{ }, { "type": "integer", - "description": "Page limit, if 0 returns all members", + "description": "Page limit", "name": "limit", "in": "query" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index a009b6e7086..c937721ba31 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1835,8 +1835,7 @@ "type": "integer", "description": "Page limit", "name": "limit", - "in": "query", - "required": true + "in": "query" }, { "type": "integer", @@ -1974,8 +1973,7 @@ "type": "integer", "description": "Page limit", "name": "limit", - "in": "query", - "required": true + "in": "query" }, { "type": "integer", @@ -5187,7 +5185,7 @@ }, { "type": "integer", - "description": "Page limit, if 0 returns all members", + "description": "Page limit", "name": "limit", "in": "query" }, diff --git a/coderd/audit.go b/coderd/audit.go index 44ed30770bc..1654d252089 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -37,7 +37,7 @@ const auditLogCountCap = 2000 // @Produce json // @Tags Audit // @Param q query string false "Search query" -// @Param limit query int true "Page limit" +// @Param limit query int false "Page limit" // @Param offset query int false "Page offset" // @Success 200 {object} codersdk.AuditLogResponse // @Router /api/v2/audit [get] diff --git a/coderd/members.go b/coderd/members.go index 7f1511bebb9..e9adce1befd 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -269,7 +269,7 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { // @Param organization path string true "Organization ID" // @Param q query string false "Member search query" // @Param after_id query string false "After ID" format(uuid) -// @Param limit query int false "Page limit, if 0 returns all members" +// @Param limit query int false "Page limit" // @Param offset query int false "Page offset" // @Success 200 {object} []codersdk.PaginatedMembersResponse // @Router /api/v2/organizations/{organization}/paginated-members [get] diff --git a/coderd/pagination.go b/coderd/pagination.go index 011f8df9e7b..399a1df0c1f 100644 --- a/coderd/pagination.go +++ b/coderd/pagination.go @@ -1,6 +1,7 @@ package coderd import ( + "fmt" "net/http" "github.com/google/uuid" @@ -17,10 +18,22 @@ func ParsePagination(w http.ResponseWriter, r *http.Request) (p codersdk.Paginat parser := httpapi.NewQueryParamParser() params := codersdk.Pagination{ AfterID: parser.UUID(queryParams, uuid.Nil, "after_id"), - // A limit of 0 should be interpreted by the SQL query as "null" or - // "no limit". Do not make this value anything besides 0. - Limit: int(parser.PositiveInt32(queryParams, 0, "limit")), - Offset: int(parser.PositiveInt32(queryParams, 0, "offset")), + Offset: int(parser.PositiveInt32(queryParams, 0, "offset")), + } + limitErrsBefore := len(parser.Errors) + params.Limit = int(parser.PositiveInt32(queryParams, 0, "limit")) + limitParsed := len(parser.Errors) == limitErrsBefore + // An omitted limit resolves to codersdk.MaxPaginationLimit so the downstream + // query is never unbounded. A limit that is set must be a positive integer no + // greater than that maximum; otherwise it is rejected rather than clamped. + switch { + case queryParams.Get("limit") == "": + params.Limit = codersdk.MaxPaginationLimit + case limitParsed && (params.Limit < 1 || params.Limit > codersdk.MaxPaginationLimit): + parser.Errors = append(parser.Errors, codersdk.ValidationError{ + Field: "limit", + Detail: fmt.Sprintf("Query param \"limit\" must be a positive integer no greater than %d.", codersdk.MaxPaginationLimit), + }) } if len(parser.Errors) > 0 { httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ diff --git a/coderd/pagination_test.go b/coderd/pagination_test.go index f6e1aab7067..d4ce5e13f00 100644 --- a/coderd/pagination_test.go +++ b/coderd/pagination_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strconv" "testing" "github.com/google/uuid" @@ -72,6 +73,11 @@ func TestPagination(t *testing.T) { Offset: "-1", ExpectedError: invalidValues, }, + { + Name: "LimitAboveMax", + Limit: strconv.Itoa(codersdk.MaxPaginationLimit + 1), + ExpectedError: invalidValues, + }, // Valid values { @@ -93,11 +99,35 @@ func TestPagination(t *testing.T) { Limit: 50, }, }, + { + // An omitted limit resolves to the maximum page size so the + // query is never unbounded. + Name: "DefaultsToMaxWhenAbsent", + ExpectedParams: codersdk.Pagination{ + AfterID: uuid.Nil, + Limit: codersdk.MaxPaginationLimit, + }, + }, + { + // An explicit limit must be positive; 0 is rejected. + Name: "ZeroLimitRejected", + Limit: "0", + ExpectedError: invalidValues, + }, + { + Name: "LimitAtMax", + Limit: strconv.Itoa(codersdk.MaxPaginationLimit), + ExpectedParams: codersdk.Pagination{ + AfterID: uuid.Nil, + Limit: codersdk.MaxPaginationLimit, + }, + }, { Name: "ValidOffset", Offset: "150", ExpectedParams: codersdk.Pagination{ AfterID: uuid.Nil, + Limit: codersdk.MaxPaginationLimit, Offset: 150, }, }, @@ -106,6 +136,7 @@ func TestPagination(t *testing.T) { AfterID: "5f2005fc-acc4-4e5e-a7fa-be017359c60b", ExpectedParams: codersdk.Pagination{ AfterID: uuid.MustParse("5f2005fc-acc4-4e5e-a7fa-be017359c60b"), + Limit: codersdk.MaxPaginationLimit, }, }, } diff --git a/codersdk/pagination.go b/codersdk/pagination.go index 2201277aeca..514894f4278 100644 --- a/codersdk/pagination.go +++ b/codersdk/pagination.go @@ -7,6 +7,11 @@ import ( "github.com/google/uuid" ) +// MaxPaginationLimit is the largest page size the server accepts. An omitted +// limit resolves to this value. A limit that is set must be a positive integer +// no greater than this, so the resulting query is always bounded. +const MaxPaginationLimit = 100 + // Pagination sets pagination options for the endpoints that support it. type Pagination struct { // AfterID returns all or up to Limit results after the given @@ -15,9 +20,11 @@ type Pagination struct { // set AfterID to the last UUID returned by the previous // request. AfterID uuid.UUID `json:"after_id,omitempty" format:"uuid"` - // Limit sets the maximum number of users to be returned - // in a single page. If the limit is <= 0, there is no limit - // and all users are returned. + // Limit sets the maximum number of results returned in a single page. + // When set, it must be a positive integer no greater than + // MaxPaginationLimit, otherwise the request is rejected. A value of 0 + // omits the query parameter entirely, and the server resolves the page + // size to MaxPaginationLimit. Limit int `json:"limit,omitempty"` // Offset is used to indicate which page to return. An offset of 0 // returns the first 'limit' number of users. diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 5bb268411ad..16639100a65 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -633,7 +633,7 @@ func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) (Worksp } // WorkspacePageSize is the number of rows AllWorkspaces requests per page. -const WorkspacePageSize = 100 +const WorkspacePageSize = MaxPaginationLimit // AllWorkspaces requests successive pages of workspaces matching the filter and // returns every row. Limit and Offset on the filter are ignored. diff --git a/docs/reference/api/audit.md b/docs/reference/api/audit.md index 06a7057fb7d..ec74cd2c99b 100644 --- a/docs/reference/api/audit.md +++ b/docs/reference/api/audit.md @@ -6,7 +6,7 @@ ```sh # Example request using curl -curl -X GET http://coder-server:8080/api/v2/audit?limit=0 \ +curl -X GET http://coder-server:8080/api/v2/audit \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` @@ -18,7 +18,7 @@ curl -X GET http://coder-server:8080/api/v2/audit?limit=0 \ | Name | In | Type | Required | Description | |----------|-------|---------|----------|--------------| | `q` | query | string | false | Search query | -| `limit` | query | integer | true | Page limit | +| `limit` | query | integer | false | Page limit | | `offset` | query | integer | false | Page offset | ### Example responses diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 92da32c8353..b2a70978ba0 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -455,7 +455,7 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio ```sh # Example request using curl -curl -X GET http://coder-server:8080/api/v2/connectionlog?limit=0 \ +curl -X GET http://coder-server:8080/api/v2/connectionlog \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` @@ -467,7 +467,7 @@ curl -X GET http://coder-server:8080/api/v2/connectionlog?limit=0 \ | Name | In | Type | Required | Description | |----------|-------|---------|----------|--------------| | `q` | query | string | false | Search query | -| `limit` | query | integer | true | Page limit | +| `limit` | query | integer | false | Page limit | | `offset` | query | integer | false | Page offset | ### Example responses diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 6660b4a138d..bb752049ecc 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -777,13 +777,13 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginat ### Parameters -| Name | In | Type | Required | Description | -|----------------|-------|--------------|----------|--------------------------------------| -| `organization` | path | string | true | Organization ID | -| `q` | query | string | false | Member search query | -| `after_id` | query | string(uuid) | false | After ID | -| `limit` | query | integer | false | Page limit, if 0 returns all members | -| `offset` | query | integer | false | Page offset | +| Name | In | Type | Required | Description | +|----------------|-------|--------------|----------|---------------------| +| `organization` | path | string | true | Organization ID | +| `q` | query | string | false | Member search query | +| `after_id` | query | string(uuid) | false | After ID | +| `limit` | query | integer | false | Page limit | +| `offset` | query | integer | false | Page offset | ### Example responses diff --git a/enterprise/coderd/aibridge.go b/enterprise/coderd/aibridge.go index 06e305b7fec..126f3c2854f 100644 --- a/enterprise/coderd/aibridge.go +++ b/enterprise/coderd/aibridge.go @@ -33,12 +33,6 @@ import ( ) const ( - maxListSessionsLimit = 1000 - maxListModelsLimit = 1000 - maxListClientsLimit = 1000 - defaultListSessionsLimit = 100 - defaultListModelsLimit = 100 - defaultListClientsLimit = 100 // aiBridgeRateLimitWindow is the fixed duration for rate limiting AI Bridge // requests. This is hardcoded to keep configuration simple. aiBridgeRateLimitWindow = time.Second @@ -178,16 +172,6 @@ func (api *API) aiBridgeListSessions(rw http.ResponseWriter, r *http.Request) { }) return } - if page.Limit == 0 { - page.Limit = defaultListSessionsLimit - } - if page.Limit > maxListSessionsLimit || page.Limit < 1 { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid pagination limit value.", - Detail: fmt.Sprintf("Pagination limit must be in range (0, %d]", maxListSessionsLimit), - }) - return - } queryStr := r.URL.Query().Get("q") filter, errs := searchquery.AIBridgeSessions(ctx, api.Database, queryStr, page, apiKey.UserID, afterSessionID) @@ -518,18 +502,6 @@ func (api *API) aiBridgeListModels(rw http.ResponseWriter, r *http.Request) { return } - if page.Limit == 0 { - page.Limit = defaultListModelsLimit - } - - if page.Limit > maxListModelsLimit || page.Limit < 1 { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid pagination limit value.", - Detail: fmt.Sprintf("Pagination limit must be in range (0, %d]", maxListModelsLimit), - }) - return - } - queryStr := r.URL.Query().Get("q") filter, errs := searchquery.AIBridgeModels(queryStr, page) @@ -571,18 +543,6 @@ func (api *API) aiBridgeListClients(rw http.ResponseWriter, r *http.Request) { return } - if page.Limit == 0 { - page.Limit = defaultListClientsLimit - } - - if page.Limit > maxListClientsLimit || page.Limit < 1 { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid pagination limit value.", - Detail: fmt.Sprintf("Pagination limit must be in range (0, %d]", maxListClientsLimit), - }) - return - } - queryStr := r.URL.Query().Get("q") filter, errs := searchquery.AIBridgeClients(queryStr, page) diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index cba71e26178..47a2085af88 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -781,12 +781,12 @@ func TestAIBridgeListSessions(t *testing.T) { //nolint:gocritic // Owner role is irrelevant; testing pagination validation. res, err := client.AIBridgeListSessions(ctx, codersdk.AIBridgeListSessionsFilter{ Pagination: codersdk.Pagination{ - Limit: 1001, + Limit: codersdk.MaxPaginationLimit + 1, }, }) var sdkErr *codersdk.Error require.ErrorAs(t, err, &sdkErr) - require.Contains(t, sdkErr.Message, "Invalid pagination limit value.") + require.Contains(t, sdkErr.Message, "Query parameters have invalid values.") require.Empty(t, res.Sessions) }) diff --git a/enterprise/coderd/connectionlog.go b/enterprise/coderd/connectionlog.go index 9754abab520..c7734aea6d5 100644 --- a/enterprise/coderd/connectionlog.go +++ b/enterprise/coderd/connectionlog.go @@ -25,7 +25,7 @@ const connectionLogCountCap = 2000 // @Produce json // @Tags Enterprise // @Param q query string false "Search query" -// @Param limit query int true "Page limit" +// @Param limit query int false "Page limit" // @Param offset query int false "Page offset" // @Success 200 {object} codersdk.ConnectionLogResponse // @Router /api/v2/connectionlog [get] diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 8d275769cbf..b3b782ede89 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -48,7 +48,7 @@ export const SessionTokenCookie = "coder_session_token"; /** * WORKSPACE_PAGE_SIZE is the number of rows getAllWorkspaces requests per page. */ -export const WORKSPACE_PAGE_SIZE = 100; +export const WORKSPACE_PAGE_SIZE = TypesGen.MaxPaginationLimit; /** * @param agentId @@ -674,6 +674,32 @@ class ApiMethods { return response.data; }; + /** + * Requests successive pages of organization members until the offset reaches + * the total the server reports. + * + * @param organization Can be the organization's ID or name + */ + getAllOrganizationMembers = async ( + organization: string, + options: Omit = {}, + ): Promise => { + const members: TypesGen.OrganizationMemberWithUserData[] = []; + let count = 0; + let offset = 0; + do { + const page = await this.getOrganizationPaginatedMembers(organization, { + ...options, + limit: TypesGen.MaxPaginationLimit, + offset, + }); + members.push(...page.members); + count = page.count; + offset += TypesGen.MaxPaginationLimit; + } while (offset < count); + return { members, count }; + }; + /** * @param organization Can be the organization's ID or name */ diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 1dcaac36596..7c2c731de6d 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -93,6 +93,30 @@ export const organizationMembers = (id: string, req: UsersRequest) => { }; }; +type AllOrganizationMembersRequest = Omit; + +export const allOrganizationMembersKey = ( + id: string, + req: AllOrganizationMembersRequest, +) => ["organization", id, "members", { ...req, all: true }]; + +/** + * Creates a query configuration that fetches every page of an organization's + * members. Prefer a server-side search filter and a single page when the caller + * can express one. + * + * @param id - The unique identifier of the organization + */ +export const allOrganizationMembers = ( + id: string, + req: AllOrganizationMembersRequest = {}, +) => { + return { + queryFn: () => API.getAllOrganizationMembers(id, req), + queryKey: allOrganizationMembersKey(id, req), + }; +}; + export const paginatedOrganizationMembers = ( id: string, searchParams: URLSearchParams, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0222095336d..3aa17a25039 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -5952,6 +5952,14 @@ export const MaxChatFileIDs = 50; */ export const MaxChatFileSizeBytes = 10485760; +// From codersdk/pagination.go +/** + * MaxPaginationLimit is the largest page size the server accepts. An omitted + * limit resolves to this value. A limit that is set must be a positive integer + * no greater than this, so the resulting query is always bounded. + */ +export const MaxPaginationLimit = 100; + // From codersdk/usersecretsimport.go /** * MaxSecretsFileBytes bounds the raw size of a secrets file before parsing. @@ -6984,9 +6992,11 @@ export interface Pagination { */ readonly after_id?: string; /** - * Limit sets the maximum number of users to be returned - * in a single page. If the limit is <= 0, there is no limit - * and all users are returned. + * Limit sets the maximum number of results returned in a single page. + * When set, it must be a positive integer no greater than + * MaxPaginationLimit, otherwise the request is rejected. A value of 0 + * omits the query parameter entirely, and the server resolves the page + * size to MaxPaginationLimit. */ readonly limit?: number; /** @@ -11345,6 +11355,12 @@ export interface WorkspaceOptions { readonly include_deleted?: boolean; } +// From codersdk/workspaces.go +/** + * WorkspacePageSize is the number of rows AllWorkspaces requests per page. + */ +export const WorkspacePageSize = 100; + // From codersdk/workspaceproxy.go export interface WorkspaceProxy extends Region { readonly derp_enabled: boolean; diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx index b04f4251eb8..a59e51594e5 100644 --- a/site/src/components/UserAutocomplete/UserAutocomplete.tsx +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -1,7 +1,7 @@ import { type FC, useId, useState } from "react"; import { keepPreviousData, useQuery } from "react-query"; import { getErrorMessage } from "#/api/errors"; -import { organizationMembers } from "#/api/queries/organizations"; +import { allOrganizationMembers } from "#/api/queries/organizations"; import { users, workspaceAvailableUsers } from "#/api/queries/users"; import type { MinimalUser, @@ -79,7 +79,7 @@ export const MemberAutocomplete: FC = ({ const [filter, setFilter] = useState(); const membersQuery = useQuery({ - ...organizationMembers(organizationId, { limit: 0 }), + ...allOrganizationMembers(organizationId), enabled: filter !== undefined, placeholderData: keepPreviousData, }); diff --git a/site/src/modules/workspaces/WorkspaceSharingForm/UserOrGroupAutocomplete.tsx b/site/src/modules/workspaces/WorkspaceSharingForm/UserOrGroupAutocomplete.tsx index 4a099987182..1e5b0e930de 100644 --- a/site/src/modules/workspaces/WorkspaceSharingForm/UserOrGroupAutocomplete.tsx +++ b/site/src/modules/workspaces/WorkspaceSharingForm/UserOrGroupAutocomplete.tsx @@ -2,7 +2,7 @@ import { CheckIcon } from "lucide-react"; import { type FC, useState } from "react"; import { keepPreviousData, useQuery } from "react-query"; import { groupsByOrganization } from "#/api/queries/groups"; -import { organizationMembers } from "#/api/queries/organizations"; +import { allOrganizationMembers } from "#/api/queries/organizations"; import type { Group, OrganizationMemberWithUserData, @@ -53,7 +53,7 @@ export const UserOrGroupAutocomplete: FC = ({ // This allows regular org members to see other members in their org // for workspace sharing, without needing site-wide user:read permission. const membersQuery = useQuery({ - ...organizationMembers(organizationId, { limit: 0 }), + ...allOrganizationMembers(organizationId), enabled: open, placeholderData: keepPreviousData, }); diff --git a/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx b/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx index 2b449c7e416..51c26809cc6 100644 --- a/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx +++ b/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx @@ -99,7 +99,7 @@ const noBudgetStatus = aiSpendStatus({ const userWorkspacesRequest = { q: `owner:me organization:${MockDefaultOrganization.name}`, - limit: 0, + limit: 1, }; const noWorkspaceQuota = { credits_consumed: 0, diff --git a/site/src/pages/AgentsPage/components/UsageIndicator.tsx b/site/src/pages/AgentsPage/components/UsageIndicator.tsx index 872e20b76a1..10173ec7c43 100644 --- a/site/src/pages/AgentsPage/components/UsageIndicator.tsx +++ b/site/src/pages/AgentsPage/components/UsageIndicator.tsx @@ -69,7 +69,8 @@ export const UsageIndicator: FC = () => { const workspacesQuery = useQuery({ ...workspaces({ q: `owner:me organization:${organizationName}`, - limit: 0, + // Only the count is read, so request the smallest valid page. + limit: 1, }), enabled: hasWorkspaceQuotaUsage && organizationName !== "", }); diff --git a/site/src/pages/OrganizationSettingsPage/DisableWorkspaceSharingDialog.tsx b/site/src/pages/OrganizationSettingsPage/DisableWorkspaceSharingDialog.tsx index b95b9aea1d1..6e5c859dbab 100644 --- a/site/src/pages/OrganizationSettingsPage/DisableWorkspaceSharingDialog.tsx +++ b/site/src/pages/OrganizationSettingsPage/DisableWorkspaceSharingDialog.tsx @@ -39,7 +39,8 @@ export const DisableWorkspaceSharingDialog: FC< queryFn: async () => { const response = await API.getWorkspaces({ q: `organization:${organizationId} shared:true`, - limit: 0, // Avoid fetching workspaces as we only need the count. + // Only the count is read, so request the smallest valid page. + limit: 1, }); return response.count; }, diff --git a/support/support.go b/support/support.go index c46d80ad7fc..6ece4ac8631 100644 --- a/support/support.go +++ b/support/support.go @@ -266,7 +266,7 @@ func DeploymentInfo(ctx context.Context, client *codersdk.Client, log slog.Logge eg.Go(func() error { var ( offset int - limit = 200 + limit = codersdk.MaxPaginationLimit all []codersdk.Workspace count int )