Files
factorio-clusterio/packages/web_ui/src/components/UserViewPage.jsx
T
Hornwitser bfe119eaa3 Flatten lib namespace
Move all functions and classes currently available through lib/name and
libName to be under a single flat namespace in lib.  This make
reorganising the lib namespace much simpler as the location of symbols
can move without breaking the consumers of lib.

The multi-entrypoint design was already problematic for the web plugin
system which couldn't effectively use it due to how webpack's shared
module system is awkward to set up with multiple entrypoints per package.
2023-07-28 16:03:29 +02:00

343 lines
9.9 KiB
React

import React, { useEffect, useContext, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Button, Col, Descriptions, Form, Input, Popconfirm,
Row, Table, Tag, Select, Space, Spin, Switch,
} from "antd";
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
import * as lib from "@clusterio/lib";
import { useAccount } from "../model/account";
import { useInstanceList } from "../model/instance";
import ControlContext from "./ControlContext";
import PageHeader from "./PageHeader";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
import SectionHeader from "./SectionHeader";
import notify, { 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;
// The user page has a lot of optional elements, that does not make it
// praticularly complicated.
// eslint-disable-next-line complexity
export default function UserViewPage() {
let params = useParams();
let userName = params.name;
let navigate = useNavigate();
let account = useAccount();
let control = useContext(ControlContext);
let [instanceList] = useInstanceList();
let [user] = 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(() => {
control.send(new lib.RoleListRequest()).then(newRoles => {
setRoles(new Map(newRoles.map(role => [role.id, role])));
}).catch(() => {
setRoles(new Map());
});
}, []);
useEffect(() => {
if (
account.hasPermission("core.user.set_banned")
&& !banReasonDirty
&& user.banReason !== form.getFieldValue("banReason")
) {
form.setFieldsValue({ "banReason": user.banReason });
}
}, [account, form, user, banReasonDirty]);
let nav = [{ name: "Users", path: "/users" }, { name: userName }];
if (user.loading) {
return <PageLayout nav={nav}>
<PageHeader title={userName} />
<Spin size="large" />
</PageLayout>;
}
if (user.missing) {
return <PageLayout nav={nav}>
<PageHeader title="User not found" />
<p>User with name {userName} was not found on the controller.</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
title={<Space>
{userName}
<span>
{user.isAdmin && <Tag color="gold">Admin</Tag>}
{user.isWhitelisted && <Tag>Whitelisted</Tag>}
{user.isBanned && <Tag color="red">Banned</Tag>}
</span>
</Space>}
extra={<>
{account.hasPermission("core.user.revoke_token") && (
account.name === userName || account.hasPermission("core.user.revoke_other_token")
) && <Button
danger
onClick={() => {
control.send(
new lib.UserRevokeTokenRequest(userName)
).then(() => {
notify("User token revoked");
}, err => {
if (err instanceof lib.SessionLost && userName === account.name) {
// Got kicked out after revoking our own token
notify("User token revoked");
return;
}
throw err;
}).catch(notifyErrorHandler("Error revoking token"));
}}
>Revoke token</Button>}
{account.hasPermission("core.user.delete") && <Popconfirm
title={<>
Delete user account and all data associated with it?
{(user.isBanned && <><br/>This will remove the user ban!</>)}
</>}
placement="bottomRight"
okText="Delete"
okButtonProps={{ danger: true }}
onConfirm={() => {
control.send(
new lib.UserDeleteRequest(userName)
).then(() => {
navigate("/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, 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);
control.send(
new lib.UserUpdateRolesRequest(userName, newRoles)
).then(() => {
setRolesDirty(false);
setRolesError();
}).catch(err => {
setRolesError(err.message);
return form.validateFields();
}).finally(() => {
setApplyingRoles(false);
});
}}
>Apply</Button>
</Col>}
</Row>
</Form.Item>
{account.hasPermission("core.user.set_admin") && <Form.Item label="In-game Admin">
<Switch
checked={user.isAdmin}
onClick={() => {
control.send(
new lib.UserSetAdminRequest(userName, false, !user.isAdmin)
).catch(notifyErrorHandler("Error toggling user admin status"));
}}
/>
</Form.Item>}
{account.hasPermission("core.user.set_whitelisted") && <Form.Item label="Whitelisted">
<Switch
checked={user.isWhitelisted}
onClick={() => {
control.send(
new lib.UserSetWhitelistedRequest(userName, false, !user.isWhitelisted)
).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="banReason" initialValue={user.banReason}>
<Input
onChange={e => {
setBanReasonDirty(e.target.value !== user.banReason);
}}
/>
</Form.Item>
</Col>
<Col flex="0 0 auto">
{user.isBanned
? <Space>
<Button
type={banReasonDirty ? "primary" : "default"}
onClick={() => {
control.send(
new lib.UserSetBannedRequest(
userName, false, true, form.getFieldValue("banReason")
)
).then(() => {
setBanReasonDirty(false);
}).catch(notifyErrorHandler("Error updating ban"));
}}
>Update</Button>
<Button
onClick={() => {
control.send(
new lib.UserSetBannedRequest(userName, false, false, "")
).catch(notifyErrorHandler("Error unbanning user"));
}}
>Unban</Button>
</Space>
: <Button
type={banReasonDirty ? "primary" : "default"}
onClick={() => {
control.send(
new lib.UserSetBannedRequest(
userName, false, true, form.getFieldValue("banReason")
)
).then(() => {
setBanReasonDirty(false);
}).catch(notifyErrorHandler("Error banning user"));
}}
>Ban</Button>
}
</Col>
</Row>
</Form.Item>
: user.isBanned
&& <Form.Item label="Ban reason">{user.banReason}</Form.Item>
}
</Form>
<SectionHeader title="Player stats" />
<Descriptions size="small" bordered column={{ xs: 1, sm: 2, lg: 3 }}>
<Descriptions.Item label="Total online time">
{formatDuration(user.playerStats.onlineTimeMs || 0)}
</Descriptions.Item>
<Descriptions.Item label="Total join count">
{user.playerStats.joinCount || 0}
</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: "Online time",
key: "onlineTime",
render: ([, stats]) => formatDuration(stats.onlineTimeMs || 0),
sorter: (a, b) => (a[1].onlineTimeMs || 0) - (b[1].onlineTimeMs || 0),
},
{
title: "Join count",
key: "joinCoint",
render: ([, stats]) => stats.joinCount || 0,
sorter: (a, b) => (a[1].joinCount || 0) - (b[1].joinCount || 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.instanceStats.entries()]}
pagination={false}
rowKey={([id]) => id}
onRow={([id], rowIndex) => ({
onClick: event => {
navigate(`/instances/${id}/view`);
},
})}
/>
<PluginExtra component="UserViewPage" user={user} />
</PageLayout>;
}