282 lines
7.1 KiB
TypeScript
282 lines
7.1 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Stack,
|
|
Group,
|
|
Checkbox,
|
|
Text,
|
|
Box,
|
|
Collapse,
|
|
ActionIcon,
|
|
Paper,
|
|
} from '@mantine/core';
|
|
import { IconChevronDown, IconChevronRight } from '@tabler/icons-react';
|
|
|
|
interface PermissionNode {
|
|
id: string;
|
|
label: string;
|
|
description?: string;
|
|
children?: PermissionNode[];
|
|
}
|
|
|
|
interface PermissionTreeProps {
|
|
permissions: string[];
|
|
onChange: (permissions: string[]) => void;
|
|
}
|
|
|
|
const permissionHierarchy: PermissionNode[] = [
|
|
{
|
|
id: 'app',
|
|
label: 'Application',
|
|
description: 'Access to application management',
|
|
children: [
|
|
{
|
|
id: 'app.read',
|
|
label: 'Read',
|
|
description: 'Read application information',
|
|
},
|
|
{
|
|
id: 'app.write',
|
|
label: 'Write',
|
|
description: 'Create and update applications',
|
|
},
|
|
{
|
|
id: 'app.delete',
|
|
label: 'Delete',
|
|
description: 'Delete applications',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'token',
|
|
label: 'Token',
|
|
description: 'Access to token management',
|
|
children: [
|
|
{
|
|
id: 'token.read',
|
|
label: 'Read',
|
|
description: 'Read token information',
|
|
},
|
|
{
|
|
id: 'token.create',
|
|
label: 'Create',
|
|
description: 'Create new tokens',
|
|
},
|
|
{
|
|
id: 'token.revoke',
|
|
label: 'Revoke',
|
|
description: 'Revoke existing tokens',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'repo',
|
|
label: 'Repository',
|
|
description: 'Access to repository operations',
|
|
children: [
|
|
{
|
|
id: 'repo.read',
|
|
label: 'Read',
|
|
description: 'Read repository data',
|
|
},
|
|
{
|
|
id: 'repo.write',
|
|
label: 'Write',
|
|
description: 'Write to repositories',
|
|
},
|
|
{
|
|
id: 'repo.admin',
|
|
label: 'Admin',
|
|
description: 'Administrative access to repositories',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'permission',
|
|
label: 'Permission',
|
|
description: 'Access to permission management',
|
|
children: [
|
|
{
|
|
id: 'permission.read',
|
|
label: 'Read',
|
|
description: 'Read permission information',
|
|
},
|
|
{
|
|
id: 'permission.write',
|
|
label: 'Write',
|
|
description: 'Create and update permissions',
|
|
},
|
|
{
|
|
id: 'permission.grant',
|
|
label: 'Grant',
|
|
description: 'Grant permissions to tokens',
|
|
},
|
|
{
|
|
id: 'permission.revoke',
|
|
label: 'Revoke',
|
|
description: 'Revoke permissions from tokens',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const PermissionTree: React.FC<PermissionTreeProps> = ({ permissions, onChange }) => {
|
|
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
|
|
|
// Expand all nodes by default
|
|
useEffect(() => {
|
|
const allParentIds = permissionHierarchy.map(node => node.id);
|
|
setExpandedNodes(new Set(allParentIds));
|
|
}, []);
|
|
|
|
const toggleExpanded = (nodeId: string) => {
|
|
setExpandedNodes(prev => {
|
|
const newSet = new Set(prev);
|
|
if (newSet.has(nodeId)) {
|
|
newSet.delete(nodeId);
|
|
} else {
|
|
newSet.add(nodeId);
|
|
}
|
|
return newSet;
|
|
});
|
|
};
|
|
|
|
const getNodeState = (node: PermissionNode): 'checked' | 'indeterminate' | 'unchecked' => {
|
|
if (!node.children) {
|
|
// For leaf nodes, check if they're explicitly selected
|
|
return permissions.includes(node.id) ? 'checked' : 'unchecked';
|
|
}
|
|
|
|
// For parent nodes, check how many children are selected
|
|
const checkedChildren = node.children.filter(child =>
|
|
permissions.includes(child.id)
|
|
);
|
|
|
|
if (checkedChildren.length === 0) {
|
|
return 'unchecked';
|
|
} else if (checkedChildren.length === node.children.length) {
|
|
return 'checked';
|
|
} else {
|
|
return 'indeterminate';
|
|
}
|
|
};
|
|
|
|
const handleNodeChange = (node: PermissionNode, checked: boolean) => {
|
|
let newPermissions = [...permissions];
|
|
|
|
if (!node.children) {
|
|
// Leaf node
|
|
if (checked) {
|
|
if (!newPermissions.includes(node.id)) {
|
|
newPermissions.push(node.id);
|
|
}
|
|
} else {
|
|
newPermissions = newPermissions.filter(p => p !== node.id);
|
|
}
|
|
} else {
|
|
// Parent node - add/remove all child permissions individually
|
|
if (checked) {
|
|
// Add all child permissions
|
|
node.children.forEach(child => {
|
|
if (!newPermissions.includes(child.id)) {
|
|
newPermissions.push(child.id);
|
|
}
|
|
});
|
|
} else {
|
|
// Remove all child permissions
|
|
node.children.forEach(child => {
|
|
newPermissions = newPermissions.filter(p => p !== child.id);
|
|
});
|
|
}
|
|
}
|
|
|
|
onChange(newPermissions);
|
|
};
|
|
|
|
const renderNode = (node: PermissionNode, depth = 0): React.ReactNode => {
|
|
const state = getNodeState(node);
|
|
const isExpanded = expandedNodes.has(node.id);
|
|
const hasChildren = node.children && node.children.length > 0;
|
|
|
|
return (
|
|
<Box key={node.id}>
|
|
<Paper
|
|
p="xs"
|
|
radius="sm"
|
|
style={{
|
|
border: '1px solid var(--mantine-color-gray-3)',
|
|
marginBottom: '2px',
|
|
}}
|
|
>
|
|
<Group gap="xs" wrap="nowrap" align="flex-start">
|
|
{hasChildren ? (
|
|
<ActionIcon
|
|
variant="subtle"
|
|
size="sm"
|
|
onClick={() => toggleExpanded(node.id)}
|
|
style={{ marginTop: '1px' }}
|
|
>
|
|
{isExpanded ? (
|
|
<IconChevronDown size={14} />
|
|
) : (
|
|
<IconChevronRight size={14} />
|
|
)}
|
|
</ActionIcon>
|
|
) : (
|
|
<Box w={28} /> // Spacer for alignment
|
|
)}
|
|
|
|
<Checkbox
|
|
checked={state === 'checked'}
|
|
indeterminate={state === 'indeterminate'}
|
|
onChange={(event) => handleNodeChange(node, event.currentTarget.checked)}
|
|
size="sm"
|
|
style={{ marginTop: '1px' }}
|
|
/>
|
|
|
|
<Box style={{ flex: 1 }}>
|
|
<Group gap="xs" wrap="nowrap">
|
|
<Text
|
|
size="sm"
|
|
fw={hasChildren ? 600 : 500}
|
|
>
|
|
{node.label}
|
|
{hasChildren && ' (all)'}
|
|
</Text>
|
|
{node.description && (
|
|
<Text
|
|
size="xs"
|
|
c="dimmed"
|
|
style={{ whiteSpace: 'nowrap' }}
|
|
>
|
|
- {node.description}
|
|
</Text>
|
|
)}
|
|
</Group>
|
|
</Box>
|
|
</Group>
|
|
</Paper>
|
|
|
|
{hasChildren && (
|
|
<Collapse in={isExpanded}>
|
|
<Box pl="lg" mt="2px">
|
|
<Stack gap="2px">
|
|
{node.children!.map(child => renderNode(child, depth + 1))}
|
|
</Stack>
|
|
</Box>
|
|
</Collapse>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Stack gap="xs">
|
|
<Text size="sm" fw={500} mb="xs">
|
|
Select permissions for this token
|
|
</Text>
|
|
{permissionHierarchy.map(node => renderNode(node))}
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
export default PermissionTree; |