diff options
Diffstat (limited to 'FrontEnd/src/migrating/admin')
-rw-r--r-- | FrontEnd/src/migrating/admin/Admin.tsx | 27 | ||||
-rw-r--r-- | FrontEnd/src/migrating/admin/AdminNav.tsx | 29 | ||||
-rw-r--r-- | FrontEnd/src/migrating/admin/MoreAdmin.tsx | 7 | ||||
-rw-r--r-- | FrontEnd/src/migrating/admin/UserAdmin.tsx | 301 | ||||
-rw-r--r-- | FrontEnd/src/migrating/admin/index.css | 33 | ||||
-rw-r--r-- | FrontEnd/src/migrating/admin/index.tsx | 7 |
6 files changed, 404 insertions, 0 deletions
diff --git a/FrontEnd/src/migrating/admin/Admin.tsx b/FrontEnd/src/migrating/admin/Admin.tsx new file mode 100644 index 00000000..986c36b4 --- /dev/null +++ b/FrontEnd/src/migrating/admin/Admin.tsx @@ -0,0 +1,27 @@ +import { Route, Routes } from "react-router-dom"; +import { useTranslation } from "react-i18next"; + +import AdminNav from "./AdminNav"; +import UserAdmin from "./UserAdmin"; +import MoreAdmin from "./MoreAdmin"; + +import "./index.css"; + +const Admin: React.FC = () => { + useTranslation("admin"); + + return ( + <> + <div className="container"> + <AdminNav className="mt-2" /> + <Routes> + <Route index element={<UserAdmin />} /> + <Route path="user" element={<UserAdmin />} /> + <Route path="more" element={<MoreAdmin />} /> + </Routes> + </div> + </> + ); +}; + +export default Admin; diff --git a/FrontEnd/src/migrating/admin/AdminNav.tsx b/FrontEnd/src/migrating/admin/AdminNav.tsx new file mode 100644 index 00000000..b7385e5c --- /dev/null +++ b/FrontEnd/src/migrating/admin/AdminNav.tsx @@ -0,0 +1,29 @@ +import { useLocation } from "react-router-dom"; + +import Tabs from "../common/tab/Tabs"; + +export function AdminNav({ className }: { className?: string }) { + const location = useLocation(); + const name = location.pathname.split("/")[2] ?? "user"; + + return ( + <Tabs + className={className} + activeTabName={name} + tabs={[ + { + name: "user", + text: "admin:nav.users", + link: "/admin/user", + }, + { + name: "more", + text: "admin:nav.more", + link: "/admin/more", + }, + ]} + /> + ); +} + +export default AdminNav; diff --git a/FrontEnd/src/migrating/admin/MoreAdmin.tsx b/FrontEnd/src/migrating/admin/MoreAdmin.tsx new file mode 100644 index 00000000..d49d211f --- /dev/null +++ b/FrontEnd/src/migrating/admin/MoreAdmin.tsx @@ -0,0 +1,7 @@ +import * as React from "react"; + +const MoreAdmin: React.FC = () => { + return <>More...</>; +}; + +export default MoreAdmin; diff --git a/FrontEnd/src/migrating/admin/UserAdmin.tsx b/FrontEnd/src/migrating/admin/UserAdmin.tsx new file mode 100644 index 00000000..08560c87 --- /dev/null +++ b/FrontEnd/src/migrating/admin/UserAdmin.tsx @@ -0,0 +1,301 @@ +// eslint-disable +// @ts-nocheck + +import { useState, useEffect } from "react"; +import * as React from "react"; +import { Trans, useTranslation } from "react-i18next"; +import classnames from "classnames"; + +import { getHttpUserClient, HttpUser, kUserPermissionList } from "@/http/user"; + +import OperationDialog from "../common/dialog/OperationDialog"; +import Button from "../common/button/Button"; +import Spinner from "../common/Spinner"; +import FlatButton from "../common/button/FlatButton"; +import IconButton from "../common/button/IconButton"; + +const CreateUserDialog: React.FC<{ + open: boolean; + close: () => void; + onSuccess: (user: HttpUser) => void; +}> = ({ open, close, onSuccess }) => { + return ( + <OperationDialog + title="admin:user.dialog.create.title" + inputPrompt="admin:user.dialog.create.prompt" + inputs={[ + { key: "username", type: "text", label: "admin:user.username" }, + { key: "password", type: "text", label: "admin:user.password" }, + ]} + onProcess={({ username, password }) => + getHttpUserClient().post({ + username: username as string, + password: password as string, + }) + } + onClose={close} + open={open} + onSuccessAndClose={onSuccess} + /> + ); +}; + +const UsernameLabel: React.FC<{ children: React.ReactNode }> = (props) => { + return <span style={{ color: "blue" }}>{props.children}</span>; +}; + +const UserDeleteDialog: React.FC<{ + open: boolean; + close: () => void; + user: HttpUser; + onSuccess: () => void; +}> = ({ open, close, user, onSuccess }) => { + return ( + <OperationDialog + open={open} + onClose={close} + title="admin:user.dialog.delete.title" + themeColor="danger" + inputPrompt={() => ( + <Trans i18nKey="user.dialog.delete.prompt" ns="admin"> + 0<UsernameLabel>{user.username}</UsernameLabel>2 + </Trans> + )} + onProcess={() => getHttpUserClient().delete(user.username)} + onSuccessAndClose={onSuccess} + /> + ); +}; + +const UserModifyDialog: React.FC<{ + open: boolean; + close: () => void; + user: HttpUser; + onSuccess: () => void; +}> = ({ open, close, user, onSuccess }) => { + return ( + <OperationDialog + open={open} + onClose={close} + title="admin:user.dialog.modify.title" + inputPromptNode={ + <Trans i18nKey="admin:user.dialog.modify.prompt"> + 0<UsernameLabel>{user.username}</UsernameLabel>2 + </Trans> + } + inputs={ + [ + { + type: "text", + label: "admin:user.username", + initValue: user.username, + }, + { type: "text", label: "admin:user.password" }, + { + type: "text", + label: "admin:user.nickname", + initValue: user.nickname, + }, + ] as const + } + onProcess={([username, password, nickname]) => + getHttpUserClient().patch(user.username, { + username: username !== user.username ? username : undefined, + password: password !== "" ? password : undefined, + nickname: nickname !== user.nickname ? nickname : undefined, + }) + } + onSuccessAndClose={onSuccess} + /> + ); +}; + +const UserPermissionModifyDialog: React.FC<{ + open: boolean; + close: () => void; + user: HttpUser; + onSuccess: () => void; +}> = ({ open, close, user, onSuccess }) => { + const oldPermissionBoolList: boolean[] = kUserPermissionList.map( + (permission) => user.permissions.includes(permission), + ); + + return ( + <OperationDialog + open={open} + onClose={close} + title="admin:user.dialog.modifyPermissions.title" + themeColor="danger" + inputPrompt={() => ( + <Trans i18nKey="admin:user.dialog.modifyPermissions.prompt"> + 0<UsernameLabel>{user.username}</UsernameLabel>2 + </Trans> + )} + inputScheme={kUserPermissionList.map<OperationDialogBoolInput>( + (permission, index) => ({ + type: "bool", + label: { type: "custom", value: permission }, + initValue: oldPermissionBoolList[index], + }), + )} + 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( + user.username, + permission, + ); + } else { + await getHttpUserClient().deleteUserPermission( + user.username, + permission, + ); + } + } + return newPermissionBoolList; + }} + onSuccessAndClose={onSuccess} + /> + ); +}; + +interface UserItemProps { + user: HttpUser; + onChange: () => void; +} + +const UserItem: React.FC<UserItemProps> = ({ user, onChange }) => { + const { t } = useTranslation(); + + const [dialog, setDialog] = useState< + "delete" | "modify" | "permission" | null + >(null); + + const [editMaskVisible, setEditMaskVisible] = React.useState<boolean>(false); + + return ( + <> + <div className="admin-user-item"> + <IconButton + icon="pencil-square" + className="cru-float-right" + onClick={() => setEditMaskVisible(true)} + /> + <h5 className="cru-color-primary">{user.username}</h5> + <small className="d-block cru-color-secondary"> + {t("admin:user.nickname")} + {user.nickname} + </small> + <small className="d-block cru-color-secondary"> + {t("admin:user.uniqueId")} + {user.uniqueId} + </small> + <small className="d-block cru-color-secondary"> + {t("admin:user.permissions")} + {user.permissions.map((permission) => { + return ( + <span key={permission} className="cru-color-danger"> + {permission} + </span> + ); + })} + </small> + <div + className={classnames("edit-mask", !editMaskVisible && "d-none")} + onClick={() => setEditMaskVisible(false)} + > + <FlatButton + text="admin:user.modify" + onClick={() => setDialog("modify")} + /> + <FlatButton + text="admin:user.modifyPermissions" + onClick={() => setDialog("permission")} + /> + <FlatButton + text="admin:user.delete" + color="danger" + onClick={() => setDialog("delete")} + /> + </div> + </div> + <UserDeleteDialog + open={dialog === "delete"} + close={() => setDialog(null)} + user={user} + onSuccess={onChange} + /> + <UserModifyDialog + open={dialog === "modify"} + close={() => setDialog(null)} + user={user} + onSuccess={onChange} + /> + <UserPermissionModifyDialog + open={dialog === "permission"} + close={() => setDialog(null)} + user={user} + onSuccess={onChange} + /> + </> + ); +}; + +const UserAdmin: React.FC = () => { + const [users, setUsers] = useState<HttpUser[] | null>(null); + const [dialog, setDialog] = useState<"create" | null>(null); + const [usersVersion, setUsersVersion] = useState<number>(0); + const updateUsers = (): void => { + setUsersVersion(usersVersion + 1); + }; + + useEffect(() => { + let subscribe = true; + void getHttpUserClient() + .list() + .then((us) => { + if (subscribe) { + setUsers(us.items); + } + }); + return () => { + subscribe = false; + }; + }, [usersVersion]); + + if (users) { + const userComponents = users.map((user) => { + return ( + <UserItem key={user.username} user={user} onChange={updateUsers} /> + ); + }); + + return ( + <> + <div className="row justify-content-end my-2"> + <div className="col col-auto"> + <Button + text="admin:create" + color="success" + onClick={() => setDialog("create")} + /> + </div> + </div> + {userComponents} + <CreateUserDialog + open={dialog === "create"} + close={() => setDialog(null)} + onSuccess={updateUsers} + /> + </> + ); + } else { + return <Spinner />; + } +}; + +export default UserAdmin; diff --git a/FrontEnd/src/migrating/admin/index.css b/FrontEnd/src/migrating/admin/index.css new file mode 100644 index 00000000..17e24586 --- /dev/null +++ b/FrontEnd/src/migrating/admin/index.css @@ -0,0 +1,33 @@ +.admin-user-item {
+ position: relative;
+ border: var(--cru-primary-color) solid;
+ border-width: 1px 1px 0;
+ padding: 1em;
+}
+
+.admin-user-item:last-of-type {
+ border-bottom-width: 1px;
+}
+
+.admin-user-item .edit-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(255, 255, 255, 0.9);
+ position: absolute;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+}
+
+@media (max-width: 576px) {
+ .admin-user-item .edit-mask {
+ flex-direction: column;
+ }
+}
+
+.admin-user-item .edit-mask button {
+ margin: 0.5em 2em;
+}
diff --git a/FrontEnd/src/migrating/admin/index.tsx b/FrontEnd/src/migrating/admin/index.tsx new file mode 100644 index 00000000..0467711d --- /dev/null +++ b/FrontEnd/src/migrating/admin/index.tsx @@ -0,0 +1,7 @@ +import { lazy } from "react"; + +const Admin = lazy( + () => import(/* webpackChunkName: "admin" */ "./Admin") +); + +export default Admin; |