Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
fix(site,support): adopt the bounded page size
An explicit limit of 0 is now rejected, so the callers that used it either
request a single row when they only read the count, or page to exhaustion.
Workspace and support page sizes derive from MaxPaginationLimit.
  • Loading branch information
jscottmiller committed Aug 11, 2026
commit 546969fd92d062484c2efda97fc3835c17869835
2 changes: 1 addition & 1 deletion codersdk/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 27 additions & 1 deletion site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<TypesGen.UsersRequest, "limit" | "offset"> = {},
): Promise<TypesGen.PaginatedMembersResponse> => {
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
*/
Expand Down
24 changes: 24 additions & 0 deletions site/src/api/queries/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,30 @@ export const organizationMembers = (id: string, req: UsersRequest) => {
};
};

type AllOrganizationMembersRequest = Omit<UsersRequest, "limit" | "offset">;

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,
Expand Down
20 changes: 17 additions & 3 deletions site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions site/src/components/UserAutocomplete/UserAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -79,7 +79,7 @@ export const MemberAutocomplete: FC<MemberAutocompleteProps> = ({
const [filter, setFilter] = useState<string>();

const membersQuery = useQuery({
...organizationMembers(organizationId, { limit: 0 }),
...allOrganizationMembers(organizationId),
enabled: filter !== undefined,
placeholderData: keepPreviousData,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -53,7 +53,7 @@ export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
// 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,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const noBudgetStatus = aiSpendStatus({

const userWorkspacesRequest = {
q: `owner:me organization:${MockDefaultOrganization.name}`,
limit: 0,
limit: 1,
};
const noWorkspaceQuota = {
credits_consumed: 0,
Expand Down
3 changes: 2 additions & 1 deletion site/src/pages/AgentsPage/components/UsageIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 !== "",
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down
2 changes: 1 addition & 1 deletion support/support.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Loading