@expcluster/permission-groups 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/controller.ts +434 -0
- package/dist/browser/messages.d.ts +527 -0
- package/dist/browser/messages.d.ts.map +1 -0
- package/dist/browser/messages.js +561 -0
- package/dist/browser/messages.js.map +1 -0
- package/dist/browser/package.json +49 -0
- package/dist/browser/tsconfig.browser.tsbuildinfo +1 -0
- package/dist/browser/web/components/AssignmentForm.d.ts +7 -0
- package/dist/browser/web/components/AssignmentForm.d.ts.map +1 -0
- package/dist/browser/web/components/AssignmentForm.js +29 -0
- package/dist/browser/web/components/AssignmentForm.js.map +1 -0
- package/dist/browser/web/components/AssignmentsTable.d.ts +2 -0
- package/dist/browser/web/components/AssignmentsTable.d.ts.map +1 -0
- package/dist/browser/web/components/AssignmentsTable.js +61 -0
- package/dist/browser/web/components/AssignmentsTable.js.map +1 -0
- package/dist/browser/web/components/DeleteConfirm.d.ts +6 -0
- package/dist/browser/web/components/DeleteConfirm.d.ts.map +1 -0
- package/dist/browser/web/components/DeleteConfirm.js +7 -0
- package/dist/browser/web/components/DeleteConfirm.js.map +1 -0
- package/dist/browser/web/components/GroupForm.d.ts +5 -0
- package/dist/browser/web/components/GroupForm.d.ts.map +1 -0
- package/dist/browser/web/components/GroupForm.js +22 -0
- package/dist/browser/web/components/GroupForm.js.map +1 -0
- package/dist/browser/web/components/GroupViewPage.d.ts +2 -0
- package/dist/browser/web/components/GroupViewPage.d.ts.map +1 -0
- package/dist/browser/web/components/GroupViewPage.js +161 -0
- package/dist/browser/web/components/GroupViewPage.js.map +1 -0
- package/dist/browser/web/components/GroupsTable.d.ts +2 -0
- package/dist/browser/web/components/GroupsTable.d.ts.map +1 -0
- package/dist/browser/web/components/GroupsTable.js +29 -0
- package/dist/browser/web/components/GroupsTable.js.map +1 -0
- package/dist/browser/web/components/RoleMappingForm.d.ts +7 -0
- package/dist/browser/web/components/RoleMappingForm.d.ts.map +1 -0
- package/dist/browser/web/components/RoleMappingForm.js +32 -0
- package/dist/browser/web/components/RoleMappingForm.js.map +1 -0
- package/dist/browser/web/components/RoleMappingsTable.d.ts +2 -0
- package/dist/browser/web/components/RoleMappingsTable.d.ts.map +1 -0
- package/dist/browser/web/components/RoleMappingsTable.js +64 -0
- package/dist/browser/web/components/RoleMappingsTable.js.map +1 -0
- package/dist/browser/web/index.d.ts +13 -0
- package/dist/browser/web/index.d.ts.map +1 -0
- package/dist/browser/web/index.js +60 -0
- package/dist/browser/web/index.js.map +1 -0
- package/dist/node/controller.d.ts +37 -0
- package/dist/node/controller.d.ts.map +1 -0
- package/dist/node/controller.js +372 -0
- package/dist/node/controller.js.map +1 -0
- package/dist/node/index.d.ts +10 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +196 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/instance.d.ts +40 -0
- package/dist/node/instance.d.ts.map +1 -0
- package/dist/node/instance.js +155 -0
- package/dist/node/instance.js.map +1 -0
- package/dist/node/messages.d.ts +527 -0
- package/dist/node/messages.d.ts.map +1 -0
- package/dist/node/messages.js +620 -0
- package/dist/node/messages.js.map +1 -0
- package/dist/node/tsconfig.node.tsbuildinfo +1 -0
- package/dist/web/manifest.json +10 -0
- package/dist/web/static/exp_groups.afefd90b210f7c8e0df2.js +543 -0
- package/dist/web/static/index_ts.f7d30aa0c13ff0be393a.js +22 -0
- package/dist/web/static/main.c2178975889c1c720e80.js +653 -0
- package/dist/web/static/messages_ts.65a81c17fb37953c5d68.js +22 -0
- package/dist/web/static/package_json.aab0d3f37da0567c250c.js +22 -0
- package/dist/web/static/vendors-node_modules_pnpm_ant-design_icons_6_2_3_rea_94b50600dfe79448cabf92d130d83059_node_mo-c79f54.cf768c64b40c811ce6c9.js +273 -0
- package/dist/web/static/vendors-node_modules_pnpm_sinclair_typebox_0_30_4_node_modules_sinclair_typebox_typebox_js.7fa46fac84f2e9324e54.js +22 -0
- package/dist/web/static/web_index_tsx-webpack_sharing_consume_default_react.b4766180f4d467b7b24f.js +102 -0
- package/index.ts +183 -0
- package/instance.ts +171 -0
- package/messages.ts +692 -0
- package/module/control.lua +438 -0
- package/module/globals.lua +11 -0
- package/module/module.json +12 -0
- package/module/module_exports.lua +3 -0
- package/package.json +47 -0
- package/tsconfig.browser.json +4 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +5 -0
- package/web/components/AssignmentForm.tsx +74 -0
- package/web/components/AssignmentsTable.tsx +111 -0
- package/web/components/DeleteConfirm.tsx +16 -0
- package/web/components/GroupForm.tsx +45 -0
- package/web/components/GroupViewPage.tsx +287 -0
- package/web/components/GroupsTable.tsx +49 -0
- package/web/components/RoleMappingForm.tsx +106 -0
- package/web/components/RoleMappingsTable.tsx +107 -0
- package/web/index.tsx +108 -0
- package/webpack.config.js +34 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React, { useContext, useState, useRef } from "react";
|
|
2
|
+
import { Table, Button, Space, Input, InputRef } from "antd";
|
|
3
|
+
import { EditOutlined, SearchOutlined } from "@ant-design/icons";
|
|
4
|
+
|
|
5
|
+
import { ControlContext, useAccount } from "@clusterio/web_ui";
|
|
6
|
+
import { AssignmentDeleteRequest, AssignmentRecord } from "../../messages";
|
|
7
|
+
import type { WebPlugin } from "..";
|
|
8
|
+
|
|
9
|
+
import AssignmentForm from "./AssignmentForm";
|
|
10
|
+
import DeletedConfirm from "./DeleteConfirm";
|
|
11
|
+
|
|
12
|
+
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
|
13
|
+
|
|
14
|
+
export default function AssignmentsTable() {
|
|
15
|
+
const control = useContext(ControlContext);
|
|
16
|
+
const plugin = control.plugins.get("exp_groups") as WebPlugin;
|
|
17
|
+
const account = useAccount();
|
|
18
|
+
|
|
19
|
+
const searchInput = useRef<InputRef>(null);
|
|
20
|
+
|
|
21
|
+
const [assignments, assignmentsSynced] = plugin.useAssignments();
|
|
22
|
+
const [groups, groupsSynced] = plugin.useGroups();
|
|
23
|
+
|
|
24
|
+
const [editing, setEditing] = useState<AssignmentRecord | undefined>();
|
|
25
|
+
const [open, setOpen] = useState(false);
|
|
26
|
+
|
|
27
|
+
const assignmentsArray = [...assignments.values()];
|
|
28
|
+
|
|
29
|
+
const groupFilters = [...groups.values()]
|
|
30
|
+
.filter(group => assignmentsArray.some(assignment => assignment.groupId === group.id))
|
|
31
|
+
.map(group => ({ text: group.name, value: group.id }))
|
|
32
|
+
|
|
33
|
+
return <>
|
|
34
|
+
<Table
|
|
35
|
+
columns={[
|
|
36
|
+
{
|
|
37
|
+
title: "Player",
|
|
38
|
+
dataIndex: "name",
|
|
39
|
+
sorter: (a: AssignmentRecord, b: AssignmentRecord) => strcmp(a.name, b.name),
|
|
40
|
+
filterIcon: (filtered: boolean) => (
|
|
41
|
+
<SearchOutlined style={{ color: filtered ? "#1677ff" : undefined }} />
|
|
42
|
+
),
|
|
43
|
+
onFilter: (value, record: AssignmentRecord) => (
|
|
44
|
+
record.name.toLowerCase().includes(String(value).toLowerCase())
|
|
45
|
+
),
|
|
46
|
+
filterDropdownProps: {
|
|
47
|
+
onOpenChange: (open: boolean) => open && setTimeout(() => searchInput.current?.select(), 100),
|
|
48
|
+
},
|
|
49
|
+
filterDropdown: ({ selectedKeys, setSelectedKeys, confirm, clearFilters }) => (
|
|
50
|
+
<div style={{ padding: 4 }} onKeyDown={(e) => e.stopPropagation()}>
|
|
51
|
+
<Input.Search
|
|
52
|
+
allowClear
|
|
53
|
+
ref={searchInput}
|
|
54
|
+
placeholder={"Search username"}
|
|
55
|
+
value={selectedKeys[0]}
|
|
56
|
+
onChange={(e) => setSelectedKeys([e.target.value])}
|
|
57
|
+
onSearch={() => confirm({ closeDropdown: false })}
|
|
58
|
+
onClear={() => {
|
|
59
|
+
clearFilters?.({ closeDropdown: false });
|
|
60
|
+
confirm({ closeDropdown: true });
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: "Group",
|
|
68
|
+
width: "40%",
|
|
69
|
+
filters: groupFilters,
|
|
70
|
+
onFilter: (value, record: AssignmentRecord) => (
|
|
71
|
+
record.groupId === value
|
|
72
|
+
),
|
|
73
|
+
render: (_: any, a: any) => (
|
|
74
|
+
groups.get(a.groupId)?.name ?? a.groupId
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
...(account.hasAnyPermission(
|
|
78
|
+
"exp_groups.assignment.update",
|
|
79
|
+
"exp_groups.assignment.delete",
|
|
80
|
+
) ? [{
|
|
81
|
+
title: "Actions",
|
|
82
|
+
width: "10%",
|
|
83
|
+
render: (_: any, record: AssignmentRecord) => (
|
|
84
|
+
<Space>
|
|
85
|
+
{account.hasPermission("exp_groups.assignment.update") &&
|
|
86
|
+
<Button icon={<EditOutlined />} onClick={() => {
|
|
87
|
+
setEditing(record);
|
|
88
|
+
setOpen(true);
|
|
89
|
+
}} />
|
|
90
|
+
}
|
|
91
|
+
{account.hasPermission("exp_groups.assignment.delete") && <DeletedConfirm
|
|
92
|
+
onConfirm={() => control.send(new AssignmentDeleteRequest(record.name))}
|
|
93
|
+
/>}
|
|
94
|
+
</Space>
|
|
95
|
+
),
|
|
96
|
+
}] : []),
|
|
97
|
+
]}
|
|
98
|
+
dataSource={assignmentsArray}
|
|
99
|
+
loading={!assignmentsSynced || !groupsSynced}
|
|
100
|
+
rowKey={(a) => a.name}
|
|
101
|
+
pagination={{
|
|
102
|
+
defaultPageSize: 50,
|
|
103
|
+
showSizeChanger: true,
|
|
104
|
+
pageSizeOptions: ["10", "20", "50", "100", "200"],
|
|
105
|
+
showTotal: (total: number) => `${total} Assignments`,
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
<AssignmentForm open={open} setOpen={setOpen} initial={editing} />
|
|
110
|
+
</>;
|
|
111
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button, Popconfirm } from "antd";
|
|
3
|
+
import { DeleteOutlined } from "@ant-design/icons";
|
|
4
|
+
import { TooltipPlacement } from "antd/es/tooltip";
|
|
5
|
+
|
|
6
|
+
export default function DeletedConfirm(props: { onConfirm: () => void, placement?: TooltipPlacement }) {
|
|
7
|
+
return <Popconfirm
|
|
8
|
+
title="Delete?"
|
|
9
|
+
okText="Delete"
|
|
10
|
+
okButtonProps={{ danger: true }}
|
|
11
|
+
onConfirm={props.onConfirm}
|
|
12
|
+
placement={props.placement}
|
|
13
|
+
>
|
|
14
|
+
<Button danger icon={<DeleteOutlined />} />
|
|
15
|
+
</Popconfirm>
|
|
16
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { useContext, useEffect } from "react";
|
|
2
|
+
import { Modal, Form, Input, Switch } from "antd";
|
|
3
|
+
import { useNavigate } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
import { ControlContext } from "@clusterio/web_ui";
|
|
6
|
+
import * as messages from "../../messages";
|
|
7
|
+
|
|
8
|
+
export default function GroupForm({ open, setOpen }: {
|
|
9
|
+
open: boolean,
|
|
10
|
+
setOpen: (open: boolean) => void,
|
|
11
|
+
}) {
|
|
12
|
+
const control = useContext(ControlContext);
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
|
|
15
|
+
const [form] = Form.useForm();
|
|
16
|
+
|
|
17
|
+
async function submit(values: any) {
|
|
18
|
+
const group = await control.send(new messages.GroupCreateRequest(
|
|
19
|
+
values.name, new messages.GroupPermissions(Boolean(values.isBlacklist), [])
|
|
20
|
+
));
|
|
21
|
+
navigate(`/permission_groups/${group.id}/view`);
|
|
22
|
+
setOpen(false);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (open) form.resetFields();
|
|
27
|
+
}, [open]);
|
|
28
|
+
|
|
29
|
+
return <Modal
|
|
30
|
+
title="Create Group"
|
|
31
|
+
open={open}
|
|
32
|
+
onCancel={() => setOpen(false)}
|
|
33
|
+
onOk={() => form.submit()}
|
|
34
|
+
>
|
|
35
|
+
<Form form={form} onFinish={submit}>
|
|
36
|
+
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
|
|
37
|
+
<Input />
|
|
38
|
+
</Form.Item>
|
|
39
|
+
|
|
40
|
+
<Form.Item name="isBlacklist" label="Grant all by default" valuePropName="checked">
|
|
41
|
+
<Switch />
|
|
42
|
+
</Form.Item>
|
|
43
|
+
</Form>
|
|
44
|
+
</Modal>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useNavigate, useParams } from "react-router-dom";
|
|
3
|
+
import { Button, Checkbox, Input, Space, Spin, Alert } from "antd";
|
|
4
|
+
|
|
5
|
+
import { ControlContext, useAccount, useDefaultModPack, PageLayout, PageHeader, notifyErrorHandler } from "@clusterio/web_ui";
|
|
6
|
+
import DeletedConfirm from "./DeleteConfirm";
|
|
7
|
+
|
|
8
|
+
import * as messages from "../../messages";
|
|
9
|
+
import type { WebPlugin } from "..";
|
|
10
|
+
|
|
11
|
+
const DOMAIN_MAPPING = {
|
|
12
|
+
"Admin": ["admin", "cheat", "permission", "infinity", "editor", "spawn"],
|
|
13
|
+
"Building & Crafting": ["mining", "build", "craft", "deconstruct", "rotate", "entity"],
|
|
14
|
+
"Inventory": ["inventory", "stack", "cursor", "slot", "quick_bar", "item", "equipment"],
|
|
15
|
+
"Blueprints": ["blueprint", "copy", "paste", "undo", "redo", "upgrade"],
|
|
16
|
+
"Trains": ["train", "station", "schedule", "interrupt", "rolling_stock"],
|
|
17
|
+
"Circuit Network": ["combinator", "circuit", "signal", "wire", "cable", "speaker", "display_panel"],
|
|
18
|
+
"GUI": ["gui", "open"],
|
|
19
|
+
"Pins & Alerts": ["pin", "alert", "tag"],
|
|
20
|
+
"Logistics": ["logistic", "filter", "request"],
|
|
21
|
+
"Space": ["space", "surface", "planet", "rocket"],
|
|
22
|
+
"Toggles": ["toggle", "switch", "swap", "set"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function getDomain(name: string) {
|
|
26
|
+
const n = name.toLowerCase();
|
|
27
|
+
|
|
28
|
+
for (const [domain, keywords] of Object.entries(DOMAIN_MAPPING)) {
|
|
29
|
+
if (keywords.some(k => n.includes(k))) return domain;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return "Other";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function GroupViewPage() {
|
|
36
|
+
const control = useContext(ControlContext);
|
|
37
|
+
const plugin = control.plugins.get("exp_groups") as WebPlugin;
|
|
38
|
+
const navigate = useNavigate();
|
|
39
|
+
const account = useAccount();
|
|
40
|
+
|
|
41
|
+
const { id } = useParams();
|
|
42
|
+
const groupId = Number(id);
|
|
43
|
+
|
|
44
|
+
const [groups, synced] = plugin.useGroups();
|
|
45
|
+
const group = groups.get(groupId);
|
|
46
|
+
|
|
47
|
+
const [name, setName] = useState("");
|
|
48
|
+
|
|
49
|
+
const defaultModPack = useDefaultModPack();
|
|
50
|
+
const [definesJson, setDefinesJson] = useState<Record<string, number> | null>(null);
|
|
51
|
+
|
|
52
|
+
const [permissionState, setPermissionState] = useState<Record<string, boolean>>({});
|
|
53
|
+
const [baselineState, setBaselineState] = useState<Record<string, boolean>>({});
|
|
54
|
+
const [search, setSearch] = useState("");
|
|
55
|
+
|
|
56
|
+
const canUpdate = Boolean(account.hasPermission("exp_groups.group.update"));
|
|
57
|
+
const canDelete = Boolean(account.hasPermission("exp_groups.group.delete"));
|
|
58
|
+
|
|
59
|
+
// fetch defines
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!defaultModPack?.exportManifest?.assets?.defines) return;
|
|
62
|
+
if (definesJson) return;
|
|
63
|
+
|
|
64
|
+
const assetName = defaultModPack.exportManifest.assets.defines;
|
|
65
|
+
|
|
66
|
+
(async () => {
|
|
67
|
+
const response = await fetch(`${staticRoot}${assetName}`);
|
|
68
|
+
const json = await response.json();
|
|
69
|
+
setDefinesJson(json.input_action ?? {});
|
|
70
|
+
})().catch(notifyErrorHandler("Failed to load permissions"));
|
|
71
|
+
}, [defaultModPack]);
|
|
72
|
+
|
|
73
|
+
// initialise permissions
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!group || !definesJson) return;
|
|
76
|
+
if (Object.keys(permissionState).length) return;
|
|
77
|
+
|
|
78
|
+
const { isBlacklist, permissions } = group.permissions;
|
|
79
|
+
|
|
80
|
+
const base = Object.fromEntries(
|
|
81
|
+
Object.keys(definesJson).map(p => [
|
|
82
|
+
p, isBlacklist !== permissions.includes(p)
|
|
83
|
+
])
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
setPermissionState(base);
|
|
87
|
+
setBaselineState(base);
|
|
88
|
+
}, [group, definesJson]);
|
|
89
|
+
|
|
90
|
+
// sync name
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (group) setName(group.name);
|
|
93
|
+
}, [group]);
|
|
94
|
+
|
|
95
|
+
// derived data
|
|
96
|
+
const allPermissions = useMemo(() => (
|
|
97
|
+
Object.keys(definesJson ?? {})
|
|
98
|
+
), [definesJson]);
|
|
99
|
+
|
|
100
|
+
const filtered = useMemo(() => allPermissions.filter(p =>
|
|
101
|
+
p.toLowerCase().includes(search.toLowerCase())
|
|
102
|
+
), [allPermissions, search]);
|
|
103
|
+
|
|
104
|
+
const grouped = useMemo(() => {
|
|
105
|
+
const map = new Map<string, string[]>();
|
|
106
|
+
|
|
107
|
+
for (const p of filtered) {
|
|
108
|
+
const domain = getDomain(p);
|
|
109
|
+
if (!map.has(domain)) map.set(domain, []);
|
|
110
|
+
map.get(domain)!.push(p);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sorted = [...map.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
114
|
+
|
|
115
|
+
for (const [, perms] of sorted) {
|
|
116
|
+
perms.sort((a, b) => a.localeCompare(b));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return sorted;
|
|
120
|
+
}, [filtered]);
|
|
121
|
+
|
|
122
|
+
const permissionChanged = Object.keys(permissionState).some(
|
|
123
|
+
k => permissionState[k] !== baselineState[k]
|
|
124
|
+
);
|
|
125
|
+
const metaChanged = group ? name !== group.name : false;
|
|
126
|
+
const edited = permissionChanged || metaChanged;
|
|
127
|
+
|
|
128
|
+
function applyChanges() {
|
|
129
|
+
if (!group) return;
|
|
130
|
+
|
|
131
|
+
const whitelist: string[] = [];
|
|
132
|
+
const blacklist: string[] = [];
|
|
133
|
+
|
|
134
|
+
for (const [perm, enabled] of Object.entries(permissionState)) {
|
|
135
|
+
if (enabled) whitelist.push(perm);
|
|
136
|
+
else blacklist.push(perm);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isBlacklist = blacklist.length < whitelist.length;
|
|
140
|
+
const permissions = isBlacklist ? blacklist : whitelist;
|
|
141
|
+
|
|
142
|
+
control.send(new messages.GroupUpdateRequest(
|
|
143
|
+
new messages.GroupRecord(group.id, name, new messages.GroupPermissions(isBlacklist, permissions))
|
|
144
|
+
))
|
|
145
|
+
.then(() => setBaselineState(permissionState))
|
|
146
|
+
.catch(notifyErrorHandler("Error applying changes"));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function revertChanges() {
|
|
150
|
+
if (!group) return;
|
|
151
|
+
setPermissionState(baselineState);
|
|
152
|
+
setName(group.name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!synced) {
|
|
156
|
+
return <PageLayout nav={[{ name: "exp_groups" }, { name: String(groupId) }]}>
|
|
157
|
+
<Spin />
|
|
158
|
+
</PageLayout>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!group) {
|
|
162
|
+
return <PageLayout nav={[{ name: "exp_groups" }, { name: String(groupId) }]}>
|
|
163
|
+
<PageHeader title="Group not found" />
|
|
164
|
+
</PageLayout>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return <PageLayout nav={[{ name: "Permission Groups", path: "/permission_groups" }, { name: group.name }]}>
|
|
168
|
+
<PageHeader
|
|
169
|
+
title={group.name}
|
|
170
|
+
extra={<Space wrap>
|
|
171
|
+
{canDelete && <DeletedConfirm placement="bottomRight" onConfirm={async () => {
|
|
172
|
+
control.send(new messages.GroupDeleteRequest(group.id));
|
|
173
|
+
navigate(`/permission_groups`);
|
|
174
|
+
}}/>}
|
|
175
|
+
</Space>}
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
{edited && (
|
|
179
|
+
<div style={{
|
|
180
|
+
position: "fixed",
|
|
181
|
+
bottom: 24,
|
|
182
|
+
left: "50%",
|
|
183
|
+
transform: "translateX(-50%)",
|
|
184
|
+
background: "#2a1912",
|
|
185
|
+
borderRadius: 8,
|
|
186
|
+
padding: "10px 14px",
|
|
187
|
+
display: "flex",
|
|
188
|
+
gap: 12,
|
|
189
|
+
zIndex: 1000,
|
|
190
|
+
}}>
|
|
191
|
+
<span>You have unsaved changes</span>
|
|
192
|
+
<Space>
|
|
193
|
+
<Button onClick={revertChanges}>Revert</Button>
|
|
194
|
+
<Button type="primary" onClick={applyChanges}>Apply</Button>
|
|
195
|
+
</Space>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
<div style={{ marginBottom: 16, display: "flex", gap: 12 }}>
|
|
200
|
+
<label style={{ width: 110 }}>Name</label>
|
|
201
|
+
<Input value={name} disabled={!canUpdate} onChange={e => setName(e.target.value)} />
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<h3>Permissions</h3>
|
|
205
|
+
|
|
206
|
+
{!defaultModPack?.exportManifest?.assets?.defines && (
|
|
207
|
+
<Alert
|
|
208
|
+
type="warning"
|
|
209
|
+
message="Missing export"
|
|
210
|
+
description="An export of the default mod pack is required to modify permissions."
|
|
211
|
+
style={{ marginBottom: 16 }}
|
|
212
|
+
showIcon
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{defaultModPack?.exportManifest?.assets?.defines && !definesJson && <Spin />}
|
|
217
|
+
|
|
218
|
+
{definesJson && <>
|
|
219
|
+
<Input.Search
|
|
220
|
+
placeholder="Search permissions"
|
|
221
|
+
allowClear
|
|
222
|
+
onChange={e => setSearch(e.target.value)}
|
|
223
|
+
style={{ marginBottom: 16 }}
|
|
224
|
+
/>
|
|
225
|
+
|
|
226
|
+
<Space direction="vertical" style={{ width: "100%" }}>
|
|
227
|
+
{grouped.map(([groupName, permissions]) => (
|
|
228
|
+
<div key={groupName} style={{ border: "1px solid #424242", padding: 12, borderRadius: 6 }}>
|
|
229
|
+
|
|
230
|
+
<Space style={{ width: "100%", justifyContent: "space-between" }}>
|
|
231
|
+
<strong>{groupName}</strong>
|
|
232
|
+
<Space>
|
|
233
|
+
<Button
|
|
234
|
+
size="small"
|
|
235
|
+
disabled={!canUpdate}
|
|
236
|
+
onClick={() => {
|
|
237
|
+
setPermissionState(prev => {
|
|
238
|
+
const next = { ...prev };
|
|
239
|
+
for (const p of permissions) next[p] = true;
|
|
240
|
+
return next;
|
|
241
|
+
});
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
Select all
|
|
245
|
+
</Button>
|
|
246
|
+
|
|
247
|
+
<Button
|
|
248
|
+
size="small"
|
|
249
|
+
disabled={!canUpdate}
|
|
250
|
+
onClick={() => {
|
|
251
|
+
setPermissionState(prev => {
|
|
252
|
+
const next = { ...prev };
|
|
253
|
+
for (const p of permissions) next[p] = false;
|
|
254
|
+
return next;
|
|
255
|
+
});
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
Clear
|
|
259
|
+
</Button>
|
|
260
|
+
</Space>
|
|
261
|
+
</Space>
|
|
262
|
+
|
|
263
|
+
<div style={{
|
|
264
|
+
marginTop: 12,
|
|
265
|
+
display: "grid",
|
|
266
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
|
|
267
|
+
gap: 8,
|
|
268
|
+
}}>
|
|
269
|
+
{permissions.map(p => (
|
|
270
|
+
<Checkbox
|
|
271
|
+
key={p}
|
|
272
|
+
checked={permissionState[p]}
|
|
273
|
+
disabled={!canUpdate}
|
|
274
|
+
onChange={e =>
|
|
275
|
+
setPermissionState(prev => ({ ...prev, [p]: e.target.checked }))
|
|
276
|
+
}
|
|
277
|
+
>
|
|
278
|
+
{p}
|
|
279
|
+
</Checkbox>
|
|
280
|
+
))}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
))}
|
|
284
|
+
</Space>
|
|
285
|
+
</>}
|
|
286
|
+
</PageLayout>;
|
|
287
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useContext, useState } from "react";
|
|
2
|
+
import { Table } from "antd";
|
|
3
|
+
import { useNavigate } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
import { ControlContext } from "@clusterio/web_ui";
|
|
6
|
+
import { GroupRecord } from "../../messages";
|
|
7
|
+
import type { WebPlugin } from "..";
|
|
8
|
+
|
|
9
|
+
import GroupForm from "./GroupForm";
|
|
10
|
+
|
|
11
|
+
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
|
12
|
+
|
|
13
|
+
export default function GroupsTable() {
|
|
14
|
+
const control = useContext(ControlContext);
|
|
15
|
+
const plugin = control.plugins.get("exp_groups") as WebPlugin;
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
|
|
18
|
+
const [groups, synced] = plugin.useGroups();
|
|
19
|
+
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
|
|
22
|
+
return <>
|
|
23
|
+
<Table
|
|
24
|
+
columns={[
|
|
25
|
+
{
|
|
26
|
+
title: "Name",
|
|
27
|
+
dataIndex: "name",
|
|
28
|
+
sorter: (a: any, b: GroupRecord) => strcmp(a.name, b.name),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: "Permissions",
|
|
32
|
+
width: "50%",
|
|
33
|
+
render: (_: any, g: GroupRecord) => (
|
|
34
|
+
`${g.permissions.permissions.length} ${g.permissions.isBlacklist ? "Disallowed" : "Allowed"}`
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
]}
|
|
38
|
+
dataSource={[...groups.values()]}
|
|
39
|
+
loading={!synced}
|
|
40
|
+
rowKey={(g) => g.id}
|
|
41
|
+
pagination={false}
|
|
42
|
+
onRow={(g) => ({
|
|
43
|
+
onClick: () => navigate(`/permission_groups/${g.id}/view`),
|
|
44
|
+
})}
|
|
45
|
+
/>
|
|
46
|
+
|
|
47
|
+
<GroupForm open={open} setOpen={setOpen} />
|
|
48
|
+
</>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useContext, useEffect } from "react";
|
|
2
|
+
import { Modal, Form, Select, InputNumber, Switch, Alert } from "antd";
|
|
3
|
+
|
|
4
|
+
import { ControlContext, useRoles } from "@clusterio/web_ui";
|
|
5
|
+
import { RoleMappingRecord, RoleMappingCreateRequest, RoleMappingUpdateRequest } from "../../messages";
|
|
6
|
+
import type { WebPlugin } from "..";
|
|
7
|
+
|
|
8
|
+
export default function RoleMappingForm({ open, setOpen, initial }: {
|
|
9
|
+
open: boolean,
|
|
10
|
+
setOpen: (open: boolean) => void,
|
|
11
|
+
initial?: RoleMappingRecord,
|
|
12
|
+
}) {
|
|
13
|
+
const control = useContext(ControlContext);
|
|
14
|
+
const plugin = control.plugins.get("exp_groups") as WebPlugin;
|
|
15
|
+
|
|
16
|
+
const [groups] = plugin.useGroups();
|
|
17
|
+
const [roles] = useRoles();
|
|
18
|
+
|
|
19
|
+
const [form] = Form.useForm();
|
|
20
|
+
|
|
21
|
+
function submit(values: any) {
|
|
22
|
+
if (initial) {
|
|
23
|
+
control.send(new RoleMappingUpdateRequest(
|
|
24
|
+
new RoleMappingRecord(
|
|
25
|
+
initial.id,
|
|
26
|
+
new Set(values.roleIds),
|
|
27
|
+
values.groupId,
|
|
28
|
+
values.priority,
|
|
29
|
+
values.enabled,
|
|
30
|
+
)
|
|
31
|
+
));
|
|
32
|
+
} else {
|
|
33
|
+
control.send(new RoleMappingCreateRequest(
|
|
34
|
+
values.roleIds,
|
|
35
|
+
values.groupId,
|
|
36
|
+
values.priority,
|
|
37
|
+
values.enabled,
|
|
38
|
+
));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setOpen(false);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (open) {
|
|
46
|
+
form.resetFields();
|
|
47
|
+
form.setFieldsValue({
|
|
48
|
+
...initial,
|
|
49
|
+
roleIds: initial ? [...initial.roleIds] : [],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}, [open, initial]);
|
|
53
|
+
|
|
54
|
+
return <Modal
|
|
55
|
+
title={initial ? "Edit Role Mapping" : "Create Role Mapping"}
|
|
56
|
+
open={open}
|
|
57
|
+
onCancel={() => setOpen(false)}
|
|
58
|
+
onOk={() => form.submit()}
|
|
59
|
+
>
|
|
60
|
+
<Alert
|
|
61
|
+
type="info"
|
|
62
|
+
showIcon
|
|
63
|
+
message="How role mappings work"
|
|
64
|
+
style={{ marginBottom: 16 }}
|
|
65
|
+
description={
|
|
66
|
+
<ul style={{ paddingLeft: 16, margin: 0 }}>
|
|
67
|
+
<li>Manual assignments always take priority.</li>
|
|
68
|
+
<li>Mappings are checked in priority order (highest first).</li>
|
|
69
|
+
<li>A mapping applies only if the user has <b>all listed roles</b>.</li>
|
|
70
|
+
<li>The first matching mapping assigns the group.</li>
|
|
71
|
+
<li>If none match, the player is given all permissions.</li>
|
|
72
|
+
</ul>
|
|
73
|
+
}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<Form form={form} onFinish={submit}>
|
|
77
|
+
<Form.Item name="groupId" label="Group" rules={[{ required: true }]}>
|
|
78
|
+
<Select>
|
|
79
|
+
{[...groups.values()].map(g => (
|
|
80
|
+
<Select.Option key={g.id} value={g.id}>
|
|
81
|
+
{g.name}
|
|
82
|
+
</Select.Option>
|
|
83
|
+
))}
|
|
84
|
+
</Select>
|
|
85
|
+
</Form.Item>
|
|
86
|
+
|
|
87
|
+
<Form.Item name="roleIds" label="Roles" rules={[{ required: true }]}>
|
|
88
|
+
<Select mode="multiple">
|
|
89
|
+
{[...roles.values()].map(r => (
|
|
90
|
+
<Select.Option key={r.id} value={r.id}>
|
|
91
|
+
{r.name}
|
|
92
|
+
</Select.Option>
|
|
93
|
+
))}
|
|
94
|
+
</Select>
|
|
95
|
+
</Form.Item>
|
|
96
|
+
|
|
97
|
+
<Form.Item name="priority" label="Priority" initialValue={100}>
|
|
98
|
+
<InputNumber style={{ width: "100%" }} />
|
|
99
|
+
</Form.Item>
|
|
100
|
+
|
|
101
|
+
<Form.Item name="enabled" label="Enabled" valuePropName="checked" initialValue={true}>
|
|
102
|
+
<Switch />
|
|
103
|
+
</Form.Item>
|
|
104
|
+
</Form>
|
|
105
|
+
</Modal>;
|
|
106
|
+
}
|