aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/app/views/admin/UserAdmin.tsx
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-11-22 09:53:31 +0800
committerGitHub <noreply@github.com>2020-11-22 09:53:31 +0800
commitb3c9cf4bf66bd3b78e94dc522c53e7f7522897f0 (patch)
tree996eed1f087a50e8d5cd865e1440f4dbb8a7873b /FrontEnd/src/app/views/admin/UserAdmin.tsx
parente0785b385138057a23ffd1703a7265c371aef45d (diff)
parentf1aabc06f1005b26bd1c0c5f36c98c28a62fc31e (diff)
downloadtimeline-b3c9cf4bf66bd3b78e94dc522c53e7f7522897f0.tar.gz
timeline-b3c9cf4bf66bd3b78e94dc522c53e7f7522897f0.tar.bz2
timeline-b3c9cf4bf66bd3b78e94dc522c53e7f7522897f0.zip
Merge pull request #189 from crupest/admin
Refactor front end to use the new permission system. Enhance admin page.
Diffstat (limited to 'FrontEnd/src/app/views/admin/UserAdmin.tsx')
-rw-r--r--FrontEnd/src/app/views/admin/UserAdmin.tsx611
1 files changed, 283 insertions, 328 deletions
diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx
index 0f5f8796..948cbb25 100644
--- a/FrontEnd/src/app/views/admin/UserAdmin.tsx
+++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx
@@ -1,173 +1,59 @@
import React, { useState, useEffect } from "react";
-import axios from "axios";
-import {
- ListGroup,
- Row,
- Col,
- Dropdown,
- Spinner,
- Button,
-} from "react-bootstrap";
-
-import OperationDialog from "../common/OperationDialog";
-import { User, UserWithToken } from "@/services/user";
-
-const apiBaseUrl = "/api";
-
-async function fetchUserList(_token: string): Promise<User[]> {
- const res = await axios.get<User[]>(`${apiBaseUrl}/users`);
- return res.data;
-}
-
-interface CreateUserInfo {
- username: string;
- password: string;
- administrator: boolean;
-}
+import clsx from "clsx";
+import { ListGroup, Row, Col, Spinner, Button } from "react-bootstrap";
+import InlineSVG from "react-inlinesvg";
+import PencilSquareIcon from "bootstrap-icons/icons/pencil-square.svg";
-async function createUser(user: CreateUserInfo, token: string): Promise<User> {
- const res = await axios.post<User>(
- `${apiBaseUrl}/userop/createuser?token=${token}`,
- user
- );
- return res.data;
-}
-
-function deleteUser(username: string, token: string): Promise<void> {
- return axios.delete(`${apiBaseUrl}/users/${username}?token=${token}`);
-}
-
-function changeUsername(
- oldUsername: string,
- newUsername: string,
- token: string
-): Promise<void> {
- return axios.patch(`${apiBaseUrl}/users/${oldUsername}?token=${token}`, {
- username: newUsername,
- });
-}
-
-function changePassword(
- username: string,
- newPassword: string,
- token: string
-): Promise<void> {
- return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, {
- password: newPassword,
- });
-}
-
-function changePermission(
- username: string,
- newPermission: boolean,
- token: string
-): Promise<void> {
- return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, {
- administrator: newPermission,
- });
-}
-
-const kChangeUsername = "changeusername";
-const kChangePassword = "changepassword";
-const kChangePermission = "changepermission";
-const kDelete = "delete";
-
-type TChangeUsername = typeof kChangeUsername;
-type TChangePassword = typeof kChangePassword;
-type TChangePermission = typeof kChangePermission;
-type TDelete = typeof kDelete;
-
-type ContextMenuItem =
- | TChangeUsername
- | TChangePassword
- | TChangePermission
- | TDelete;
-
-interface UserCardProps {
- onContextMenu: (item: ContextMenuItem) => void;
- user: User;
-}
+import OperationDialog, {
+ OperationBoolInputInfo,
+} from "../common/OperationDialog";
-const UserItem: React.FC<UserCardProps> = (props) => {
- const user = props.user;
-
- const createClickCallback = (item: ContextMenuItem): (() => void) => {
- return () => {
- props.onContextMenu(item);
- };
- };
-
- return (
- <ListGroup.Item className="container">
- <Row className="align-items-center">
- <Col>
- <p className="mb-0 text-primary">{user.username}</p>
- <small
- className={user.administrator ? "text-danger" : "text-secondary"}
- >
- {user.administrator ? "administrator" : "user"}
- </small>
- </Col>
- <Col className="col-auto">
- <Dropdown>
- <Dropdown.Toggle variant="warning" className="text-light">
- Manage
- </Dropdown.Toggle>
- <Dropdown.Menu>
- <Dropdown.Item onClick={createClickCallback(kChangeUsername)}>
- Change Username
- </Dropdown.Item>
- <Dropdown.Item onClick={createClickCallback(kChangePassword)}>
- Change Password
- </Dropdown.Item>
- <Dropdown.Item onClick={createClickCallback(kChangePermission)}>
- Change Permission
- </Dropdown.Item>
- <Dropdown.Item
- className="text-danger"
- onClick={createClickCallback(kDelete)}
- >
- Delete
- </Dropdown.Item>
- </Dropdown.Menu>
- </Dropdown>
- </Col>
- </Row>
- </ListGroup.Item>
- );
-};
-
-interface DialogProps {
+import { User, AuthUser } from "@/services/user";
+import {
+ getHttpUserClient,
+ HttpUser,
+ kUserPermissionList,
+ UserPermission,
+} from "@/http/user";
+import { Trans, useTranslation } from "react-i18next";
+
+interface DialogProps<TData = undefined, TReturn = undefined> {
open: boolean;
close: () => void;
+ token: string;
+ data: TData;
+ onSuccess: (data: TReturn) => void;
}
-interface CreateUserDialogProps extends DialogProps {
- process: (user: CreateUserInfo) => Promise<void>;
-}
-
-const CreateUserDialog: React.FC<CreateUserDialogProps> = (props) => {
+const CreateUserDialog: React.FC<DialogProps<undefined, HttpUser>> = ({
+ open,
+ close,
+ token,
+ onSuccess,
+}) => {
return (
<OperationDialog
- title="Create"
- titleColor="create"
- inputPrompt="You are creating a new user."
+ title="admin:user.dialog.create.title"
+ themeColor="success"
+ inputPrompt="admin:user.dialog.create.prompt"
inputScheme={
[
- { type: "text", label: "Username" },
- { type: "text", label: "Password" },
- { type: "bool", label: "Administrator" },
+ { type: "text", label: "admin:user.username" },
+ { type: "text", label: "admin:user.password" },
] as const
}
- onProcess={([username, password, administrator]) =>
- props.process({
- username: username,
- password: password,
- administrator: administrator,
- })
+ onProcess={([username, password]) =>
+ getHttpUserClient().createUser(
+ {
+ username,
+ password,
+ },
+ token
+ )
}
- close={props.close}
- open={props.open}
+ close={close}
+ open={open}
+ onSuccessAndClose={onSuccess}
/>
);
};
@@ -176,242 +62,301 @@ const UsernameLabel: React.FC = (props) => {
return <span style={{ color: "blue" }}>{props.children}</span>;
};
-interface UserDeleteDialogProps extends DialogProps {
- username: string;
- process: () => Promise<void>;
-}
-
-const UserDeleteDialog: React.FC<UserDeleteDialogProps> = (props) => {
+const UserDeleteDialog: React.FC<DialogProps<
+ { username: string },
+ unknown
+>> = ({ open, close, token, data: { username }, onSuccess }) => {
return (
<OperationDialog
- open={props.open}
- close={props.close}
- title="Dangerous"
- titleColor="dangerous"
+ open={open}
+ close={close}
+ title="admin:user.dialog.delete.title"
+ themeColor="danger"
inputPrompt={() => (
- <>
- {"You are deleting user "}
- <UsernameLabel>{props.username}</UsernameLabel>
- {" !"}
- </>
+ <Trans i18nKey="admin:user.dialog.delete.prompt">
+ 0<UsernameLabel>{username}</UsernameLabel>2
+ </Trans>
)}
- onProcess={props.process}
+ onProcess={() => getHttpUserClient().delete(username, token)}
+ onSuccessAndClose={onSuccess}
/>
);
};
-interface UserModifyDialogProps<T> extends DialogProps {
- username: string;
- process: (value: T) => Promise<void>;
-}
-
-const UserChangeUsernameDialog: React.FC<UserModifyDialogProps<string>> = (
- props
-) => {
+const UserModifyDialog: React.FC<DialogProps<
+ {
+ oldUser: HttpUser;
+ },
+ HttpUser
+>> = ({ open, close, token, data: { oldUser }, onSuccess }) => {
return (
<OperationDialog
- open={props.open}
- close={props.close}
- title="Caution"
- titleColor="dangerous"
+ open={open}
+ close={close}
+ title="admin:user.dialog.modify.title"
+ themeColor="danger"
inputPrompt={() => (
- <>
- {"You are change the username of user "}
- <UsernameLabel>{props.username}</UsernameLabel>
- {" !"}
- </>
+ <Trans i18nKey="admin:user.dialog.modify.prompt">
+ 0<UsernameLabel>{oldUser.username}</UsernameLabel>2
+ </Trans>
)}
- inputScheme={[{ type: "text", label: "New Username" }]}
- onProcess={([newUsername]) => {
- return props.process(newUsername);
- }}
+ inputScheme={
+ [
+ {
+ type: "text",
+ label: "admin:user.username",
+ initValue: oldUser.username,
+ },
+ { type: "text", label: "admin:user.password" },
+ {
+ type: "text",
+ label: "admin:user.nickname",
+ initValue: oldUser.nickname,
+ },
+ ] as const
+ }
+ onProcess={([username, password, nickname]) =>
+ getHttpUserClient().patch(
+ oldUser.username,
+ {
+ username: username !== oldUser.username ? username : undefined,
+ password: password !== "" ? password : undefined,
+ nickname: nickname !== oldUser.nickname ? nickname : undefined,
+ },
+ token
+ )
+ }
+ onSuccessAndClose={onSuccess}
/>
);
};
-const UserChangePasswordDialog: React.FC<UserModifyDialogProps<string>> = (
- props
-) => {
+const UserPermissionModifyDialog: React.FC<DialogProps<
+ {
+ username: string;
+ permissions: UserPermission[];
+ },
+ UserPermission[]
+>> = ({ open, close, token, data: { username, permissions }, onSuccess }) => {
+ const oldPermissionBoolList: boolean[] = kUserPermissionList.map(
+ (permission) => permissions.includes(permission)
+ );
+
return (
<OperationDialog
- open={props.open}
- close={props.close}
- title="Caution"
- titleColor="dangerous"
+ open={open}
+ close={close}
+ title="admin:user.dialog.modifyPermissions.title"
+ themeColor="danger"
inputPrompt={() => (
- <>
- {"You are change the password of user "}
- <UsernameLabel>{props.username}</UsernameLabel>
- {" !"}
- </>
+ <Trans i18nKey="admin:user.dialog.modifyPermissions.prompt">
+ 0<UsernameLabel>{username}</UsernameLabel>2
+ </Trans>
+ )}
+ inputScheme={kUserPermissionList.map<OperationBoolInputInfo>(
+ (permission, index) => ({
+ type: "bool",
+ label: permission,
+ initValue: oldPermissionBoolList[index],
+ })
)}
- inputScheme={[{ type: "text", label: "New Password" }]}
- onProcess={([newPassword]) => {
- return props.process(newPassword);
+ onProcess={async (newPermissionBoolList): Promise<boolean[]> => {
+ for (let index = 0; index < kUserPermissionList.length; index++) {
+ const oldValue = oldPermissionBoolList[index];
+ const newValue = newPermissionBoolList[index];
+ const permission = kUserPermissionList[index];
+ if (oldValue === newValue) continue;
+ if (newValue) {
+ await getHttpUserClient().putUserPermission(
+ username,
+ permission,
+ token
+ );
+ } else {
+ await getHttpUserClient().deleteUserPermission(
+ username,
+ permission,
+ token
+ );
+ }
+ }
+ return newPermissionBoolList;
+ }}
+ onSuccessAndClose={(newPermissionBoolList: boolean[]) => {
+ const permissions: UserPermission[] = [];
+ for (let index = 0; index < kUserPermissionList.length; index++) {
+ if (newPermissionBoolList[index]) {
+ permissions.push(kUserPermissionList[index]);
+ }
+ }
+ onSuccess(permissions);
}}
/>
);
};
-interface UserChangePermissionDialogProps extends DialogProps {
- username: string;
- newPermission: boolean;
- process: () => Promise<void>;
+const kModify = "modify";
+const kModifyPermission = "permission";
+const kDelete = "delete";
+
+type TModify = typeof kModify;
+type TModifyPermission = typeof kModifyPermission;
+type TDelete = typeof kDelete;
+
+type ContextMenuItem = TModify | TModifyPermission | TDelete;
+
+interface UserItemProps {
+ on: { [key in ContextMenuItem]: () => void };
+ user: User;
}
-const UserChangePermissionDialog: React.FC<UserChangePermissionDialogProps> = (
- props
-) => {
+const UserItem: React.FC<UserItemProps> = ({ user, on }) => {
+ const { t } = useTranslation();
+
+ const [editMaskVisible, setEditMaskVisible] = React.useState<boolean>(false);
+
return (
- <OperationDialog
- open={props.open}
- close={props.close}
- title="Caution"
- titleColor="dangerous"
- inputPrompt={() => (
- <>
- {"You are change user "}
- <UsernameLabel>{props.username}</UsernameLabel>
- {" to "}
- <span style={{ color: "orange" }}>
- {props.newPermission ? "administrator" : "normal user"}
- </span>
- {" !"}
- </>
- )}
- onProcess={props.process}
- />
+ <ListGroup.Item className="admin-user-item">
+ <InlineSVG
+ src={PencilSquareIcon}
+ className="float-right icon-button text-warning"
+ onClick={() => setEditMaskVisible(true)}
+ />
+ <h4 className="text-primary">{user.username}</h4>
+ <div className="text-secondary">
+ {t("admin:user.nickname")}
+ {user.nickname}
+ </div>
+ <div className="text-secondary">
+ {t("admin:user.uniqueId")}
+ {user.uniqueId}
+ </div>
+ <div className="text-secondary">
+ {t("admin:user.permissions")}
+ {user.permissions.map((permission) => {
+ return (
+ <span key={permission} className="text-danger">
+ {permission}{" "}
+ </span>
+ );
+ })}
+ </div>
+ <div
+ className={clsx("edit-mask", !editMaskVisible && "d-none")}
+ onClick={() => setEditMaskVisible(false)}
+ >
+ <button className="text-button primary" onClick={on[kModify]}>
+ {t("admin:user.modify")}
+ </button>
+ <button className="text-button primary" onClick={on[kModifyPermission]}>
+ {t("admin:user.modifyPermissions")}
+ </button>
+ <button className="text-button danger" onClick={on[kDelete]}>
+ {t("admin:user.delete")}
+ </button>
+ </div>
+ </ListGroup.Item>
);
};
interface UserAdminProps {
- user: UserWithToken;
+ user: AuthUser;
}
const UserAdmin: React.FC<UserAdminProps> = (props) => {
+ const { t } = useTranslation();
+
type DialogInfo =
| null
| {
type: "create";
}
- | { type: TDelete; username: string }
| {
- type: TChangeUsername;
- username: string;
+ type: TModify;
+ user: HttpUser;
}
| {
- type: TChangePassword;
+ type: TModifyPermission;
username: string;
+ permissions: UserPermission[];
}
- | {
- type: TChangePermission;
- username: string;
- newPermission: boolean;
- };
+ | { type: TDelete; username: string };
const [users, setUsers] = useState<User[] | null>(null);
const [dialog, setDialog] = useState<DialogInfo>(null);
+ const [usersVersion, setUsersVersion] = useState<number>(0);
+ const updateUsers = (): void => {
+ setUsersVersion(usersVersion + 1);
+ };
const token = props.user.token;
useEffect(() => {
let subscribe = true;
- void fetchUserList(props.user.token).then((us) => {
- if (subscribe) {
- setUsers(us);
- }
- });
+ void getHttpUserClient()
+ .list()
+ .then((us) => {
+ if (subscribe) {
+ setUsers(us);
+ }
+ });
return () => {
subscribe = false;
};
- }, [props.user]);
+ }, [usersVersion]);
let dialogNode: React.ReactNode;
- if (dialog)
+ if (dialog) {
switch (dialog.type) {
case "create":
dialogNode = (
<CreateUserDialog
open
close={() => setDialog(null)}
- process={async (user) => {
- const u = await createUser(user, token);
- setUsers((oldUsers) => [...(oldUsers ?? []), u]);
- }}
+ token={token}
+ data={undefined}
+ onSuccess={updateUsers}
/>
);
break;
- case "delete":
+ case kDelete:
dialogNode = (
<UserDeleteDialog
open
close={() => setDialog(null)}
- username={dialog.username}
- process={async () => {
- await deleteUser(dialog.username, token);
- setUsers((oldUsers) =>
- (oldUsers ?? []).filter((u) => u.username !== dialog.username)
- );
- }}
- />
- );
- break;
- case kChangeUsername:
- dialogNode = (
- <UserChangeUsernameDialog
- open
- close={() => setDialog(null)}
- username={dialog.username}
- process={async (newUsername) => {
- await changeUsername(dialog.username, newUsername, token);
- setUsers((oldUsers) => {
- const users = (oldUsers ?? []).slice();
- const findedUser = users.find(
- (u) => u.username === dialog.username
- );
- if (findedUser) findedUser.username = newUsername;
- return users;
- });
- }}
+ token={token}
+ data={{ username: dialog.username }}
+ onSuccess={updateUsers}
/>
);
break;
- case kChangePassword:
+ case kModify:
dialogNode = (
- <UserChangePasswordDialog
+ <UserModifyDialog
open
close={() => setDialog(null)}
- username={dialog.username}
- process={async (newPassword) => {
- await changePassword(dialog.username, newPassword, token);
- }}
+ token={token}
+ data={{ oldUser: dialog.user }}
+ onSuccess={updateUsers}
/>
);
break;
- case kChangePermission: {
- const newPermission = dialog.newPermission;
+ case kModifyPermission:
dialogNode = (
- <UserChangePermissionDialog
+ <UserPermissionModifyDialog
open
close={() => setDialog(null)}
- username={dialog.username}
- newPermission={newPermission}
- process={async () => {
- await changePermission(dialog.username, newPermission, token);
- setUsers((oldUsers) => {
- const users = (oldUsers ?? []).slice();
- const findedUser = users.find(
- (u) => u.username === dialog.username
- );
- if (findedUser) findedUser.administrator = newPermission;
- return users;
- });
+ token={token}
+ data={{
+ username: dialog.username,
+ permissions: dialog.permissions,
}}
+ onSuccess={updateUsers}
/>
);
break;
- }
}
+ }
if (users) {
const userComponents = users.map((user) => {
@@ -419,19 +364,26 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
<UserItem
key={user.username}
user={user}
- onContextMenu={(item) => {
- setDialog(
- item === kChangePermission
- ? {
- type: kChangePermission,
- username: user.username,
- newPermission: !user.administrator,
- }
- : {
- type: item,
- username: user.username,
- }
- );
+ on={{
+ modify: () => {
+ setDialog({
+ type: "modify",
+ user,
+ });
+ },
+ permission: () => {
+ setDialog({
+ type: kModifyPermission,
+ username: user.username,
+ permissions: user.permissions,
+ });
+ },
+ delete: () => {
+ setDialog({
+ type: "delete",
+ username: user.username,
+ });
+ },
}}
/>
);
@@ -439,17 +391,20 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
return (
<>
- <Button
- variant="success"
- onClick={() =>
- setDialog({
- type: "create",
- })
- }
- className="align-self-end"
- >
- Create User
- </Button>
+ <Row className="justify-content-end my-2">
+ <Col xs="auto">
+ <Button
+ variant="outline-success"
+ onClick={() =>
+ setDialog({
+ type: "create",
+ })
+ }
+ >
+ {t("admin:create")}
+ </Button>
+ </Col>
+ </Row>
{userComponents}
{dialogNode}
</>