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]) => (
+
+ ))}
+
+
+