use cursor endpoint

This commit is contained in:
notmd
2023-01-21 14:34:22 +07:00
parent 1f945fbdd7
commit 7eb5023e82
3 changed files with 86 additions and 54 deletions
+34 -29
View File
@@ -4,6 +4,7 @@ import Link from "next/link";
import { memo, useState } from "react";
import { FaPen } from "react-icons/fa";
import { get } from "src/lib/api";
import { FetchUsersResponse } from "src/lib/oasst_api_client";
import type { User } from "src/types/Users";
import useSWR from "swr";
@@ -58,39 +59,43 @@ const columns: DataTableColumnDef<User>[] = [
export const UserTable = memo(function UserTable() {
const toast = useToast();
const [pagination, setPagination] = useState<Pagination>({ cursor: "", direction: "forward" });
const [users, setUsers] = useState<User[]>([]);
const [response, setResponse] = useState<Omit<FetchUsersResponse<User>, "sort_key" | "order">>({
items: [],
});
const [filterValues, setFilterValues] = useState<FilterItem[]>([]);
const handleFilterValuesChange = (values: FilterItem[]) => {
setFilterValues(values);
setPagination((old) => ({ ...old, cursor: "" }));
};
// Fetch and save the users.
// This follows useSWR's recommendation for simple pagination:
// https://swr.vercel.app/docs/pagination#when-to-use-useswr
const display_name = filterValues.find((value) => value.id === "display_name")?.value ?? "";
useSWR(
`/api/admin/users?direction=${pagination.direction}&cursor=${pagination.cursor}&display_name=${display_name}`,
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);
},
}
);
useSWR<
FetchUsersResponse<User>
>(`/api/admin/users?direction=${pagination.direction}&cursor=${pagination.cursor}&searchDisplayName=${display_name}&sortKey=display_name`, 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.items.length === 0) {
toast({
title: "No more users",
status: "warning",
duration: 1000,
isClosable: true,
});
return;
}
setResponse(data);
},
});
const toPreviousPage = () => {
if (users.length >= 0) {
if (response.items.length >= 0) {
setPagination({
cursor: users[0].user_id,
cursor: response.prev,
direction: "back",
});
} else {
@@ -104,9 +109,9 @@ export const UserTable = memo(function UserTable() {
};
const toNextPage = () => {
if (users.length >= 0) {
if (response.items.length >= 0) {
setPagination({
cursor: users[users.length - 1].user_id,
cursor: response.next,
direction: "forward",
});
} else {
@@ -121,13 +126,13 @@ export const UserTable = memo(function UserTable() {
return (
<DataTable
data={users}
data={response.items}
columns={columns}
caption="Users"
onNextClick={toNextPage}
onPreviousClick={toPreviousPage}
filterValues={filterValues}
onFilterChange={setFilterValues}
onFilterChange={handleFilterValuesChange}
></DataTable>
);
});
+38 -13
View File
@@ -1,7 +1,7 @@
import type { Message } from "src/types/Conversation";
import { LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
import type { AvailableTasks } from "src/types/Task";
import type { BackendUser, BackendUserCore } from "src/types/Users";
import type { BackendUser, BackendUserCore, User } from "src/types/Users";
export class OasstError {
message: string;
@@ -15,6 +15,22 @@ export class OasstError {
}
}
export type FetchUsersParams = {
limit: number;
cursor?: string;
direction: "forward" | "back";
searchDisplayName?: string;
sortKey?: "username" | "display_name";
};
export type FetchUsersResponse<T extends User | BackendUser = BackendUser> = {
items: T[];
next?: string;
prev?: string;
sort_key: "username" | "display_name";
order: "asc" | "desc";
};
export class OasstApiClient {
oasstApiUrl: string;
oasstApiKey: string;
@@ -164,30 +180,39 @@ export class OasstApiClient {
* forward. If false and `cursor` is not empty, pages backwards.
* @returns {Promise<BackendUser[]>} A Promise that returns an array of `BackendUser` objects.
*/
async fetch_users(max_count: number, cursor: string, isForward: boolean): Promise<BackendUser[]> {
const params = new URLSearchParams();
params.append("max_count", max_count.toString());
async fetch_users({
direction,
limit,
cursor,
searchDisplayName,
sortKey = "display_name",
}: FetchUsersParams): Promise<FetchUsersResponse> {
const params = new URLSearchParams({
search_text: searchDisplayName,
sort_key: sortKey,
max_count: limit.toString(),
});
// The backend API uses different query parameters 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);
params.append(direction === "forward" ? "gt" : "lt", cursor);
}
const BASE_URL = `/api/v1/frontend_users`;
const BASE_URL = `/api/v1/users/cursor`;
const url = `${BASE_URL}/?${params.toString()}`;
return this.get(url);
}
async fetch_user_by_display_name(name: string): Promise<BackendUser[]> {
const params = new URLSearchParams({
search_text: name,
});
// async fetch_user_by_display_name(name: string): Promise<BackendUser[]> {
// const params = new URLSearchParams({
// search_text: name,
// });
const endpoint = `/api/v1/frontend_users/by_display_name`;
// const endpoint = `/api/v1/frontend_users/by_display_name`;
return this.get(`${endpoint}?${params.toString()}`);
}
// return this.get(`${endpoint}?${params.toString()}`);
// }
/**
* Returns the `Message`s associated with `user_id` in the backend.
+14 -12
View File
@@ -1,12 +1,11 @@
import { withRole } from "src/lib/auth";
import { oasstApiClient } from "src/lib/oasst_api_client";
import { FetchUsersParams, oasstApiClient } from "src/lib/oasst_api_client";
import prisma from "src/lib/prismadb";
import { BackendUser } from "src/types/Users";
/**
* The number of users to fetch in a single request. Could later be a query parameter.
*/
const PAGE_SIZE = 20;
const PAGE_SIZE = 2;
/**
* Returns a list of user results from the database when the requesting user is
@@ -18,16 +17,16 @@ const PAGE_SIZE = 20;
* direction.
*/
const handler = withRole("admin", async (req, res) => {
const { cursor, direction, display_name = "" } = req.query;
const { cursor, direction, searchDisplayName = "", sortKey = "username" } = req.query;
// First, get all the users according to the backend.
let all_users: BackendUser[] = [];
if (typeof display_name === "string" && display_name) {
all_users = await oasstApiClient.fetch_user_by_display_name(display_name);
} else {
all_users = await oasstApiClient.fetch_users(PAGE_SIZE, cursor as string, direction === "forward");
}
const { items: all_users, ...rest } = await oasstApiClient.fetch_users({
searchDisplayName: searchDisplayName as FetchUsersParams["searchDisplayName"],
direction: direction as FetchUsersParams["direction"],
limit: PAGE_SIZE,
cursor: cursor as FetchUsersParams["cursor"],
sortKey: sortKey === "username" || sortKey === "display_name" ? sortKey : undefined,
});
// 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);
@@ -58,7 +57,10 @@ const handler = withRole("admin", async (req, res) => {
};
});
res.status(200).json(users);
res.status(200).json({
items: users,
...rest,
});
});
export default handler;