From dd8ccc62b9eb2ecce5d0698f0125dbfc677e5c26 Mon Sep 17 00:00:00 2001 From: Dennis Chen <41879777+chennisden@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:36:09 -0700 Subject: [PATCH] Query batch (#141) * Apparently Edit Part was removed somehow?? * Make card heights consistent * Query owners in batch * paginate in case of >100 users --- frontend/src/App.css | 4 ++++ frontend/src/App.tsx | 2 ++ frontend/src/hooks/api.tsx | 15 +++++++++++++++ frontend/src/pages/Parts.tsx | 18 +----------------- frontend/src/pages/Robots.tsx | 14 +------------- store/app/crud/users.py | 10 ++++++++++ store/app/routers/users.py | 33 ++++++++++++++++++++++++++------- 7 files changed, 59 insertions(+), 37 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 61653ecc..69e77761 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -27,6 +27,10 @@ form button { width: 100%; } +div.card { + height: 100%; +} + p.card-text { flex-grow: 1; max-height: 100%; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fb8e7927..eca9af32 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import { AuthenticationProvider } from "hooks/auth"; import { ThemeProvider } from "hooks/theme"; import About from "pages/About"; import ChangeEmail from "pages/ChangeEmail"; +import EditPartForm from "pages/EditPartForm"; import EditRobotForm from "pages/EditRobotForm"; import Forgot from "pages/Forgot"; import Home from "pages/Home"; @@ -66,6 +67,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/hooks/api.tsx b/frontend/src/hooks/api.tsx index 88f0f837..477271bf 100644 --- a/frontend/src/hooks/api.tsx +++ b/frontend/src/hooks/api.tsx @@ -240,6 +240,7 @@ export class api { const response = await this.api.get(`/users/${userId}`); return response.data.username; } + public async getRobots(): Promise { try { const response = await this.api.get("/robots/"); @@ -256,6 +257,20 @@ export class api { } } } + + public async getUserBatch(userIds: string[]): Promise> { + const params = new URLSearchParams(); + userIds.forEach((id) => params.append("user_ids", id)); + const response = await this.api.get("/users/batch/", { + params, + }); + const map = new Map(); + for (const index in response.data) { + map.set(response.data[index].user_id, response.data[index].username); + } + return map; + } + public async getYourRobots(): Promise { try { const response = await this.api.get("/robots/your/"); diff --git a/frontend/src/pages/Parts.tsx b/frontend/src/pages/Parts.tsx index f317d49b..a178a80f 100644 --- a/frontend/src/pages/Parts.tsx +++ b/frontend/src/pages/Parts.tsx @@ -13,7 +13,6 @@ import { } from "react-bootstrap"; import Markdown from "react-markdown"; import { useNavigate } from "react-router-dom"; -import { isFulfilled } from "utils/isfullfiled"; const Parts = () => { const auth = useAuthentication(); @@ -32,22 +31,7 @@ const Parts = () => { partsQuery.forEach((part) => { ids.add(part.owner); }); - const idMap = await Promise.allSettled( - Array.from(ids).map(async (id) => { - try { - return [id, await auth_api.getUserById(id)]; - } catch (err) { - return null; - } - }), - ); - setIdMap( - new Map( - idMap - .filter(isFulfilled) - .map((result) => result.value as [string, string]), - ), - ); + setIdMap(await auth_api.getUserBatch(Array.from(ids))); } catch (err) { if (err instanceof Error) { setError(err.message); diff --git a/frontend/src/pages/Robots.tsx b/frontend/src/pages/Robots.tsx index c8a44dfa..e0b4fc74 100644 --- a/frontend/src/pages/Robots.tsx +++ b/frontend/src/pages/Robots.tsx @@ -12,7 +12,6 @@ import { } from "react-bootstrap"; import Markdown from "react-markdown"; import { useNavigate } from "react-router-dom"; -import { isFulfilled } from "utils/isfullfiled"; const Robots = () => { const auth = useAuthentication(); @@ -29,18 +28,7 @@ const Robots = () => { robotsQuery.forEach((robot) => { ids.add(robot.owner); }); - const idMap = await Promise.allSettled( - Array.from(ids).map(async (id) => { - return [id, await auth_api.getUserById(id)]; - }), - ); - setIdMap( - new Map( - idMap - .filter(isFulfilled) - .map((result) => result.value as [string, string]), - ), - ); + setIdMap(await auth_api.getUserBatch(Array.from(ids))); } catch (err) { if (err instanceof Error) { setError(err.message); diff --git a/store/app/crud/users.py b/store/app/crud/users.py index 45ee8de5..f24fbd46 100644 --- a/store/app/crud/users.py +++ b/store/app/crud/users.py @@ -26,6 +26,16 @@ async def get_user(self, user_id: str) -> User | None: return None return User.model_validate(user_dict["Item"]) + async def get_user_batch(self, user_ids: list[str]) -> list[User]: + users: list[User] = [] + chunk_size = 100 + for i in range(0, len(user_ids), chunk_size): + chunk = user_ids[i : i + chunk_size] + keys = [{"user_id": user_id} for user_id in chunk] + response = await self.db.batch_get_item(RequestItems={"Users": {"Keys": keys}}) + users.extend(User.model_validate(user) for user in response["Responses"]["Users"]) + return users + async def get_user_from_email(self, email: str) -> User | None: table = await self.db.Table("Users") user_dict = await table.query( diff --git a/store/app/routers/users.py b/store/app/routers/users.py index 62c032d9..1ebd4b35 100644 --- a/store/app/routers/users.py +++ b/store/app/routers/users.py @@ -4,7 +4,7 @@ from email.utils import parseaddr as parse_email_address from typing import Annotated -from fastapi import APIRouter, Depends, HTTPException, Request, Response, status +from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status from fastapi.security.utils import get_authorization_scheme_param from pydantic.main import BaseModel @@ -310,15 +310,34 @@ async def logout_user_endpoint( return True -@users_router.get("/{user_id}", response_model=UserInfoResponse) -async def get_user_info_by_id_endpoint(user_id: str, crud: Annotated[Crud, Depends(Crud.get)]) -> UserInfoResponse: +class PublicUserInfoResponse(BaseModel): + username: str + user_id: str + + +@users_router.get("/batch", response_model=list[PublicUserInfoResponse]) +async def get_users_batch_endpoint( + crud: Annotated[Crud, Depends(Crud.get)], + user_ids: list[str] = Query(...), +) -> list[PublicUserInfoResponse]: + user_objs = await crud.get_user_batch(user_ids) + return [ + PublicUserInfoResponse( + username=user_obj.username, + user_id=user_obj.user_id, + ) + for user_obj in user_objs + ] + + +@users_router.get("/{user_id}", response_model=PublicUserInfoResponse) +async def get_user_info_by_id_endpoint( + user_id: str, crud: Annotated[Crud, Depends(Crud.get)] +) -> PublicUserInfoResponse: user_obj = await crud.get_user(user_id) if user_obj is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") - return UserInfoResponse( - email=user_obj.email, + return PublicUserInfoResponse( username=user_obj.username, user_id=user_obj.user_id, - verified=user_obj.verified, - admin=user_obj.admin, )