diff --git a/backend/oasst_backend/api/v1/frontend_users.py b/backend/oasst_backend/api/v1/frontend_users.py index e78added..0b2db515 100644 --- a/backend/oasst_backend/api/v1/frontend_users.py +++ b/backend/oasst_backend/api/v1/frontend_users.py @@ -19,14 +19,14 @@ router = APIRouter() def get_users( api_client_id: Optional[UUID] = None, max_count: Optional[int] = Query(100, gt=0, le=10000), - gte: Optional[str] = None, + gt: Optional[str] = None, lt: Optional[str] = None, auth_method: Optional[str] = None, api_client: ApiClient = Depends(deps.get_api_client), db: Session = Depends(deps.get_db), ): ur = UserRepository(db, api_client) - users = ur.query_users(api_client_id=api_client_id, limit=max_count, gte=gte, lt=lt, auth_method=auth_method) + users = ur.query_users(api_client_id=api_client_id, limit=max_count, gt=gt, lt=lt, auth_method=auth_method) return [u.to_protocol_frontend_user() for u in users] diff --git a/backend/oasst_backend/user_repository.py b/backend/oasst_backend/user_repository.py index 26de963f..5234dbf4 100644 --- a/backend/oasst_backend/user_repository.py +++ b/backend/oasst_backend/user_repository.py @@ -162,7 +162,7 @@ class UserRepository: self, api_client_id: Optional[UUID] = None, limit: Optional[int] = 20, - gte: Optional[str] = None, + gt: Optional[str] = None, lt: Optional[str] = None, auth_method: Optional[str] = None, ) -> list[User]: @@ -183,8 +183,8 @@ class UserRepository: users = users.order_by(User.display_name) - if gte: - users = users.filter(User.display_name >= gte) + if gt: + users = users.filter(User.display_name > gt) if lt: users = users.filter(User.display_name < lt) diff --git a/website/src/components/UsersCell.tsx b/website/src/components/UsersCell.tsx index f5545be1..7f165431 100644 --- a/website/src/components/UsersCell.tsx +++ b/website/src/components/UsersCell.tsx @@ -11,6 +11,7 @@ import { Th, Thead, Tr, + useToast, } from "@chakra-ui/react"; import Link from "next/link"; import { useState } from "react"; @@ -18,26 +19,60 @@ import { get } from "src/lib/api"; import type { User } from "src/types/Users"; import useSWR from "swr"; +interface Pagination { + /** + * The user's `display_name` used for pagination. + */ + cursor: string; + + /** + * The pagination direction. + */ + direction: "forward" | "back"; +} + /** * Fetches users from the users api route and then presents them in a simple Chakra table. */ const UsersCell = () => { - const [pageIndex, setPageIndex] = useState(0); + const toast = useToast(); + const [pagination, setPagination] = useState({ cursor: "", direction: "forward" }); const [users, setUsers] = useState([]); // Fetch and save the users. // This follows useSWR's recommendation for simple pagination: // https://swr.vercel.app/docs/pagination#when-to-use-useswr - useSWR(`/api/admin/users?pageIndex=${pageIndex}`, get, { - onSuccess: setUsers, + useSWR(`/api/admin/users?direction=${pagination.direction}&cursor=${pagination.cursor}`, get, { + onSuccess: (data) => { + // When no more users can be found, trigger a toast to indicate why no + // changes have taken place. We have to maintain a non-empty set of + // users otherwise we can't paginate using a cursor (since we've lost the + // cursor). + if (data.length === 0) { + toast({ + title: "No more users", + status: "warning", + duration: 1000, + isClosable: true, + }); + return; + } + setUsers(data); + }, }); const toPreviousPage = () => { - setPageIndex(Math.max(0, pageIndex - 1)); + setPagination({ + cursor: users[0].display_name, + direction: "back", + }); }; const toNextPage = () => { - setPageIndex(pageIndex + 1); + setPagination({ + cursor: users[users.length - 1].display_name, + direction: "forward", + }); }; // Present users in a naive table. diff --git a/website/src/lib/oasst_api_client.ts b/website/src/lib/oasst_api_client.ts index 43ca6c49..31de930b 100644 --- a/website/src/lib/oasst_api_client.ts +++ b/website/src/lib/oasst_api_client.ts @@ -157,10 +157,27 @@ export class OasstApiClient { } /** - * Returns the `max_count` `BackendUser`s stored by the backend. + * Returns the set of `BackendUser`s stored by the backend. + * + * @param {number} max_count - The maximum number of users to fetch. + * @param {string} cursor - The user's `display_name` to use when paginating. + * @param {boolean} isForward - If true and `cursor` is not empty, pages + * forward. If false and `cursor` is not empty, pages backwards. + * @returns {Promise} A Promise that returns an array of `BackendUser` objects. */ - async fetch_users(max_count: number): Promise { - return this.get(`/api/v1/frontend_users/?max_count=${max_count}`); + async fetch_users(max_count: number, cursor: string, isForward: boolean): Promise { + const params = new URLSearchParams(); + params.append("max_count", max_count.toString()); + + // The backend API uses different query paramters depending on the + // pagination direction but they both take the same cursor value. + // Depending on direction, pick the right query param. + if (cursor !== "") { + params.append(isForward ? "gt" : "lt", cursor); + } + const BASE_URL = `/api/v1/frontend_users`; + const url = `${BASE_URL}/?${params.toString()}`; + return this.get(url); } /** diff --git a/website/src/pages/api/admin/users.ts b/website/src/pages/api/admin/users.ts index 04bcacf0..e600650d 100644 --- a/website/src/pages/api/admin/users.ts +++ b/website/src/pages/api/admin/users.ts @@ -2,17 +2,27 @@ import { withRole } from "src/lib/auth"; import { oasstApiClient } from "src/lib/oasst_api_client"; import prisma from "src/lib/prismadb"; +/** + * The number of users to fetch in a single request. Could later be a query parameter. + */ +const PAGE_SIZE = 20; + /** * Returns a list of user results from the database when the requesting user is * a logged in admin. + * + * This takes two query params: + * - `cursor`: A string representing a user's `display_name`. + * - `direction`: Either "forward" or "backward" representing the pagination + * direction. */ const handler = withRole("admin", async (req, res) => { - // TODO(#673): Update this to support pagination. + const { cursor, direction } = req.query; // First, get all the users according to the backend. - const all_users = await oasstApiClient.fetch_users(20); + const all_users = await oasstApiClient.fetch_users(PAGE_SIZE, cursor as string, direction === "forward"); - // Next, get all the users stored in the web's auth datbase to fetch their role. + // Next, get all the users stored in the web's auth database to fetch their role. const local_user_ids = all_users.map(({ id }) => id); const local_users = await prisma.user.findMany({ where: {