Files
factorio-clusterio/packages/web_ui/src/components/UserViewPage.jsx
T
Hornwitser 9fa41e37f3 Show ban reason in Web UI
Show and update live the reason given for someone being banned in the
Web UI.
2022-07-15 15:57:45 +02:00

343 lines
9.7 KiB
React

import React, { useEffect, useContext, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import {
Button, Col, Descriptions, Form, Input, PageHeader, Popconfirm,
Row, Table, Tag, Select, Space, Spin, Switch,
} from "antd";
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
import { libLink } from "@clusterio/lib";
import { useAccount } from "../model/account";
import { useInstanceList } from "../model/instance";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
import SectionHeader from "./SectionHeader";
import { notifyErrorHandler } from "../util/notify";
import { formatDuration } from "../util/time_format";
import { formatLastSeen, sortLastSeen, useUser } from "../model/user";
const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
export default function UserViewPage() {
let params = useParams();
let userName = params.name;
let history = useHistory();
let account = useAccount();
let control = useContext(ControlContext);
let [instanceList] = useInstanceList();
let [user, updateUser] = useUser(userName);
let [roles, setRoles] = useState(null);
let [form] = Form.useForm();
let [rolesDirty, setRolesDirty] = useState(false);
let [banReasonDirty, setBanReasonDirty] = useState(false);
let [applyingRoles, setApplyingRoles] = useState(false);
let [rolesError, setRolesError] = useState();
useEffect(() => {
libLink.messages.listRoles.send(control).then(result => {
setRoles(new Map(result["list"].map(role => [role["id"], role])));
}).catch(() => {
setRoles(new Map());
});
}, []);
useEffect(() => {
if (
account.hasPermission("core.user.set_banned")
&& !banReasonDirty
&& user["ban_reason"] !== form.getFieldValue("ban_reason")
) {
form.setFieldsValue({ "ban_reason": user["ban_reason"] });
}
}, [account, form, user, banReasonDirty]);
let nav = [{ name: "Users", path: "/users" }, { name: userName }];
if (user.loading) {
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={userName}
/>
<Spin size="large" />
</PageLayout>;
}
if (user.missing) {
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title="User not found"
/>
<p>User with name {userName} was not found on the master server.</p>
</PageLayout>;
}
let roleSelector = <Form.Item
noStyle
name="roles"
initialValue={user["roles"]}
rules={[
{ validator: () => (rolesError ? Promise.reject(rolesError) : Promise.resolve()) },
]}
>
<Select
onChange={() => setRolesDirty(true)}
loading={!roles}
mode="multiple"
showArrow={true}
filterOption={(inputValue, option) => {
let role = roles.get(option.value);
return role && role["name"].toLowerCase().includes(inputValue.toLowerCase());
}}
>
{roles && [...roles.values()].map(r => <Select.Option
key={r["id"]}
value={r["id"]}
>{r["name"]}</Select.Option>)}
</Select>
</Form.Item>;
function instanceName(id) {
let instance = instanceList.find(i => i.id === id);
return instance ? instance["name"] : id;
}
return <PageLayout nav={nav}>
<PageHeader
className="site-page-header"
title={<Space>
{userName}
<span>
{user["is_admin"] && <Tag color="gold">Admin</Tag>}
{user["is_whitelisted"] && <Tag>Whitelisted</Tag>}
{user["is_banned"] && <Tag color="red">Banned</Tag>}
</span>
</Space>}
extra={
account.hasPermission("core.user.delete") && <Popconfirm
title={<>
Delete user account and all data associated with it?
{(user["is_banned"] && <><br/>This will remove the user ban!</>)}
</>}
placement="bottomRight"
okText="Delete"
okButtonProps={{ danger: true }}
onConfirm={() => {
libLink.messages.deleteUser.send(
control, { name: userName }
).then(() => {
history.push("/users");
}).catch(notifyErrorHandler("Error deleting user"));
}}
>
<Button danger >
<DeleteOutlined />
</Button>
</Popconfirm>
}
/>
<Form
form={form}
labelCol={{
sm: { span: 6 },
md: { span: 6 },
lg: { span: 4 },
xl: { span: 3 },
xxl: { span: 2 },
}}
wrapperCol={{
sm: { span: 18 },
md: { span: 18 },
lg: { span: 20 },
xl: { span: 21 },
xxl: { span: 22 },
}}
>
<Form.Item
label="Roles"
>
<Row gutter={8}>
<Col flex="auto">
{account.hasPermission("core.user.update_roles")
? roleSelector
: [...user["roles"]].map(id => <Tag key={id}>{
roles ? (roles.get(id) || { name: id })["name"] : id
}</Tag>)
}
</Col>
{account.hasPermission("core.user.update_roles") && <Col flex="0 0 auto">
<Button
type={"primary"}
disabled={!rolesDirty}
loading={applyingRoles}
onClick={() => {
let newRoles = form.getFieldValue("roles");
setApplyingRoles(true);
libLink.messages.updateUserRoles.send(control, {
name: userName,
roles: newRoles,
}).then(() => {
setRolesDirty(false);
setRolesError();
}).catch(err => {
setRolesError(err.message);
return form.validateFields();
}).finally(() => {
setApplyingRoles(false);
});
}}
>Apply</Button>
</Col>}
</Row>
</Form.Item>
<Form.Item label="In-game Admin">
<Switch
checked={user["is_admin"]}
disabled={!account.hasPermission("core.user.set_admin")}
onClick={() => {
libLink.messages.setUserAdmin.send(control, {
name: userName,
admin: !user["is_admin"],
create: false,
}).then(() => {
updateUser();
}).catch(notifyErrorHandler("Error toggling user admin status"));
}}
/>
</Form.Item>
<Form.Item label="Whitelisted">
<Switch
checked={user["is_whitelisted"]}
disabled={!account.hasPermission("core.user.set_whitelisted")}
onClick={() => {
libLink.messages.setUserWhitelisted.send(control, {
name: userName,
whitelisted: !user["is_whitelisted"],
create: false,
}).then(() => {
updateUser();
}).catch(notifyErrorHandler("Error toggling user whitelisted status"));
}}
/>
</Form.Item>
{account.hasPermission("core.user.set_banned")
? <Form.Item label="Ban reason">
<Row gutter={[8, 8]} justify={"end"}>
<Col flex="auto" style={{ minWidth: "20em" }}>
<Form.Item noStyle name="ban_reason" initialValue={user["ban_reason"]}>
<Input
onChange={e => {
setBanReasonDirty(e.target.value !== user["ban_reason"]);
}}
/>
</Form.Item>
</Col>
<Col flex="0 0 auto">
{user["is_banned"]
? <Space>
<Button
type={banReasonDirty ? "primary" : "default"}
onClick={() => {
libLink.messages.setUserBanned.send(control, {
name: userName,
create: false,
banned: true,
reason: form.getFieldValue("ban_reason"),
}).then(() => {
setBanReasonDirty(false);
}).catch(notifyErrorHandler("Error updating ban"));
}}
>Update</Button>
<Button
onClick={() => {
libLink.messages.setUserBanned.send(control, {
name: userName,
create: false,
banned: false,
reason: "",
}).catch(notifyErrorHandler("Error unbanning user"));
}}
>Unban</Button>
</Space>
: <Button
type={banReasonDirty ? "primary" : "default"}
onClick={() => {
libLink.messages.setUserBanned.send(control, {
name: userName,
create: false,
banned: true,
reason: form.getFieldValue("ban_reason"),
}).then(() => {
setBanReasonDirty(false);
}).catch(notifyErrorHandler("Error banning user"));
}}
>Ban</Button>
}
</Col>
</Row>
</Form.Item>
: user["is_banned"]
&& <Form.Item label="Ban reason">{user["ban_reason"]}</Form.Item>
}
</Form>
<SectionHeader title="Player stats" />
<Descriptions size="small" bordered column={{ xs: 1, sm: 2, lg: 3 }}>
<Descriptions.Item label="Total play time">
{user["player_stats"]["online_time_ms"] && formatDuration(user["player_stats"]["online_time_ms"])}
</Descriptions.Item>
<Descriptions.Item label="Total join count">
{user["player_stats"]["join_count"]}
</Descriptions.Item>
<Descriptions.Item label="Last seen">
{formatLastSeen(user)}
</Descriptions.Item>
</Descriptions>
<SectionHeader title="Instance stats" />
<Table
size="small"
columns={[
{
title: "Instance",
key: "instance",
render: ([id]) => instanceName(id),
defaultSortOrder: "ascend",
sorter: (a, b) => strcmp(a[0], b[0]),
},
{
title: "Play time",
key: "playTime",
render: ([, stats]) => formatDuration(stats["online_time_ms"] || 0),
sorter: (a, b) => (a[1]["online_time_ms"] || 0) - (b[1]["online_time_ms"] || 0),
},
{
title: "Join count",
key: "joinCoint",
render: ([, stats]) => stats["join_count"] || 0,
sorter: (a, b) => (a[1]["join_count"] || 0) - (b[1]["join_count"] || 0),
responsive: ["sm"],
},
{
title: "Last seen",
key: "lastSeen",
render: ([id]) => formatLastSeen(user, id),
sorter: (a, b) => sortLastSeen(user, user, a[0], b[0]),
},
]}
dataSource={user["instance_stats"]}
pagination={false}
rowKey={([id]) => id}
onRow={([id], rowIndex) => ({
onClick: event => {
history.push(`/instances/${id}/view`);
},
})}
/>
<PluginExtra component="UserViewPage" user={user} />
</PageLayout>;
}