From 47587812b809fee2a95c76266d9d0e42fc4ac1ca Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 14:14:28 +0800 Subject: ... --- FrontEnd/src/views/admin/Admin.tsx | 48 ++++ FrontEnd/src/views/admin/AdminNav.tsx | 44 ++++ FrontEnd/src/views/admin/MoreAdmin.tsx | 13 ++ FrontEnd/src/views/admin/UserAdmin.tsx | 396 +++++++++++++++++++++++++++++++++ FrontEnd/src/views/admin/admin.sass | 22 ++ 5 files changed, 523 insertions(+) create mode 100644 FrontEnd/src/views/admin/Admin.tsx create mode 100644 FrontEnd/src/views/admin/AdminNav.tsx create mode 100644 FrontEnd/src/views/admin/MoreAdmin.tsx create mode 100644 FrontEnd/src/views/admin/UserAdmin.tsx create mode 100644 FrontEnd/src/views/admin/admin.sass (limited to 'FrontEnd/src/views/admin') diff --git a/FrontEnd/src/views/admin/Admin.tsx b/FrontEnd/src/views/admin/Admin.tsx new file mode 100644 index 00000000..0b6d1f05 --- /dev/null +++ b/FrontEnd/src/views/admin/Admin.tsx @@ -0,0 +1,48 @@ +import React, { Fragment } from "react"; +import { Redirect, Route, Switch, useRouteMatch, match } from "react-router"; +import { Container } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +import { AuthUser } from "@/services/user"; + +import AdminNav from "./AdminNav"; +import UserAdmin from "./UserAdmin"; +import MoreAdmin from "./MoreAdmin"; + +interface AdminProps { + user: AuthUser; +} + +const Admin: React.FC = ({ user }) => { + useTranslation("admin"); + + const match = useRouteMatch(); + + return ( + + + + + {(p) => { + const match = p.match as match<{ name: string }>; + const name = match.params["name"]; + return ( + + + {(() => { + if (name === "users") { + return ; + } else if (name === "more") { + return ; + } + })()} + + ); + }} + + + + ); +}; + +export default Admin; diff --git a/FrontEnd/src/views/admin/AdminNav.tsx b/FrontEnd/src/views/admin/AdminNav.tsx new file mode 100644 index 00000000..47e2138f --- /dev/null +++ b/FrontEnd/src/views/admin/AdminNav.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { Nav } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; +import { useHistory, useRouteMatch } from "react-router"; + +const AdminNav: React.FC = () => { + const match = useRouteMatch<{ name: string }>(); + const history = useHistory(); + + const { t } = useTranslation(); + + const name = match.params.name; + + function toggle(newTab: string): void { + history.push(`/admin/${newTab}`); + } + + return ( + + ); +}; + +export default AdminNav; diff --git a/FrontEnd/src/views/admin/MoreAdmin.tsx b/FrontEnd/src/views/admin/MoreAdmin.tsx new file mode 100644 index 00000000..042789a0 --- /dev/null +++ b/FrontEnd/src/views/admin/MoreAdmin.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +import { AuthUser } from "@/services/user"; + +export interface MoreAdminProps { + user: AuthUser; +} + +const MoreAdmin: React.FC = () => { + return <>More...; +}; + +export default MoreAdmin; diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx new file mode 100644 index 00000000..4e9cd600 --- /dev/null +++ b/FrontEnd/src/views/admin/UserAdmin.tsx @@ -0,0 +1,396 @@ +import React, { useState, useEffect } from "react"; +import classnames from "classnames"; +import { ListGroup, Row, Col, Spinner, Button } from "react-bootstrap"; + +import OperationDialog, { + OperationDialogBoolInput, +} from "../common/OperationDialog"; + +import { AuthUser } from "@/services/user"; +import { + getHttpUserClient, + HttpUser, + kUserPermissionList, + UserPermission, +} from "http/user"; +import { Trans, useTranslation } from "react-i18next"; + +interface DialogProps { + open: boolean; + close: () => void; + data: TData; + onSuccess: (data: TReturn) => void; +} + +const CreateUserDialog: React.FC> = ({ + open, + close, + onSuccess, +}) => { + return ( + + getHttpUserClient().post({ + username, + password, + }) + } + close={close} + open={open} + onSuccessAndClose={onSuccess} + /> + ); +}; + +const UsernameLabel: React.FC = (props) => { + return {props.children}; +}; + +const UserDeleteDialog: React.FC> = + ({ open, close, data: { username }, onSuccess }) => { + return ( + ( + + 0{username}2 + + )} + onProcess={() => getHttpUserClient().delete(username)} + onSuccessAndClose={onSuccess} + /> + ); + }; + +const UserModifyDialog: React.FC< + DialogProps< + { + oldUser: HttpUser; + }, + HttpUser + > +> = ({ open, close, data: { oldUser }, onSuccess }) => { + return ( + ( + + 0{oldUser.username}2 + + )} + 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, + }) + } + onSuccessAndClose={onSuccess} + /> + ); +}; + +const UserPermissionModifyDialog: React.FC< + DialogProps< + { + username: string; + permissions: UserPermission[]; + }, + UserPermission[] + > +> = ({ open, close, data: { username, permissions }, onSuccess }) => { + const oldPermissionBoolList: boolean[] = kUserPermissionList.map( + (permission) => permissions.includes(permission) + ); + + return ( + ( + + 0{username}2 + + )} + inputScheme={kUserPermissionList.map( + (permission, index) => ({ + type: "bool", + label: permission, + initValue: oldPermissionBoolList[index], + }) + )} + onProcess={async (newPermissionBoolList): Promise => { + 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); + } else { + await getHttpUserClient().deleteUserPermission( + username, + permission + ); + } + } + 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); + }} + /> + ); +}; + +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: HttpUser; +} + +const UserItem: React.FC = ({ user, on }) => { + const { t } = useTranslation(); + + const [editMaskVisible, setEditMaskVisible] = React.useState(false); + + return ( + + setEditMaskVisible(true)} + /> +

{user.username}

+
+ {t("admin:user.nickname")} + {user.nickname} +
+
+ {t("admin:user.uniqueId")} + {user.uniqueId} +
+
+ {t("admin:user.permissions")} + {user.permissions.map((permission) => { + return ( + + {permission}{" "} + + ); + })} +
+
setEditMaskVisible(false)} + > + + + +
+
+ ); +}; + +interface UserAdminProps { + user: AuthUser; +} + +const UserAdmin: React.FC = () => { + const { t } = useTranslation(); + + type DialogInfo = + | null + | { + type: "create"; + } + | { + type: TModify; + user: HttpUser; + } + | { + type: TModifyPermission; + username: string; + permissions: UserPermission[]; + } + | { type: TDelete; username: string }; + + const [users, setUsers] = useState(null); + const [dialog, setDialog] = useState(null); + const [usersVersion, setUsersVersion] = useState(0); + const updateUsers = (): void => { + setUsersVersion(usersVersion + 1); + }; + + useEffect(() => { + let subscribe = true; + void getHttpUserClient() + .list() + .then((us) => { + if (subscribe) { + setUsers(us); + } + }); + return () => { + subscribe = false; + }; + }, [usersVersion]); + + let dialogNode: React.ReactNode; + if (dialog) { + switch (dialog.type) { + case "create": + dialogNode = ( + setDialog(null)} + data={undefined} + onSuccess={updateUsers} + /> + ); + break; + case kDelete: + dialogNode = ( + setDialog(null)} + data={{ username: dialog.username }} + onSuccess={updateUsers} + /> + ); + break; + case kModify: + dialogNode = ( + setDialog(null)} + data={{ oldUser: dialog.user }} + onSuccess={updateUsers} + /> + ); + break; + case kModifyPermission: + dialogNode = ( + setDialog(null)} + data={{ + username: dialog.username, + permissions: dialog.permissions, + }} + onSuccess={updateUsers} + /> + ); + break; + } + } + + if (users) { + const userComponents = users.map((user) => { + return ( + { + setDialog({ + type: "modify", + user, + }); + }, + permission: () => { + setDialog({ + type: kModifyPermission, + username: user.username, + permissions: user.permissions, + }); + }, + delete: () => { + setDialog({ + type: "delete", + username: user.username, + }); + }, + }} + /> + ); + }); + + return ( + <> + + + + + + {userComponents} + {dialogNode} + + ); + } else { + return ; + } +}; + +export default UserAdmin; diff --git a/FrontEnd/src/views/admin/admin.sass b/FrontEnd/src/views/admin/admin.sass new file mode 100644 index 00000000..1ce010f8 --- /dev/null +++ b/FrontEnd/src/views/admin/admin.sass @@ -0,0 +1,22 @@ +.admin-user-item + position: relative + + .edit-mask + position: absolute + top: 0 + left: 0 + bottom: 0 + right: 0 + + background: #ffffffc5 + position: absolute + + display: flex + justify-content: center + align-items: center + + @include media-breakpoint-down(xs) + flex-direction: column + + button + margin: 0.5em 2em -- cgit v1.2.3