From 0c74aef68e13a9a8be0d6c99acc9df86ad0a731b Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Mon, 20 Sep 2021 00:54:43 +0200 Subject: [PATCH] Hide unauthorized web interface controls Hide or disable controls in the web interface that the user for the control connection it's connected as don't have the requisite permissions to use. --- .../web_ui/src/components/InstanceConsole.jsx | 32 +- .../src/components/InstanceViewPage.jsx | 40 ++- .../web_ui/src/components/InstancesPage.jsx | 4 +- packages/web_ui/src/components/MasterPage.jsx | 5 +- .../web_ui/src/components/RoleViewPage.jsx | 72 ++--- packages/web_ui/src/components/RolesPage.jsx | 116 ++++++-- packages/web_ui/src/components/SavesList.jsx | 34 ++- packages/web_ui/src/components/SiteLayout.jsx | 9 +- packages/web_ui/src/components/SlavesPage.jsx | 5 +- .../web_ui/src/components/UserViewPage.jsx | 98 +++--- packages/web_ui/src/components/UsersPage.jsx | 132 +++++--- packages/web_ui/src/components/data-table.jsx | 281 ------------------ packages/web_ui/src/pages.jsx | 64 +++- plugins/inventory_sync/web/index.jsx | 7 +- plugins/subspace_storage/web/index.jsx | 7 +- 15 files changed, 417 insertions(+), 489 deletions(-) delete mode 100644 packages/web_ui/src/components/data-table.jsx diff --git a/packages/web_ui/src/components/InstanceConsole.jsx b/packages/web_ui/src/components/InstanceConsole.jsx index 30e8f76f..aa2d50ca 100644 --- a/packages/web_ui/src/components/InstanceConsole.jsx +++ b/packages/web_ui/src/components/InstanceConsole.jsx @@ -3,6 +3,7 @@ import { Typography } from "antd"; import { libLink } from "@clusterio/lib"; +import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; const { Paragraph } = Typography; @@ -49,6 +50,7 @@ function formatLog(info, key) { } export default function InstanceConsole(props) { + let account = useAccount(); let control = useContext(ControlContext); let anchor = useRef(); let [pastLines, setPastLines] = useState([{"Loading past entries..."}
]); @@ -59,19 +61,23 @@ export default function InstanceConsole(props) { let parent = anchor.current.parentElement; parent.scrollTop = parent.scrollHeight - parent.clientHeight; - libLink.messages.queryLog.send(control, { - all: false, - master: false, - slave_ids: [], - instance_ids: [props.id], - max_level: null, - limit: 400, - order: "desc", - }).then(result => { - setPastLines(result.log.map((info, index) => formatLog(info, index)).reverse()); - }).catch(err => { - setPastLines([{`Error loading log: ${err.message}`}
]); - }); + if (account.hasPermission("core.log.query")) { + libLink.messages.queryLog.send(control, { + all: false, + master: false, + slave_ids: [], + instance_ids: [props.id], + max_level: null, + limit: 400, + order: "desc", + }).then(result => { + setPastLines(result.log.map((info, index) => formatLog(info, index)).reverse()); + }).catch(err => { + setPastLines([{`Error loading log: ${err.message}`}
]); + }); + } else { + setPastLines([]); + } function logHandler(info) { setLines(currentLines => currentLines.concat( diff --git a/packages/web_ui/src/components/InstanceViewPage.jsx b/packages/web_ui/src/components/InstanceViewPage.jsx index d38a300f..e9bf72c9 100644 --- a/packages/web_ui/src/components/InstanceViewPage.jsx +++ b/packages/web_ui/src/components/InstanceViewPage.jsx @@ -5,6 +5,7 @@ import DeleteOutlined from "@ant-design/icons/DeleteOutlined"; import { libLink } from "@clusterio/lib"; +import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; import PageLayout from "./PageLayout"; import InstanceConfigTree from "./InstanceConfigTree"; @@ -27,6 +28,7 @@ export default function InstanceViewPage(props) { let history = useHistory(); + let account = useAccount(); let control = useContext(ControlContext); let [instance] = useInstance(instanceId); let [slave] = useSlave(Number(instance["assigned_slave"])); @@ -46,9 +48,12 @@ export default function InstanceViewPage(props) { } let instanceButtons = - - - - } + {account.hasPermission("core.instance.delete") && - + } ; let assigned = instance["assigned_slave"] !== null; @@ -99,7 +104,7 @@ export default function InstanceViewPage(props) { ? Unassigned : slave["name"] || instance["assigned_slave"] } - + />} {instance["status"]} - - Console - - + { + account.hasAllPermission("core.instance.save.list", "core.instance.save.list_subscribe") + && + } + { + account.hasAnyPermission("core.log.follow", "core.instance.send_rcon") + && Console + } + {account.hasPermission("core.log.follow") && } + { + account.hasPermission("core.instance.send_rcon") + && + } - + {account.hasPermission("core.instance.get_config") && } ; } diff --git a/packages/web_ui/src/components/InstancesPage.jsx b/packages/web_ui/src/components/InstancesPage.jsx index be54b6c7..e7020a05 100644 --- a/packages/web_ui/src/components/InstancesPage.jsx +++ b/packages/web_ui/src/components/InstancesPage.jsx @@ -4,6 +4,7 @@ import { Button, Form, Input, Modal, PageHeader, Table } from "antd"; import { libConfig, libLink } from "@clusterio/lib"; +import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; import PageLayout from "./PageLayout"; import { useInstanceList } from "../model/instance"; @@ -59,6 +60,7 @@ function CreateInstanceButton(props) { export default function InstancesPage() { let history = useHistory(); + let account = useAccount(); let [slaveList] = useSlaveList(); let [instanceList] = useInstanceList(); @@ -66,7 +68,7 @@ export default function InstancesPage() { + extra={account.hasPermission("core.instance.create") && } />

Master

- + {account.hasPermission("core.master.get_config") && } ; }; diff --git a/packages/web_ui/src/components/RoleViewPage.jsx b/packages/web_ui/src/components/RoleViewPage.jsx index 900dfb07..ddd7a9f0 100644 --- a/packages/web_ui/src/components/RoleViewPage.jsx +++ b/packages/web_ui/src/components/RoleViewPage.jsx @@ -1,10 +1,11 @@ import React, { useEffect, useContext, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; -import { Button, Checkbox, Col, Form, Input, Popconfirm, Row, Space, Spin } from "antd"; +import { Button, Checkbox, Form, Input, PageHeader, Popconfirm, Spin } from "antd"; import DeleteOutlined from "@ant-design/icons/DeleteOutlined"; import { libLink, libUsers } from "@clusterio/lib"; +import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; import PageLayout from "./PageLayout"; import { notifyErrorHandler } from "../util/notify"; @@ -42,6 +43,7 @@ export default function RoleViewPage() { let history = useHistory(); + let account = useAccount(); let control = useContext(ControlContext); let [role, updateRole] = useRole(roleId); let [edited, setEdited] = useState(false); @@ -50,14 +52,20 @@ export default function RoleViewPage() { let nav = [{ name: "Roles", path: "/roles" }, { name: role["name"] || roleId }]; if (role.loading) { return -

{roleId}

+
; } if (role.missing) { return -

Role not found

+

Role with id {roleId} was not found on the master server.

; } @@ -72,6 +80,7 @@ export default function RoleViewPage() { }, }; + let canUpdate = account.hasPermission("core.role.update"); return
- -
-

{role["name"]}

- - - - } + {account.hasPermission("core.role.delete") && { + libLink.messages.deleteRole.send( + control, { id: roleId } + ).then(() => { + history.push("/roles"); + }).catch(notifyErrorHandler("Error deleting role")); + }} + > + - { - libLink.messages.deleteRole.send( - control, { id: roleId } - ).then(() => { - history.push("/roles"); - }).catch(notifyErrorHandler("Error deleting role")); - }} - > - - - - - + } + } + /> - + - +

Permissions

{[...libUsers.permissions.values()].map(({name, title, description}) => ( @@ -141,7 +145,7 @@ export default function RoleViewPage() { labelAlign="left" colon={false} > - + ))} diff --git a/packages/web_ui/src/components/RolesPage.jsx b/packages/web_ui/src/components/RolesPage.jsx index e4a0aca2..388a48f1 100644 --- a/packages/web_ui/src/components/RolesPage.jsx +++ b/packages/web_ui/src/components/RolesPage.jsx @@ -1,50 +1,100 @@ -import React, { useContext } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; +import { Button, Form, Input, Modal, PageHeader, Table } from "antd"; import { libLink } from "@clusterio/lib"; -import DataTable from "./data-table"; +import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; +import { notifyErrorHandler } from "../util/notify"; import PageLayout from "./PageLayout"; -export default function UsersPage() { +function CreateRoleButton() { + let control = useContext(ControlContext); + let history = useHistory(); + let [visible, setVisible] = useState(false); + let [form] = Form.useForm(); + + async function createRole() { + let values = form.getFieldsValue(); + if (!values.roleName) { + form.setFields([{ name: "roleName", errors: ["Name is required"] }]); + return; + } + + let result = await libLink.messages.createRole.send(control, { + name: values.roleName, + description: values.description || "", + permissions: [], + }); + setVisible(false); + history.push(`/roles/${result.id}/view`); + } + + return <> + + { createRole().catch(notifyErrorHandler("Error creating role")); }} + onCancel={() => { setVisible(false); }} + destroyOnClose + > +
+ + + + + + + +
+ ; +} + +export default function RolesPage() { + let account = useAccount(); let control = useContext(ControlContext); let history = useHistory(); - async function listRoles() { - let result = await libLink.messages.listRoles.send(control); - return result["list"].map(item => ({ - key: item["id"], - "Name": item["name"], - "Description": item["description"], - })); - } + let [roles, setRoles] = useState([]); + + useEffect(() => { + libLink.messages.listRoles.send(control).then(result => { + setRoles(result["list"]); + }).catch(notifyErrorHandler("Error fetching role list")); + }, []); return -

Roles

- { - await libLink.messages.createRole.send(control, { - name: args.name, - description: args.description || "", - permissions: [], - }); + } + /> +
({ - onClick: event => { - history.push(`/roles/${record.key}/view`); - }, - }), - }} + { + title: "Description", + dataIndex: "description", + }, + ]} + dataSource={roles} + pagination={false} + rowKey={role => role["id"]} + onRow={(role, rowIndex) => ({ + onClick: event => { + history.push(`/roles/${role["id"]}/view`); + }, + })} /> ; } diff --git a/packages/web_ui/src/components/SavesList.jsx b/packages/web_ui/src/components/SavesList.jsx index 8b61f063..72c0554a 100644 --- a/packages/web_ui/src/components/SavesList.jsx +++ b/packages/web_ui/src/components/SavesList.jsx @@ -5,6 +5,7 @@ import LeftOutlined from "@ant-design/icons/LeftOutlined"; import { libLink } from "@clusterio/lib"; +import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; import CreateSaveModal from "./CreateSaveModal"; import SectionHeader from "./SectionHeader"; @@ -24,6 +25,7 @@ function formatBytes(bytes) { } export default function SavesList(props) { + let account = useAccount(); let control = useContext(ControlContext); let saves = useSaves(props.instance.id); let [starting, setStarting] = useState(false); @@ -65,7 +67,7 @@ export default function SavesList(props) { columnWidth: 33, expandRowByClick: true, expandedRowRender: save => - - } + {account.hasPermission("core.instance.save.download") && - Download} + {account.hasPermission("core.instance.save.delete") && - + } , }} />; @@ -158,15 +160,19 @@ export default function SavesList(props) { }; return
- - + + {account.hasPermission("core.instance.save.upload") && - - - /> - - {saveTable} - + } + {account.hasPermission("core.instance.save.create") && } + } /> + { + account.hasPermission("core.instance.save.upload") + ? + {saveTable} + + : saveTable + } {uploadingFiles.length ? {uploadingFiles.map(file => {file.name} diff --git a/packages/web_ui/src/components/SiteLayout.jsx b/packages/web_ui/src/components/SiteLayout.jsx index 843d3e6b..5da64ebc 100644 --- a/packages/web_ui/src/components/SiteLayout.jsx +++ b/packages/web_ui/src/components/SiteLayout.jsx @@ -69,9 +69,12 @@ export default function SiteLayout(props) { style={{ height: "100%", borderRight: 0 }} onClick={({ key }) => history.push(key)} > - {combinedPages.map(({sidebarName, path}) => ( - sidebarName ? {sidebarName} : null) - )} + {combinedPages.map(({sidebarName, permission, path}) => { + if (!sidebarName || permission && !account.hasPermission(permission)) { + return null; + } + return {sidebarName}; + })} diff --git a/packages/web_ui/src/components/SlavesPage.jsx b/packages/web_ui/src/components/SlavesPage.jsx index 751480e7..15a83f3b 100644 --- a/packages/web_ui/src/components/SlavesPage.jsx +++ b/packages/web_ui/src/components/SlavesPage.jsx @@ -3,6 +3,7 @@ import { Button, Form, Input, Modal, PageHeader, Table } from "antd"; import { libLink } from "@clusterio/lib"; +import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; import PageLayout from "./PageLayout"; import { useSlaveList } from "../model/slave"; @@ -71,14 +72,14 @@ function GenerateSlaveTokenButton(props) { export default function SlavesPage() { - let control = useContext(ControlContext); + let account = useAccount(); let [slaveList] = useSlaveList(); return + extra={account.hasPermission("core.slave.generate_token") && } />
-

{userName}

+ ; } if (user.missing) { return -

User not found

+

User with name {userName} was not found on the master server.

; } + let roleSelector = (rolesError ? Promise.reject(rolesError) : Promise.resolve()) }, + ]} + > + + ; + return - -
-

{userName}

- - - Delete user account and all data associated with it? {(user["is_banned"] && <>
This will remove the user ban!)} @@ -97,40 +129,22 @@ export default function UserViewPage() {
- - + } + /> - (rolesError ? Promise.reject(rolesError) : Promise.resolve()) }, - ]} - > - - + {account.hasPermission("core.user.update_roles") + ? roleSelector + : [...user["roles"]].map(id => { + roles ? (roles.get(id) || { name: id })["name"] : id + }) + } - + {account.hasPermission("core.user.update_roles") && - + } { libLink.messages.setUserAdmin.send(control, { name: userName, @@ -172,6 +187,7 @@ export default function UserViewPage() { { libLink.messages.setUserWhitelisted.send(control, { name: userName, @@ -186,7 +202,7 @@ export default function UserViewPage() { {user["is_banned"] ? "Yes" : "No"} - + content={ @@ -220,7 +236,7 @@ export default function UserViewPage() { >Ban - + } > - + } diff --git a/packages/web_ui/src/components/UsersPage.jsx b/packages/web_ui/src/components/UsersPage.jsx index e70911a5..c8f98376 100644 --- a/packages/web_ui/src/components/UsersPage.jsx +++ b/packages/web_ui/src/components/UsersPage.jsx @@ -1,68 +1,118 @@ import React, { useEffect, useContext, useState } from "react"; import { useHistory } from "react-router-dom"; -import { Spin, Tag } from "antd"; +import { Button, Form, Input, Modal, PageHeader, Table, Tag } from "antd"; import { libLink } from "@clusterio/lib"; -import DataTable from "./data-table"; +import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; +import { notifyErrorHandler } from "../util/notify"; import PageLayout from "./PageLayout"; +function CreateUserButton() { + let control = useContext(ControlContext); + let history = useHistory(); + let [visible, setVisible] = useState(false); + let [form] = Form.useForm(); + + async function createUser() { + let values = form.getFieldsValue(); + if (!values.userName) { + form.setFields([{ name: "userName", errors: ["Name is required"] }]); + return; + } + + await libLink.messages.createUser.send(control, { name: values.userName }); + setVisible(false); + history.push(`/users/${values.userName}/view`); + } + + return <> + + { createUser().catch(notifyErrorHandler("Error creating user")); }} + onCancel={() => { setVisible(false); }} + destroyOnClose + > +
+ + + + +
+ ; +} + export default function UsersPage() { + let account = useAccount(); let control = useContext(ControlContext); let history = useHistory(); - let [roles, setRoles] = useState(null); + let [users, setUsers] = useState([]); + let [roles, setRoles] = useState(new Map()); useEffect(() => { + libLink.messages.listUsers.send(control).then(result => { + setUsers(result["list"]); + }).catch(notifyErrorHandler("Error fetching user list")); libLink.messages.listRoles.send(control).then(result => { setRoles(new Map(result["list"].map(role => [role["id"], role]))); }).catch(() => { - setRoles(new Map()); + // ignore }); }, []); - async function listUsers() { - let result = await libLink.messages.listUsers.send(control); - return result["list"].map(item => ({ - key: item["name"], - "Name": item["name"], - "Roles": item["roles"].map(id => {(roles.get(id) || { name: id })["name"]}), - "Admin": item["is_admin"] && "Yes", - "Whitelisted": item["is_whitelisted"] && "Yes", - "Banned": item["is_banned"] && "Yes", - })); - } - - // DataTable does not make it possible to update the rendering function - if (roles === null) { - return -

Users

- -
; - } - return -

Users

- } + /> +
{ - await libLink.messages.createUser.send(control, { name: args.name }); + dataIndex: "name", }, - }} - TableProps={{ - onRow: (record, rowIndex) => ({ - onClick: event => { - history.push(`/users/${record.key}/view`); - }, - }), - }} + { + title: "Roles", + key: "roles", + render: user => user.roles.map(id => {(roles.get(id) || { name: id })["name"]}), + }, + { + title: "Admin", + key: "admin", + render: user => user["is_admin"] && "yes", + responsive: ["lg"], + }, + { + title: "Whitelisted", + key: "whitelisted", + render: user => user["is_whitelisted"] && "yes", + responsive: ["lg"], + }, + { + title: "Banned", + key: "banned", + render: user => user["is_banned"] && "yes", + responsive: ["lg"], + }, + ]} + dataSource={users} + pagination={false} + rowKey={user => user["name"]} + onRow={(user, rowIndex) => ({ + onClick: event => { + history.push(`/users/${user["name"]}/view`); + }, + })} /> ; } diff --git a/packages/web_ui/src/components/data-table.jsx b/packages/web_ui/src/components/data-table.jsx deleted file mode 100644 index 49c46467..00000000 --- a/packages/web_ui/src/components/data-table.jsx +++ /dev/null @@ -1,281 +0,0 @@ -import React, { Component } from "react"; -import { Collapse, Row, Col, Modal, Table, Form, Input, Button, Popover } from "antd"; -import PlusOutlined from "@ant-design/icons/PlusOutlined"; -import DeleteOutlined from "@ant-design/icons/DeleteOutlined"; - -import notify from "../util/notify"; - -const { Panel } = Collapse; - -/** - * Display data in table. Allows for editing data by clicking rows in the table. - * Basic usage: - * - * DataFunction - String, corresponds to export from util/wslink. Same as ws message name. - * columns - Array of objects, optional, same as
. Note the lowercase c - * Editable - Boolean - Allow editing - * AddRecord - Boolean - Allow adding new records - * CustomDataFilterFunction - function(tableData) - Filter tabledata and return results - * RecordIdField - String - - * By default we assume the column "Id" is a unique identifier for a record. If - * that isn't the case, supply the column name here - */ -class DataTable extends Component { - constructor(props) { - super(props); - if (!this.props.DataFunction) { - throw new Error("DataFunction not defined"); - } - this.state = { - pagination: { - pageSize: 100, - current: 1, - }, - filters: this.props.Filters, - sorter: {}, - tableData: [], - showEditModal: false, - record: {}, - }; - } - - componentDidMount() { - this.getData(); - } - - async getData(pagination, filters, sorter) { - // pagination = { - // pageSize: 10, // items per page - // current: 1, // page number - // total: 123, // Total amount of items - // } - // filters = { - // ReportId: 123 - // } - // sorter = {} - pagination = pagination || this.state.pagination; - filters = filters || this.state.filters; - sorter = sorter || this.state.sorter; - - // eslint-disable-next-line new-cap - let data = await this.props.DataFunction(); - data = data || {}; - this.setState({ - // eslint-disable-next-line new-cap - tableData: this.props.CustomDataFilterFunction ? this.props.CustomDataFilterFunction(data) : data, - pagination: { ...pagination, ...data.pagination }, - sorter, - filters, - }); - } - - showEditModal(record, index) { - if (!this.state.showEditModal) { - this.setState({ - record: record, - showEditModal: true, - }); - } - } - - hideEditModal() { - this.setState({ - showEditModal: false, - inserting: false, - record: {}, - }); - } - - async onEditFinish(values) { - if (this.state.inserting) { // Create new record - await this.props.AddRecord.insert(values); - } else { // Edit existing record, capable of doing sparse updates - await this.props.Editable.insert(values); - } - this.setState({ - showEditModal: false, - inserting: false, - record: {}, - }); - // Force refresh of data - this.getData(); - } - - async deleteRecord(record) { - delete record.key; - // Delete "null" values as they seem to cause deletes to fail silently when - // the value is null and field type is float - for (let key in record) { - if (record[key] === null) { delete record[key]; } - } - let status = {}; - // TODO: Delete data here - - if (status.ok) { notify("success", status.msg, "success"); } - if (!status.ok) { notify("error", status.msg, "error"); } - - // Force refresh of data - this.getData(); - } - - render() { - let tableProps = { - ...this.props.TableProps || {}, - onRow: (record, rowIndex) => { - // To allow for extending functionality, we take a TableProps argument - // from props and pass it through to the Table component. Since we - // are also using onRow internaly here, we handle the externally - // provided events ourselves by passing along the event object. This - // way the end user isn't affected by our event handlers. - let externalOnRow = { - ...( - this.props.TableProps - && this.props.TableProps.onRow - && this.props.TableProps.onRow(record, rowIndex) - ), - }; - return { - onClick: event => { - if (event.target.type !== "button") { - if (this.props.Editable) { this.showEditModal(record, rowIndex); } - if (externalOnRow.onClick) { externalOnRow.onClick(event); } - } - }, - onDoubleClick: event => { - if (externalOnRow.onDoubleClick) { externalOnRow.onDoubleClick(event); } - }, - onContextMenu: event => { - if (externalOnRow.onContextMenu) { externalOnRow.onContextMenu(event); } - }, - onMouseEnter: event => { - if (externalOnRow.onMouseEnter) { externalOnRow.onMouseEnter(event); } - }, - onMouseLeave: event => { - if (externalOnRow.onMouseLeave) { externalOnRow.onMouseLeave(event); } - }, - }; - }, - }; - let columns = this.props.columns || this.inferColumns(this.state.tableData); - if (this.props.DeleteRecord && !columns.find(x => x.dataIndex === "notInUse")) { - columns.push({ - // Add delete button on the right - dataIndex: "notInUse", - title: "", - render: (text, record) => { - this.deleteRecord(record); - }} - > - Slett permanent - - } - trigger="click" - > - - , - }); - } - - return <> - - - -
({ - ...x, // Add a 4th parameter to the stock render function with all of our table data - render: x.render - ? (...originalParams) => x.render(...originalParams, this.state.tableData) - : undefined, - }))} - dataSource={this.state.tableData} - pagination={this.state.pagination.total > 10 ? this.state.pagination : false} - onChange={this.getData.bind(this)} - /> - {this.props.AddRecord && } - - {this.state.inserting ? "Add" : "Edit"} - } - > - {this.state.showEditModal &&
- {( - // Use externally supplied form input fields - (this.state.inserting && this.props.AddRecord && this.props.AddRecord.fields) - // Generate input fields from external columns - || this.props.columns.filter && this.props.columns.filter(x => x.dataIndex !== "notInUse") - // Generate input fields from autogenerated columns (only works with existing data) - || this.inferColumns(this.state.tableData) - ).map(col => - {col.renderEdit && col.renderEdit( - this.state.record[col.dataIndex], - this.state.record, - undefined /* Non spec compliant, is supposed to be index of record */, - this.state.tableData - ) || } - )} - - - - } -
- - ; - } - - inferColumns(data) { - // Look at the data and find columns and how to display them. Can be - // overridden by props.Columns if more customization is needed - let columns = []; - data.forEach(row => { - Object.keys(row).filter(x => x !== "key").forEach(key => { - let existing = columns.find(col => col.key === key); - if (!existing) { - columns.push({ - key: key, - title: key, - dataIndex: key, - }); - } - }); - }); - return columns; - } -} - -export default DataTable; diff --git a/packages/web_ui/src/pages.jsx b/packages/web_ui/src/pages.jsx index 6ffe389b..b643b0e5 100644 --- a/packages/web_ui/src/pages.jsx +++ b/packages/web_ui/src/pages.jsx @@ -13,16 +13,60 @@ import PluginViewPage from "./components/PluginViewPage"; export const pages = [ - { path: "/master", sidebarName: "Master", content: }, - { path: "/slaves", sidebarName: "Slaves", content: }, - { path: "/instances", sidebarName: "Instances", content: }, - { path: "/instances/:id/view", sidebarPath: "/instances", content: }, - { path: "/users", sidebarName: "Users", content: }, - { path: "/users/:name/view", sidebarPath: "/users", content: }, - { path: "/roles", sidebarName: "Roles", content: }, - { path: "/roles/:id/view", sidebarPath: "/roles", content: }, - { path: "/plugins", sidebarName: "Plugins", content: }, - { path: "/plugins/:name/view", sidebarPath: "/plugins", content: }, + { + path: "/master", + sidebarName: "Master", + content: , + }, + { + path: "/slaves", + sidebarName: "Slaves", + permission: "core.slave.list", + content: , + }, + { + path: "/instances", + sidebarName: "Instances", + permission: "core.instance.list", + content: , + }, + { + path: "/instances/:id/view", + sidebarPath: "/instances", + content: , + }, + { + path: "/users", + sidebarName: "Users", + permission: "core.user.list", + content: , + }, + { + path: "/users/:name/view", + sidebarPath: "/users", + content: , + }, + { + path: "/roles", + sidebarName: "Roles", + permission: "core.user.list", + content: , + }, + { + path: "/roles/:id/view", + sidebarPath: "/roles", + content: , + }, + { + path: "/plugins", + sidebarName: "Plugins", + content: , + }, + { + path: "/plugins/:name/view", + sidebarPath: "/plugins", + content: , + }, ]; export const sidebarPages = [ diff --git a/plugins/inventory_sync/web/index.jsx b/plugins/inventory_sync/web/index.jsx index 8261705f..a7fdc6ee 100644 --- a/plugins/inventory_sync/web/index.jsx +++ b/plugins/inventory_sync/web/index.jsx @@ -31,7 +31,12 @@ function InventoryPage() { export class WebPlugin extends libPlugin.BaseWebPlugin { async init() { this.pages = [ - { path: "/inventory", sidebarName: "Inventory sync", content: }, + { + path: "/inventory", + sidebarName: "Inventory sync", + permission: "inventory_sync.inventory.view", + content: , + }, ]; } diff --git a/plugins/subspace_storage/web/index.jsx b/plugins/subspace_storage/web/index.jsx index c6123713..a143dc5c 100644 --- a/plugins/subspace_storage/web/index.jsx +++ b/plugins/subspace_storage/web/index.jsx @@ -117,7 +117,12 @@ function StoragePage() { export class WebPlugin extends libPlugin.BaseWebPlugin { async init() { this.pages = [ - { path: "/storage", sidebarName: "Storage", content: }, + { + path: "/storage", + sidebarName: "Storage", + permission: "subspace_storage.storage.view", + content: , + }, ]; this.storage = new Map();