mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 08:57:31 +08:00
fix: Search beyond the first page of users (#12203)
* fix: Search beyond the first page of users * Add a test for search functionality * Update users.py * Update index.tsx * Update index.tsx * Update auto-login-off.spec.ts * Update auto-login-off.spec.ts * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -59,13 +59,20 @@ async def read_all_users(
|
||||
*,
|
||||
skip: int = 0,
|
||||
limit: int = 10,
|
||||
search: str | None = None,
|
||||
session: DbSession,
|
||||
) -> UsersResponse:
|
||||
"""Retrieve a list of users from the database with pagination."""
|
||||
query: SelectOfScalar = select(User).offset(skip).limit(limit)
|
||||
users = (await session.exec(query)).fetchall()
|
||||
|
||||
query: SelectOfScalar = select(User)
|
||||
count_query = select(func.count()).select_from(User)
|
||||
|
||||
if search:
|
||||
search_filter = User.username.ilike(f"%{search}%") # type: ignore[attr-defined]
|
||||
query = query.where(search_filter)
|
||||
count_query = count_query.where(search_filter)
|
||||
|
||||
query = query.offset(skip).limit(limit)
|
||||
users = (await session.exec(query)).fetchall()
|
||||
total_count = (await session.exec(count_query)).first()
|
||||
|
||||
return UsersResponse(
|
||||
|
||||
@ -161,6 +161,76 @@ async def test_patch_user_self_deactivation_forbidden_superuser(
|
||||
assert "can't deactivate your own user account" in result["detail"]
|
||||
|
||||
|
||||
async def test_read_all_users_search(client: AsyncClient, logged_in_headers_super_user):
|
||||
"""Test that the search parameter filters users by username across all pages."""
|
||||
# Create several users with distinct usernames
|
||||
usernames = ["alice_search", "bob_search", "charlie_search"]
|
||||
created_ids = []
|
||||
for username in usernames:
|
||||
response = await client.post(
|
||||
"api/v1/users/",
|
||||
json={"username": username, "password": "password123"},
|
||||
headers=logged_in_headers_super_user,
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
created_ids.append(response.json()["id"])
|
||||
|
||||
# Search for "alice" — should return exactly one match
|
||||
response = await client.get(
|
||||
"api/v1/users/?search=alice",
|
||||
headers=logged_in_headers_super_user,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
result = response.json()
|
||||
assert result["total_count"] == 1
|
||||
assert result["users"][0]["username"] == "alice_search"
|
||||
|
||||
# Search for "_search" — should match all three created users
|
||||
response = await client.get(
|
||||
"api/v1/users/?search=_search",
|
||||
headers=logged_in_headers_super_user,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
result = response.json()
|
||||
assert result["total_count"] == 3
|
||||
returned_usernames = {u["username"] for u in result["users"]}
|
||||
assert returned_usernames == set(usernames)
|
||||
|
||||
# Search for a non-existent username — should return zero results
|
||||
response = await client.get(
|
||||
"api/v1/users/?search=nonexistentuser",
|
||||
headers=logged_in_headers_super_user,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
result = response.json()
|
||||
assert result["total_count"] == 0
|
||||
assert result["users"] == []
|
||||
|
||||
# Search is case-insensitive
|
||||
response = await client.get(
|
||||
"api/v1/users/?search=ALICE",
|
||||
headers=logged_in_headers_super_user,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
result = response.json()
|
||||
assert result["total_count"] == 1
|
||||
assert result["users"][0]["username"] == "alice_search"
|
||||
|
||||
# Search combined with pagination: limit=1 should return 1 user but total_count=3
|
||||
response = await client.get(
|
||||
"api/v1/users/?search=_search&limit=1",
|
||||
headers=logged_in_headers_super_user,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
result = response.json()
|
||||
assert result["total_count"] == 3
|
||||
assert len(result["users"]) == 1
|
||||
|
||||
# Clean up
|
||||
for user_id in created_ids:
|
||||
await client.delete(f"api/v1/users/{user_id}", headers=logged_in_headers_super_user)
|
||||
|
||||
|
||||
async def test_patch_user_deactivate_other_user_allowed(client: AsyncClient, logged_in_headers_super_user):
|
||||
"""Test that a superuser can deactivate another user's account."""
|
||||
# Create a new user
|
||||
|
||||
@ -7,6 +7,7 @@ import { UseRequestProcessor } from "../../services/request-processor";
|
||||
interface getUsersQueryParams {
|
||||
skip: number;
|
||||
limit: number;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export const useGetUsers: useMutationFunctionType<any, getUsersQueryParams> = (
|
||||
@ -17,10 +18,13 @@ export const useGetUsers: useMutationFunctionType<any, getUsersQueryParams> = (
|
||||
async function getUsers({
|
||||
skip,
|
||||
limit,
|
||||
search,
|
||||
}: getUsersQueryParams): Promise<Array<Users>> {
|
||||
const res = await api.get(
|
||||
`${getURL("USERS")}/?skip=${skip}&limit=${limit}`,
|
||||
);
|
||||
let url = `${getURL("USERS")}/?skip=${skip}&limit=${limit}`;
|
||||
if (search) {
|
||||
url += `&search=${encodeURIComponent(search)}`;
|
||||
}
|
||||
const res = await api.get(url);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { cloneDeep, debounce } from "lodash";
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import PaginatorComponent from "@/components/common/paginatorComponent";
|
||||
import {
|
||||
useAddUser,
|
||||
@ -59,71 +59,62 @@ export default function AdminPage() {
|
||||
const { mutate: mutateUpdateUser } = useUpdateUser();
|
||||
const { mutate: mutateAddUser } = useAddUser();
|
||||
|
||||
const userList = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
getUsers();
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
const [filterUserList, setFilterUserList] = useState(userList.current);
|
||||
const [userList, setUserList] = useState<Users[]>([]);
|
||||
|
||||
const { mutate: mutateGetUsers, isPending, isIdle } = useGetUsers({});
|
||||
|
||||
function getUsers() {
|
||||
mutateGetUsers(
|
||||
{
|
||||
skip: size * (index - 1),
|
||||
limit: size,
|
||||
},
|
||||
{
|
||||
onSuccess: (users) => {
|
||||
setTotalRowsCount(users["total_count"]);
|
||||
userList.current = users["users"];
|
||||
setFilterUserList(users["users"]);
|
||||
const fetchUsers = useCallback(
|
||||
(skip: number, limit: number, search?: string) => {
|
||||
mutateGetUsers(
|
||||
{ skip, limit, search: search || undefined },
|
||||
{
|
||||
onSuccess: (users) => {
|
||||
setTotalRowsCount(users["total_count"]);
|
||||
setUserList(users["users"]);
|
||||
},
|
||||
onError: () => {},
|
||||
},
|
||||
onError: () => {},
|
||||
},
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
[mutateGetUsers],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
fetchUsers(size * (index - 1), size);
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
function handleChangePagination(pageIndex: number, pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setPageIndex(pageIndex);
|
||||
|
||||
mutateGetUsers(
|
||||
{
|
||||
skip: pageSize * (pageIndex - 1),
|
||||
limit: pageSize,
|
||||
},
|
||||
{
|
||||
onSuccess: (users) => {
|
||||
setTotalRowsCount(users["total_count"]);
|
||||
userList.current = users["users"];
|
||||
setFilterUserList(users["users"]);
|
||||
},
|
||||
},
|
||||
);
|
||||
fetchUsers(pageSize * (pageIndex - 1), pageSize, inputValue);
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
setInputValue("");
|
||||
setPageIndex(PAGINATION_PAGE);
|
||||
setPageSize(PAGINATION_SIZE);
|
||||
getUsers();
|
||||
fetchUsers(0, PAGINATION_SIZE);
|
||||
}
|
||||
|
||||
const debouncedSearch = useCallback(
|
||||
debounce((search: string) => {
|
||||
setPageIndex(PAGINATION_PAGE);
|
||||
fetchUsers(0, size, search);
|
||||
}, 300),
|
||||
[size, fetchUsers],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSearch.cancel();
|
||||
};
|
||||
}, [debouncedSearch]);
|
||||
|
||||
function handleFilterUsers(input: string) {
|
||||
setInputValue(input);
|
||||
|
||||
if (input === "") {
|
||||
setFilterUserList(userList.current);
|
||||
} else {
|
||||
const filteredList = userList.current.filter((user: Users) =>
|
||||
user.username.toLowerCase().includes(input.toLowerCase()),
|
||||
);
|
||||
setFilterUserList(filteredList);
|
||||
}
|
||||
debouncedSearch(input);
|
||||
}
|
||||
|
||||
function handleDeleteUser(user) {
|
||||
@ -276,7 +267,8 @@ export default function AdminPage() {
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
setInputValue("");
|
||||
setFilterUserList(userList.current);
|
||||
setPageIndex(PAGINATION_PAGE);
|
||||
fetchUsers(0, size);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="X" className="w-6 text-foreground" />
|
||||
@ -306,11 +298,11 @@ export default function AdminPage() {
|
||||
</UserManagementModal>
|
||||
</div>
|
||||
</div>
|
||||
{isPending || isIdle ? (
|
||||
{(isPending || isIdle) && userList.length === 0 ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<CustomLoader remSize={12} />
|
||||
</div>
|
||||
) : userList.current.length === 0 && !isIdle ? (
|
||||
) : userList.length === 0 && !isPending && !isIdle ? (
|
||||
<>
|
||||
<div className="m-4 flex items-center justify-between text-sm">
|
||||
No users registered.
|
||||
@ -340,10 +332,10 @@ export default function AdminPage() {
|
||||
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
{!isPending && (
|
||||
{
|
||||
<TableBody className="border-b">
|
||||
{filterUserList.map((user: UserInputType, index) => (
|
||||
<TableRow key={index}>
|
||||
{userList.map((user: UserInputType, index) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="truncate py-2 font-medium">
|
||||
<ShadTooltip content={user.id}>
|
||||
<span className="cursor-default">{user.id}</span>
|
||||
@ -496,7 +488,7 @@ export default function AdminPage() {
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
)}
|
||||
}
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
|
||||
@ -111,7 +111,12 @@ test(
|
||||
|
||||
await page.waitForSelector("text=new user added", { timeout: 30000 });
|
||||
|
||||
const searchResponse = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes("/api/v1/users") && response.status() === 200,
|
||||
);
|
||||
await page.getByPlaceholder("Username").last().fill(randomName);
|
||||
await searchResponse;
|
||||
|
||||
await page.getByTestId("icon-Pencil").last().click();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user