Skip to content

Commit

Permalink
AdminUserTable & Perf
Browse files Browse the repository at this point in the history
  • Loading branch information
slhmy committed Sep 25, 2024
1 parent 5909f3e commit 7082737
Show file tree
Hide file tree
Showing 14 changed files with 340 additions and 17 deletions.
3 changes: 2 additions & 1 deletion src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import RankList from "./pages/RankList";
const Problem = lazy(() => import("@/pages/problem/Problem"));
const AdminProblemList = lazy(() => import("@/pages/admin/ProblemList"));
const AdminCreateProblem = lazy(() => import("@/pages/admin/CreateProblem"));
const AdminUserList = lazy(() => import("@/pages/admin/UserList"));
const ProblemList = lazy(() => import("@/pages/problem/ProblemList"));
const JudgeList = lazy(() => import("@/pages//judge/JudgeList"));
const Judge = lazy(() => import("@/pages/judge/Judge"));
Expand Down Expand Up @@ -37,7 +38,7 @@ const Router: React.FC = () => {
/>
<Route path="problems" element={<AdminProblemList />} />
<Route path="problems/create" element={<AdminCreateProblem />} />
<Route path="users" element={<div>user</div>} />
<Route path="users" element={<AdminUserList />} />
</Route>
</Route>
</Routes>
Expand Down
27 changes: 27 additions & 0 deletions src/apis/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as UserServiceModel from "@/models/service/user";
import { axiosClient } from "@/utils/axiosClient";

export async function getUserInfoList(
limit?: number,
offset?: number,
): Promise<{
total: number;
list: UserServiceModel.UserInfo[];
}> {
limit = limit || 10;
offset = offset || 0;

let res = await axiosClient.get<{
total: number;
list: UserServiceModel.UserInfo[];
}>(`/api/v1/user`, {
params: {
limit,
offset,
},
});
if (res.status !== 200) {
throw Error("failed to get problem list");
}
return res.data;
}
10 changes: 4 additions & 6 deletions src/components/display/JudgeTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,10 @@ const JudgeTable: React.FC<JudgeTableProps> = (props) => {
<td>{judge.problem?.title}</td>
<td className="flex items-center gap-3 py-2">
<div className="avatar">
<div className="w-8 rounded-full">
<UserAvatar
alt={judge.user?.name}
avatarUrl={judge.user?.avatarUrl}
/>
</div>
<UserAvatar
alt={judge.user?.name}
avatarUrl={judge.user?.avatarUrl}
/>
</div>
<span>{judge.user?.name}</span>
</td>
Expand Down
4 changes: 2 additions & 2 deletions src/components/display/ProblemTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const ProblemTable: React.FC<ProblemTableProps> = (props) => {
</table>
</div>
<ConfirmDialog
id="delete_confirm_modal"
id="problem_delete_confirm_modal"
title="Confirm"
message="Are you sure to delete this problem?"
onClickConfirm={() => {
Expand Down Expand Up @@ -136,7 +136,7 @@ const ProblemActions: React.FC<ActionsProps> = (props) => {

props.onClickDelete();
const modal = document.getElementById(
"delete_confirm_modal",
"problem_delete_confirm_modal",
) as HTMLDialogElement;
modal?.showModal();
};
Expand Down
10 changes: 4 additions & 6 deletions src/components/display/RankTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,10 @@ const RankTable: React.FC<RankTableProps> = (props) => {
<th>{rank.rank}</th>
<td className="flex items-center gap-3 py-2">
<div className="avatar">
<div className="w-8 rounded-full">
<UserAvatar
alt={rank.user?.name}
avatarUrl={rank.user?.avatarUrl}
/>
</div>
<UserAvatar
alt={rank.user?.name}
avatarUrl={rank.user?.avatarUrl}
/>
</div>
<span>{rank.user?.name}</span>
</td>
Expand Down
128 changes: 128 additions & 0 deletions src/components/display/UserTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as UserServiceModel from "@/models/service/user";
import React from "react";
import { useTranslation } from "react-i18next";
import UserAvatar from "./UserAvatar";
import ConfirmDialog from "../control/ConfirmDialog";
import TrashIcon from "./icons/tabler/TrashIcon";
import { joinClasses } from "@/utils/common";
import UserCogIcon from "./icons/tabler/UserCogIcon";

export interface UserTableProps {
data: UserServiceModel.UserInfo[];
showActions?: boolean;
className?: string;
}

const UserTable: React.FC<UserTableProps> = (props) => {
const { t } = useTranslation();

return (
<>
<div className={props.className}>
<table className="table" aria-label="Problem Table">
<thead>
<tr className="border-base-content/10">
<th key="avatar">{t("Avatar")}</th>
<th key="name">{t("Name")}</th>
<th key="account">{t("Account")}</th>
<th key="roles">{t("Roles")}</th>
{props.showActions && <th key="actions">{t("Actions")}</th>}
</tr>
</thead>
<tbody>
{props.data.map((userInfo) => (
<tr key={userInfo.account} className="border-base-content/10">
<td className="flex items-center gap-3 py-2">
<div className="avatar">
<UserAvatar
alt={userInfo.name}
avatarUrl={userInfo.avatarUrl}
/>
</div>
</td>
<th>{userInfo.name}</th>
<td>{userInfo.account}</td>
<td>
<UserRoles roles={userInfo.roles ?? []} />
</td>
{props.showActions && (
<td className="p-2">
<UserActions userInfo={userInfo} onClickDelete={() => {}} />
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
<ConfirmDialog
id="user_delete_confirm_modal"
title="Confirm"
message="Are you sure to delete this user?"
onClickConfirm={() => {
window.location.reload();
}}
/>
</>
);
};

const UserRoles: React.FC<{ roles: string[] | undefined }> = (props) => {
if (props.roles === undefined) {
return <></>;
}

return (
<div className="space-x-2">
{props.roles.map((role) => (
<div
key={role}
className="badge border-0 bg-base-300 font-semibold text-base-content/80"
>
{role}
</div>
))}
</div>
);
};

interface ActionsProps {
userInfo: UserServiceModel.UserInfo;
onClickDelete: () => void;
}

const UserActions: React.FC<ActionsProps> = (props) => {
const onClickDelete = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();

props.onClickDelete();
const modal = document.getElementById(
"delete_confirm_modal",
) as HTMLDialogElement;
modal?.showModal();
};

return (
<div className="flex space-x-1">
<button className="btn btn-square btn-ghost btn-sm rounded">
<UserCogIcon
className={joinClasses(
"h-5 w-5",
props.userInfo.roles?.includes("admin")
? "text-success"
: "text-base-content",
)}
/>
</button>
<button
className="btn btn-square btn-ghost btn-sm rounded hover:bg-error/20"
onClick={onClickDelete}
>
<TrashIcon className="h-5 w-5 text-error" />
</button>
</div>
);
};

export default UserTable;
32 changes: 32 additions & 0 deletions src/components/display/icons/tabler/UserCogIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FC } from "react";
import { DefaultTablerIconStrokeWidth } from "@/utils/consts";

const UserCogIcon: FC<{
className?: string;
}> = (props) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={DefaultTablerIconStrokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
className={props.className}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
<path d="M6 21v-2a4 4 0 0 1 4 -4h2.5" />
<path d="M19.001 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M19.001 15.5v1.5" />
<path d="M19.001 21v1.5" />
<path d="M22.032 17.25l-1.299 .75" />
<path d="M17.27 20l-1.3 .75" />
<path d="M15.97 17.25l1.3 .75" />
<path d="M20.733 20l1.3 .75" />
</svg>
);
};

export default UserCogIcon;
26 changes: 26 additions & 0 deletions src/components/display/icons/tabler/UserShieldIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FC } from "react";
import { DefaultTablerIconStrokeWidth } from "@/utils/consts";

const UserShieldIcon: FC<{
className?: string;
}> = (props) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={DefaultTablerIconStrokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
className={props.className}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6 21v-2a4 4 0 0 1 4 -4h2" />
<path d="M22 16c0 4 -2.5 6 -3.5 6s-3.5 -2 -3.5 -6c1 0 2.5 -.5 3.5 -1.5c1 1 2.5 1.5 3.5 1.5z" />
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
</svg>
);
};

export default UserShieldIcon;
34 changes: 34 additions & 0 deletions src/hooks/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as UserServiceModel from "@/models/service/user";
import * as UserService from "@/apis/user";
import { useEffect, useState } from "react";

export const useUserInfoList = () => {
const [userInfoList, setUserInfoList] = useState<UserServiceModel.UserInfo[]>(
[],
);
const [total, setTotal] = useState<number>(0);
const [limit, setLimit] = useState<number>(10);
const [offset, setOffset] = useState<number>(0);

useEffect(() => {
UserService.getUserInfoList(limit, offset).then((res) => {
setUserInfoList(res.list);
setTotal(res.total);
});
}, [limit, offset]);

function getUserInfoList() {
return userInfoList;
}

function getPageCount(limit: number) {
return Math.ceil(total / limit);
}

function setPagenation(limit: number, offset: number) {
setLimit(limit);
setOffset(offset);
}

return { getUserInfoList, getPageCount, setPagenation };
};
14 changes: 14 additions & 0 deletions src/mocks/data/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as UserServiceModel from "@/models/service/user";

export const UserInfoList: UserServiceModel.UserInfo[] = [
{
name: "mock-user",
account: "mock-user",
roles: ["admin", "user"],
},
{
name: "mock-user-2",
account: "mock-user-2",
roles: ["user"],
},
];
4 changes: 2 additions & 2 deletions src/mocks/handlers/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export const getProblemInfo = http.get(
},
);

export const getProblemInfoList = http.get("/api/v1/problem", ({ request }) => {
const url = new URL(request.url);
export const getProblemInfoList = http.get("/api/v1/problem", (info) => {
const url = new URL(info.request.url);

const limit = url.searchParams.get("limit");
const offset = url.searchParams.get("offset");
Expand Down
20 changes: 20 additions & 0 deletions src/mocks/handlers/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HttpResponse, http } from "msw";
import * as UserMockData from "../data/user";

export const getCurrentUser = http.get("/api/v1/user/current", (info) => {
const authToken = info.cookies["auth-token"];
Expand Down Expand Up @@ -41,3 +42,22 @@ export const postSignOut = http.post("/api/v1/user/logout", async (info) => {
},
});
});

export const getUserInfoList = http.get("/api/v1/user", (info) => {
const url = new URL(info.request.url);
const limit = url.searchParams.get("limit");
const offset = url.searchParams.get("offset");

let userInfoList = UserMockData.UserInfoList.slice(
Number(offset),
Number(offset) + Number(limit),
);

return new Response(
JSON.stringify({
total: UserMockData.UserInfoList.length,
list: userInfoList,
}),
{ status: 200 },
);
});
2 changes: 2 additions & 0 deletions src/mocks/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import {
getCurrentUser,
postPasswordLogin,
postSignOut,
getUserInfoList,
} from "./handlers/user";
import { getRankList } from "./handlers/rank";

const restHandlers = [
getCurrentUser,
getUserInfoList,
postPasswordLogin,
postSignOut,
putProblem,
Expand Down
Loading

0 comments on commit 7082737

Please sign in to comment.