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 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5.5 committed 2026-10-04 23:48:06 +00:00
1 parent 7b2f7064b9
commit 2b272d230c
1 file changed
+60 -15
+60 -15
View File
@@ -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 (
<div style={{ border: "1px solid #424242", padding: 12, borderRadius: 6 }}>
<div id={`permission-group-${groupName}`} style={{
border: "1px solid #424242", padding: 12, borderRadius: 6, scrollMarginTop: 80,
}}>
<Space style={{ width: "100%", justifyContent: "space-between" }}>
<strong>{formatGroupTitle(groupName)}</strong>
<Space>
@@ -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 (
<div
role="button"
onClick={scrollToGroup}
style={{ display: "flex", alignItems: "center", gap: 8, padding: "4px 8px", cursor: "pointer" }}
>
<span style={{
width: 8,
height: 8,
borderRadius: "50%",
flexShrink: 0,
background: granted ? "#1677ff" : "#595959",
}} />
{formatGroupTitle(groupName)}
</div>
);
}
/** 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() {
<Input.Search placeholder="Search permissions" allowClear
onChange={e => setSearch(e.target.value)} style={{ marginBottom: 16 }} />
<Space direction="vertical" style={{ width: "100%" }}>
{sortedGroups.map(([groupName, permissions]) => (
<PermissionGroup
key={groupName}
groupName={groupName}
permissions={permissions}
permissionState={permissionState}
baselineState={baselineState}
setPermissionState={setPermissionState}
canUpdate={canUpdate}
/>
))}
</Space>
<Row gutter={16} wrap={false}>
<Col flex="200px">
{/* Clear of the fixed page header */}
<div style={{ position: "sticky", top: 80 }}>
{sortedGroups.map(([groupName]) => (
<PermissionGroupNav
key={groupName}
groupName={groupName}
granted={grantedGroups.has(groupName)}
/>
))}
</div>
</Col>
<Col flex="auto" style={{ minWidth: 0 }}>
<Space direction="vertical" style={{ width: "100%" }}>
{sortedGroups.map(([groupName, permissions]) => (
<PermissionGroup
key={groupName}
groupName={groupName}
permissions={permissions}
permissionState={permissionState}
baselineState={baselineState}
setPermissionState={setPermissionState}
canUpdate={canUpdate}
/>
))}
</Space>
</Col>
</Row>
<PluginExtra component="RoleViewPage" role={role} search={search}/>
</PageLayout>