aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-11-17 17:31:42 +0800
committercrupest <crupest@outlook.com>2020-11-17 17:31:42 +0800
commite51e3b7c3c987f52823798b749e6c6deb2bfbe38 (patch)
tree3ff6f99f7b87a79e3279b3bd7de1431894f87066
parent711a148fbbf4361f9c0632eff973c2f240a67c5d (diff)
downloadtimeline-e51e3b7c3c987f52823798b749e6c6deb2bfbe38.tar.gz
timeline-e51e3b7c3c987f52823798b749e6c6deb2bfbe38.tar.bz2
timeline-e51e3b7c3c987f52823798b749e6c6deb2bfbe38.zip
...
-rw-r--r--FrontEnd/src/app/http/user.ts25
-rw-r--r--FrontEnd/src/app/views/admin/AdminSubPage.tsx18
-rw-r--r--FrontEnd/src/app/views/admin/UserAdmin.tsx352
-rw-r--r--FrontEnd/src/app/views/common/OperationDialog.tsx31
4 files changed, 163 insertions, 263 deletions
diff --git a/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts
index 92a6433e..243846d1 100644
--- a/FrontEnd/src/app/http/user.ts
+++ b/FrontEnd/src/app/http/user.ts
@@ -28,6 +28,8 @@ export interface HttpUser {
}
export interface HttpUserPatchRequest {
+ username?: string;
+ password?: string;
nickname?: string;
}
@@ -36,6 +38,11 @@ export interface HttpChangePasswordRequest {
newPassword: string;
}
+export interface HttpCreateUserRequest {
+ username: string;
+ password: string;
+}
+
export class HttpUserNotExistError extends Error {
constructor(public innerError?: AxiosError) {
super();
@@ -56,6 +63,7 @@ export interface IHttpUserClient {
req: HttpUserPatchRequest,
token: string
): Promise<HttpUser>;
+ delete(username: string, token: string): Promise<void>;
getAvatar(username: string): Promise<BlobWithEtag>;
getAvatar(
username: string,
@@ -73,6 +81,8 @@ export interface IHttpUserClient {
permission: UserPermission,
token: string
): Promise<void>;
+
+ createUser(req: HttpCreateUserRequest, token: string): Promise<HttpUser>;
}
export class HttpUserClient implements IHttpUserClient {
@@ -102,6 +112,13 @@ export class HttpUserClient implements IHttpUserClient {
.catch(convertToNetworkError);
}
+ delete(username: string, token: string): Promise<void> {
+ return axios
+ .delete(`${apiBaseUrl}/users/${username}?token=${token}`)
+ .catch(convertToNetworkError)
+ .then();
+ }
+
getAvatar(username: string): Promise<BlobWithEtag>;
getAvatar(
username: string,
@@ -171,6 +188,14 @@ export class HttpUserClient implements IHttpUserClient {
.catch(convertToNetworkError)
.then();
}
+
+ createUser(req: HttpCreateUserRequest, token: string): Promise<HttpUser> {
+ return axios
+ .post<HttpUser>(`${apiBaseUrl}/userop/createuser?token=${token}`, req)
+ .then(extractResponseData)
+ .catch(convertToNetworkError)
+ .then();
+ }
}
let client: IHttpUserClient = new HttpUserClient();
diff --git a/FrontEnd/src/app/views/admin/AdminSubPage.tsx b/FrontEnd/src/app/views/admin/AdminSubPage.tsx
index 5d2df13c..5efe1ee3 100644
--- a/FrontEnd/src/app/views/admin/AdminSubPage.tsx
+++ b/FrontEnd/src/app/views/admin/AdminSubPage.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { Nav } from "react-bootstrap";
+import { Container, Nav } from "react-bootstrap";
import { useHistory, useRouteMatch } from "react-router";
const AdminSubPage: React.FC = ({ children }) => {
@@ -13,8 +13,8 @@ const AdminSubPage: React.FC = ({ children }) => {
}
return (
- <>
- <Nav variant="tabs">
+ <Container>
+ <Nav variant="tabs" className="my-2">
<Nav.Item>
<Nav.Link
active={name === "users"}
@@ -25,19 +25,9 @@ const AdminSubPage: React.FC = ({ children }) => {
Users
</Nav.Link>
</Nav.Item>
- <Nav.Item>
- <Nav.Link
- active={name === "more"}
- onClick={() => {
- toggle("more");
- }}
- >
- More
- </Nav.Link>
- </Nav.Item>
</Nav>
{children}
- </>
+ </Container>
);
};
diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx
index 856e6136..3432cddf 100644
--- a/FrontEnd/src/app/views/admin/UserAdmin.tsx
+++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx
@@ -1,5 +1,4 @@
import React, { useState, useEffect } from "react";
-import axios from "axios";
import {
ListGroup,
Row,
@@ -13,77 +12,22 @@ import OperationDialog from "../common/OperationDialog";
import AdminSubPage from "./AdminSubPage";
import { User, AuthUser } from "@/services/user";
-import { getHttpUserClient } from "@/http/user";
+import { getHttpUserClient, HttpUser } from "@/http/user";
-const apiBaseUrl = "/api";
-
-interface CreateUserInfo {
- username: string;
- password: string;
-}
-
-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,
- });
-}
-
-const kChangeUsername = "changeusername";
-const kChangePassword = "changepassword";
-const kChangePermission = "changepermission";
+const kModify = "modify";
const kDelete = "delete";
-type TChangeUsername = typeof kChangeUsername;
-type TChangePassword = typeof kChangePassword;
-type TChangePermission = typeof kChangePermission;
+type TModify = typeof kModify;
type TDelete = typeof kDelete;
-type ContextMenuItem =
- | TChangeUsername
- | TChangePassword
- | TChangePermission
- | TDelete;
+type ContextMenuItem = TModify | TDelete;
interface UserCardProps {
- onContextMenu: (item: ContextMenuItem) => void;
+ on: { [key in ContextMenuItem]: () => void };
user: User;
}
-const UserItem: React.FC<UserCardProps> = (props) => {
- const user = props.user;
-
- const createClickCallback = (item: ContextMenuItem): (() => void) => {
- return () => {
- props.onContextMenu(item);
- };
- };
-
+const UserItem: React.FC<UserCardProps> = ({ user, on }) => {
return (
<ListGroup.Item className="container">
<Row className="align-items-center">
@@ -101,19 +45,8 @@ const UserItem: React.FC<UserCardProps> = (props) => {
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)}
- >
+ <Dropdown.Item onClick={on["modify"]}>Modify</Dropdown.Item>
+ <Dropdown.Item className="text-danger" onClick={on["delete"]}>
Delete
</Dropdown.Item>
</Dropdown.Menu>
@@ -124,16 +57,20 @@ const UserItem: React.FC<UserCardProps> = (props) => {
);
};
-interface DialogProps {
+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"
@@ -146,13 +83,17 @@ const CreateUserDialog: React.FC<CreateUserDialogProps> = (props) => {
] as const
}
onProcess={([username, password]) =>
- props.process({
- username: username,
- password: password,
- })
+ getHttpUserClient().createUser(
+ {
+ username,
+ password,
+ },
+ token
+ )
}
- close={props.close}
- open={props.open}
+ close={close}
+ open={open}
+ onSuccessAndClose={onSuccess}
/>
);
};
@@ -161,110 +102,64 @@ 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}
+ open={open}
+ close={close}
title="Dangerous"
titleColor="dangerous"
inputPrompt={() => (
<>
- {"You are deleting user "}
- <UsernameLabel>{props.username}</UsernameLabel>
- {" !"}
+ You are deleting user <UsernameLabel>{username}</UsernameLabel> !
</>
)}
- 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}
+ open={open}
+ close={close}
title="Caution"
titleColor="dangerous"
inputPrompt={() => (
<>
- {"You are change the username of user "}
- <UsernameLabel>{props.username}</UsernameLabel>
- {" !"}
+ You are change the password of user
+ <UsernameLabel>{oldUser.username}</UsernameLabel> !
</>
)}
- inputScheme={[{ type: "text", label: "New Username" }]}
- onProcess={([newUsername]) => {
- return props.process(newUsername);
- }}
- />
- );
-};
-
-const UserChangePasswordDialog: React.FC<UserModifyDialogProps<string>> = (
- props
-) => {
- return (
- <OperationDialog
- open={props.open}
- close={props.close}
- title="Caution"
- titleColor="dangerous"
- inputPrompt={() => (
- <>
- {"You are change the password of user "}
- <UsernameLabel>{props.username}</UsernameLabel>
- {" !"}
- </>
- )}
- inputScheme={[{ type: "text", label: "New Password" }]}
- onProcess={([newPassword]) => {
- return props.process(newPassword);
- }}
- />
- );
-};
-
-interface UserChangePermissionDialogProps extends DialogProps {
- username: string;
- newPermission: boolean;
- process: () => Promise<void>;
-}
-
-const UserChangePermissionDialog: React.FC<UserChangePermissionDialogProps> = (
- props
-) => {
- 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}
+ inputScheme={
+ [
+ { type: "text", label: "New Username", initValue: oldUser.username },
+ { type: "text", label: "New Password" },
+ { type: "text", label: "New 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}
/>
);
};
@@ -279,23 +174,18 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
| {
type: "create";
}
- | { type: TDelete; username: string }
| {
- type: TChangeUsername;
- username: string;
+ type: TModify;
+ user: HttpUser;
}
- | {
- type: TChangePassword;
- username: string;
- }
- | {
- 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;
@@ -311,20 +201,19 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
return () => {
subscribe = false;
};
- }, []);
+ }, [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;
@@ -333,52 +222,25 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
<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: {
- break;
- }
}
+ }
if (users) {
const userComponents = users.map((user) => {
@@ -386,24 +248,40 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
<UserItem
key={user.username}
user={user}
- onContextMenu={(item) => {}}
+ on={{
+ modify: () => {
+ setDialog({
+ type: "modify",
+ user,
+ });
+ },
+ delete: () => {
+ setDialog({
+ type: "delete",
+ username: user.username,
+ });
+ },
+ }}
/>
);
});
return (
<AdminSubPage>
- <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="success"
+ onClick={() =>
+ setDialog({
+ type: "create",
+ })
+ }
+ >
+ Create User
+ </Button>
+ </Col>
+ </Row>
{userComponents}
{dialogNode}
</AdminSubPage>
diff --git a/FrontEnd/src/app/views/common/OperationDialog.tsx b/FrontEnd/src/app/views/common/OperationDialog.tsx
index e32e9277..08baf93a 100644
--- a/FrontEnd/src/app/views/common/OperationDialog.tsx
+++ b/FrontEnd/src/app/views/common/OperationDialog.tsx
@@ -77,11 +77,6 @@ type MapOperationInputInfoValueTypeList<
[Index in keyof Tuple]: MapOperationInputInfoValueType<Tuple[Index]>;
} & { length: Tuple["length"] };
-interface OperationResult {
- type: "success" | "failure";
- data: unknown;
-}
-
export type OperationInputError =
| {
[index: number]: I18nText | null | undefined;
@@ -98,6 +93,7 @@ const isNoError = (error: OperationInputError): boolean => {
};
export interface OperationDialogProps<
+ TData,
OperationInputInfoList extends readonly OperationInputInfo[]
> {
open: boolean;
@@ -106,28 +102,39 @@ export interface OperationDialogProps<
titleColor?: "default" | "dangerous" | "create" | string;
onProcess: (
inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>
- ) => Promise<unknown>;
+ ) => Promise<TData>;
inputScheme?: OperationInputInfoList;
inputValidator?: (
inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>
) => OperationInputError;
inputPrompt?: I18nText | (() => React.ReactNode);
processPrompt?: () => React.ReactNode;
- successPrompt?: (data: unknown) => React.ReactNode;
+ successPrompt?: (data: TData) => React.ReactNode;
failurePrompt?: (error: unknown) => React.ReactNode;
- onSuccessAndClose?: () => void;
+ onSuccessAndClose?: (data: TData) => void;
}
const OperationDialog = <
+ TData,
OperationInputInfoList extends readonly OperationInputInfo[]
>(
- props: OperationDialogProps<OperationInputInfoList>
+ props: OperationDialogProps<TData, OperationInputInfoList>
): React.ReactElement => {
const inputScheme = props.inputScheme as readonly OperationInputInfo[];
const { t } = useTranslation();
- type Step = "input" | "process" | OperationResult;
+ type Step =
+ | "input"
+ | "process"
+ | {
+ type: "success";
+ data: TData;
+ }
+ | {
+ type: "failure";
+ data: unknown;
+ };
const [step, setStep] = useState<Step>("input");
const [values, setValues] = useState<(boolean | string)[]>(
inputScheme.map((i) => {
@@ -153,7 +160,7 @@ const OperationDialog = <
step.type === "success" &&
props.onSuccessAndClose
) {
- props.onSuccessAndClose();
+ props.onSuccessAndClose(step.data);
}
} else {
console.log("Attempt to close modal when processing.");
@@ -169,7 +176,7 @@ const OperationDialog = <
>
)
.then(
- (d: unknown) => {
+ (d) => {
setStep({
type: "success",
data: d,