diff --git a/packages/web_ui/src/components/RoleViewPage.tsx b/packages/web_ui/src/components/RoleViewPage.tsx index ddd18535..9ff44c34 100644 --- a/packages/web_ui/src/components/RoleViewPage.tsx +++ b/packages/web_ui/src/components/RoleViewPage.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useContext, useState, useMemo } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { Button, Checkbox, Input, Popconfirm, Space, Spin, Tooltip } from "antd"; +import { Button, Checkbox, Col, Grid, Input, Popconfirm, Row, Space, Spin, Tooltip } from "antd"; import { ExclamationCircleOutlined, StarOutlined, DeleteOutlined} from "@ant-design/icons"; import * as lib from "@clusterio/lib"; @@ -109,7 +109,9 @@ function PermissionGroup({ } return ( -
+
{formatGroupTitle(groupName)} @@ -134,6 +136,30 @@ function PermissionGroup({ ); } +/** An entry in the group list; the dot is blue when any permission in the group is granted */ +function PermissionGroupNav({ groupName, granted }: { groupName: string; granted: boolean }) { + function scrollToGroup() { + document.getElementById(`permission-group-${groupName}`)?.scrollIntoView({ behavior: "smooth" }); + } + + return ( +
+ + {formatGroupTitle(groupName)} +
+ ); +} + /** The role page view */ export default function RoleViewPage() { const params = useParams(); @@ -149,6 +175,9 @@ export default function RoleViewPage() { const [permissionState, setPermissionState] = useState>({}); const [baselineState, setBaselineState] = useState>({}); const [search, setSearch] = useState(""); + // Hidden below lg, where the site sider leaves too little room; undefined until measured + const screens = Grid.useBreakpoint(); + const showGroupList = screens.lg !== false; const allPermissions = useMemo(() => [...lib.permissions.values()], []); const canUpdate = Boolean(account.hasPermission("core.role.update")); @@ -209,6 +238,9 @@ export default function RoleViewPage() { permissions.sort((a, b) => a.title.localeCompare(b.title)); } + // Groups with at least one granted permission, including those hidden by the search + const grantedGroups = new Set(allPermissions.filter(p => permissionState[p.name]).map(p => getGroupName(p.name))); + // Check if any changes have been made to the permissions or role metadata const permissionChanged = Object.keys(permissionState).some(k => permissionState[k] !== baselineState[k]); const metaChanged = name !== role.name || description !== role.description; @@ -302,19 +334,39 @@ export default function RoleViewPage() { setSearch(e.target.value)} style={{ marginBottom: 16 }} /> - - {sortedGroups.map(([groupName, permissions]) => ( - - ))} - + + {showGroupList && + {/* Clear of the fixed page header */} +
+ {sortedGroups.map(([groupName]) => ( + + ))} +
+ } + + + {sortedGroups.map(([groupName, permissions]) => ( + + ))} + + +