From 2b272d230c17fd9a4c5dcfdaf6544a699dc6397d Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:56:33 +0000 Subject: [PATCH 1/2] Add a permission group list to the role page A sticky list of permission groups sits left of the permissions. Clicking an entry scrolls to that group, and its dot is blue when any permission in the group is granted, grey otherwise. Same layout as the ExpCluster scenario features page. Co-Authored-By: Claude Opus 5.5 --- .../web_ui/src/components/RoleViewPage.tsx | 75 +++++++++++++++---- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/packages/web_ui/src/components/RoleViewPage.tsx b/packages/web_ui/src/components/RoleViewPage.tsx index ddd18535..ce6fa2f1 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, 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(); @@ -209,6 +235,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 +331,35 @@ export default function RoleViewPage() { setSearch(e.target.value)} style={{ marginBottom: 16 }} /> - - {sortedGroups.map(([groupName, permissions]) => ( - - ))} - + + + {/* Clear of the fixed page header */} +
+ {sortedGroups.map(([groupName]) => ( + + ))} +
+ + + + {sortedGroups.map(([groupName, permissions]) => ( + + ))} + + +
From 0684f4719e6a7dd28f52fb70e80a06d6b5c62ce6 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:49:37 +0000 Subject: [PATCH 2/2] Hide the permission group list on narrow screens and let it scroll Below the lg breakpoint the list is not rendered and the permission groups take the full width. At md the site sider already takes 250px, so a 200px list next to it left too little room for the permissions. The list keeps its sticky position but is capped at the viewport height minus its top offset and scrolls on its own when there are more groups than fit. Co-Authored-By: Claude Opus 5.5 --- packages/web_ui/src/components/RoleViewPage.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/web_ui/src/components/RoleViewPage.tsx b/packages/web_ui/src/components/RoleViewPage.tsx index ce6fa2f1..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, Col, Input, Popconfirm, Row, 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"; @@ -175,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")); @@ -332,9 +335,13 @@ export default function RoleViewPage() { onChange={e => setSearch(e.target.value)} style={{ marginBottom: 16 }} /> - + {showGroupList && {/* Clear of the fixed page header */} -
+
{sortedGroups.map(([groupName]) => ( ))}
- + } {sortedGroups.map(([groupName, permissions]) => (