@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.
Files changed (91) hide show
  1. package/LICENSE.md +21 -0
  2. package/controller.ts +434 -0
  3. package/dist/browser/messages.d.ts +527 -0
  4. package/dist/browser/messages.d.ts.map +1 -0
  5. package/dist/browser/messages.js +561 -0
  6. package/dist/browser/messages.js.map +1 -0
  7. package/dist/browser/package.json +49 -0
  8. package/dist/browser/tsconfig.browser.tsbuildinfo +1 -0
  9. package/dist/browser/web/components/AssignmentForm.d.ts +7 -0
  10. package/dist/browser/web/components/AssignmentForm.d.ts.map +1 -0
  11. package/dist/browser/web/components/AssignmentForm.js +29 -0
  12. package/dist/browser/web/components/AssignmentForm.js.map +1 -0
  13. package/dist/browser/web/components/AssignmentsTable.d.ts +2 -0
  14. package/dist/browser/web/components/AssignmentsTable.d.ts.map +1 -0
  15. package/dist/browser/web/components/AssignmentsTable.js +61 -0
  16. package/dist/browser/web/components/AssignmentsTable.js.map +1 -0
  17. package/dist/browser/web/components/DeleteConfirm.d.ts +6 -0
  18. package/dist/browser/web/components/DeleteConfirm.d.ts.map +1 -0
  19. package/dist/browser/web/components/DeleteConfirm.js +7 -0
  20. package/dist/browser/web/components/DeleteConfirm.js.map +1 -0
  21. package/dist/browser/web/components/GroupForm.d.ts +5 -0
  22. package/dist/browser/web/components/GroupForm.d.ts.map +1 -0
  23. package/dist/browser/web/components/GroupForm.js +22 -0
  24. package/dist/browser/web/components/GroupForm.js.map +1 -0
  25. package/dist/browser/web/components/GroupViewPage.d.ts +2 -0
  26. package/dist/browser/web/components/GroupViewPage.d.ts.map +1 -0
  27. package/dist/browser/web/components/GroupViewPage.js +161 -0
  28. package/dist/browser/web/components/GroupViewPage.js.map +1 -0
  29. package/dist/browser/web/components/GroupsTable.d.ts +2 -0
  30. package/dist/browser/web/components/GroupsTable.d.ts.map +1 -0
  31. package/dist/browser/web/components/GroupsTable.js +29 -0
  32. package/dist/browser/web/components/GroupsTable.js.map +1 -0
  33. package/dist/browser/web/components/RoleMappingForm.d.ts +7 -0
  34. package/dist/browser/web/components/RoleMappingForm.d.ts.map +1 -0
  35. package/dist/browser/web/components/RoleMappingForm.js +32 -0
  36. package/dist/browser/web/components/RoleMappingForm.js.map +1 -0
  37. package/dist/browser/web/components/RoleMappingsTable.d.ts +2 -0
  38. package/dist/browser/web/components/RoleMappingsTable.d.ts.map +1 -0
  39. package/dist/browser/web/components/RoleMappingsTable.js +64 -0
  40. package/dist/browser/web/components/RoleMappingsTable.js.map +1 -0
  41. package/dist/browser/web/index.d.ts +13 -0
  42. package/dist/browser/web/index.d.ts.map +1 -0
  43. package/dist/browser/web/index.js +60 -0
  44. package/dist/browser/web/index.js.map +1 -0
  45. package/dist/node/controller.d.ts +37 -0
  46. package/dist/node/controller.d.ts.map +1 -0
  47. package/dist/node/controller.js +372 -0
  48. package/dist/node/controller.js.map +1 -0
  49. package/dist/node/index.d.ts +10 -0
  50. package/dist/node/index.d.ts.map +1 -0
  51. package/dist/node/index.js +196 -0
  52. package/dist/node/index.js.map +1 -0
  53. package/dist/node/instance.d.ts +40 -0
  54. package/dist/node/instance.d.ts.map +1 -0
  55. package/dist/node/instance.js +155 -0
  56. package/dist/node/instance.js.map +1 -0
  57. package/dist/node/messages.d.ts +527 -0
  58. package/dist/node/messages.d.ts.map +1 -0
  59. package/dist/node/messages.js +620 -0
  60. package/dist/node/messages.js.map +1 -0
  61. package/dist/node/tsconfig.node.tsbuildinfo +1 -0
  62. package/dist/web/manifest.json +10 -0
  63. package/dist/web/static/exp_groups.afefd90b210f7c8e0df2.js +543 -0
  64. package/dist/web/static/index_ts.f7d30aa0c13ff0be393a.js +22 -0
  65. package/dist/web/static/main.c2178975889c1c720e80.js +653 -0
  66. package/dist/web/static/messages_ts.65a81c17fb37953c5d68.js +22 -0
  67. package/dist/web/static/package_json.aab0d3f37da0567c250c.js +22 -0
  68. package/dist/web/static/vendors-node_modules_pnpm_ant-design_icons_6_2_3_rea_94b50600dfe79448cabf92d130d83059_node_mo-c79f54.cf768c64b40c811ce6c9.js +273 -0
  69. package/dist/web/static/vendors-node_modules_pnpm_sinclair_typebox_0_30_4_node_modules_sinclair_typebox_typebox_js.7fa46fac84f2e9324e54.js +22 -0
  70. package/dist/web/static/web_index_tsx-webpack_sharing_consume_default_react.b4766180f4d467b7b24f.js +102 -0
  71. package/index.ts +183 -0
  72. package/instance.ts +171 -0
  73. package/messages.ts +692 -0
  74. package/module/control.lua +438 -0
  75. package/module/globals.lua +11 -0
  76. package/module/module.json +12 -0
  77. package/module/module_exports.lua +3 -0
  78. package/package.json +47 -0
  79. package/tsconfig.browser.json +4 -0
  80. package/tsconfig.json +7 -0
  81. package/tsconfig.node.json +5 -0
  82. package/web/components/AssignmentForm.tsx +74 -0
  83. package/web/components/AssignmentsTable.tsx +111 -0
  84. package/web/components/DeleteConfirm.tsx +16 -0
  85. package/web/components/GroupForm.tsx +45 -0
  86. package/web/components/GroupViewPage.tsx +287 -0
  87. package/web/components/GroupsTable.tsx +49 -0
  88. package/web/components/RoleMappingForm.tsx +106 -0
  89. package/web/components/RoleMappingsTable.tsx +107 -0
  90. package/web/index.tsx +108 -0
  91. 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
+ }