feat: add Role Editor page

This commit is contained in:
Zhi
2026-03-12 11:45:30 +00:00
parent bfaf9469e1
commit f6460e2d70
3 changed files with 189 additions and 1 deletions

View File

@@ -13,6 +13,7 @@ import ProjectDetailPage from '@/pages/ProjectDetailPage'
import MilestonesPage from '@/pages/MilestonesPage' import MilestonesPage from '@/pages/MilestonesPage'
import MilestoneDetailPage from '@/pages/MilestoneDetailPage' import MilestoneDetailPage from '@/pages/MilestoneDetailPage'
import NotificationsPage from '@/pages/NotificationsPage' import NotificationsPage from '@/pages/NotificationsPage'
import RoleEditorPage from '@/pages/RoleEditorPage'
import MonitorPage from '@/pages/MonitorPage' import MonitorPage from '@/pages/MonitorPage'
import axios from 'axios' import axios from 'axios'
@@ -66,6 +67,7 @@ export default function App() {
<Sidebar user={null} onLogout={logout} /> <Sidebar user={null} onLogout={logout} />
<main className="main-content"> <main className="main-content">
<Routes> <Routes>
<Route path="/roles" element={<RoleEditorPage />} />
<Route path="/monitor" element={<MonitorPage />} /> <Route path="/monitor" element={<MonitorPage />} />
<Route path="/login" element={<LoginPage onLogin={login} />} /> <Route path="/login" element={<LoginPage onLogin={login} />} />
<Route path="*" element={<Navigate to="/monitor" />} /> <Route path="*" element={<Navigate to="/monitor" />} />
@@ -91,6 +93,7 @@ export default function App() {
<Route path="/milestones" element={<MilestonesPage />} /> <Route path="/milestones" element={<MilestonesPage />} />
<Route path="/milestones/:id" element={<MilestoneDetailPage />} /> <Route path="/milestones/:id" element={<MilestoneDetailPage />} />
<Route path="/notifications" element={<NotificationsPage />} /> <Route path="/notifications" element={<NotificationsPage />} />
<Route path="/roles" element={<RoleEditorPage />} />
<Route path="/monitor" element={<MonitorPage />} /> <Route path="/monitor" element={<MonitorPage />} />
<Route path="*" element={<Navigate to="/" />} /> <Route path="*" element={<Navigate to="/" />} />
</Routes> </Routes>

View File

@@ -34,6 +34,7 @@ export default function Sidebar({ user, onLogout }: Props) {
{ to: '/projects', icon: '📁', label: 'Projects' }, { to: '/projects', icon: '📁', label: 'Projects' },
{ to: '/notifications', icon: '🔔', label: 'Notifications' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') }, { to: '/notifications', icon: '🔔', label: 'Notifications' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') },
{ to: '/monitor', icon: '📡', label: 'Monitor' }, { to: '/monitor', icon: '📡', label: 'Monitor' },
...(user.is_admin ? [{ to: '/roles', icon: '🔐', label: 'Roles' }] : []),
] : [ ] : [
{ to: '/monitor', icon: '📡', label: 'Monitor' }, { to: '/monitor', icon: '📡', label: 'Monitor' },
] ]

View File

@@ -0,0 +1,184 @@
import { useState, useEffect } from 'react'
import api from '@/services/api'
interface Permission {
id: number
name: string
description: string
category: string
}
interface Role {
id: number
name: string
description: string
is_global: boolean
permission_ids: number[]
}
export default function RoleEditorPage() {
const [roles, setRoles] = useState<Role[]>([])
const [permissions, setPermissions] = useState<Permission[]>([])
const [selectedRole, setSelectedRole] = useState<Role | null>(null)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState('')
useEffect(() => {
fetchData()
}, [])
const fetchData = async () => {
try {
const [rolesRes, permsRes] = await Promise.all([
api.get('/roles'),
api.get('/roles/permissions')
])
setRoles(rolesRes.data)
setPermissions(permsRes.data)
} catch (err) {
console.error('Failed to fetch data:', err)
} finally {
setLoading(false)
}
}
const handlePermissionToggle = (permId: number) => {
if (!selectedRole) return
const newPermIds = selectedRole.permission_ids.includes(permId)
? selectedRole.permission_ids.filter(id => id !== permId)
: [...selectedRole.permission_ids, permId]
setSelectedRole({ ...selectedRole, permission_ids: newPermIds })
}
const handleSave = async () => {
if (!selectedRole) return
setSaving(true)
setMessage('')
try {
await api.post(`/roles/${selectedRole.id}/permissions`, {
permission_ids: selectedRole.permission_ids
})
setMessage('Saved successfully!')
fetchData()
} catch (err: any) {
setMessage(err.response?.data?.detail || 'Failed to save')
} finally {
setSaving(false)
}
}
const groupedPermissions = permissions.reduce((acc, p) => {
if (!acc[p.category]) acc[p.category] = []
acc[p.category].push(p)
return acc
}, {} as Record<string, Permission[]>)
if (loading) return <div className="p-4">Loading...</div>
return (
<div className="role-editor-page" style={{ padding: '20px' }}>
<h2>🔐 Role Editor</h2>
<p style={{ color: '#888', marginBottom: '20px' }}>
Configure permissions for each role. Only admins can edit roles.
</p>
{message && (
<div style={{
padding: '10px',
marginBottom: '20px',
backgroundColor: message.includes('success') ? '#d4edda' : '#f8d7da',
borderRadius: '4px'
}}>
{message}
</div>
)}
<div style={{ display: 'flex', gap: '20px' }}>
{/* Role List */}
<div style={{ width: '250px' }}>
<h3>Roles</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{roles.map(role => (
<div
key={role.id}
onClick={() => setSelectedRole({ ...role })}
style={{
padding: '12px',
border: selectedRole?.id === role.id ? '2px solid #007bff' : '1px solid #ddd',
borderRadius: '6px',
cursor: 'pointer',
backgroundColor: selectedRole?.id === role.id ? '#f0f8ff' : 'white'
}}
>
<strong>{role.name}</strong>
{role.is_global && <span style={{ fontSize: '12px', marginLeft: '8px', color: '#ff6b6b' }}>🌟</span>}
<div style={{ fontSize: '12px', color: '#666' }}>{role.description}</div>
<div style={{ fontSize: '11px', color: '#999', marginTop: '4px' }}>
{role.permission_ids.length} permissions
</div>
</div>
))}
</div>
</div>
{/* Permission Editor */}
<div style={{ flex: 1 }}>
{selectedRole ? (
<>
<h3>Permissions for: {selectedRole.name}</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{Object.entries(groupedPermissions).map(([category, perms]) => (
<div key={category} style={{
border: '1px solid #eee',
borderRadius: '8px',
padding: '12px'
}}>
<h4 style={{ margin: '0 0 10px 0', textTransform: 'capitalize' }}>
{category}
</h4>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '8px' }}>
{perms.map(perm => (
<label key={perm.id} style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
cursor: 'pointer',
padding: '6px',
borderRadius: '4px',
backgroundColor: selectedRole.permission_ids.includes(perm.id) ? '#e8f5e9' : 'transparent'
}}>
<input
type="checkbox"
checked={selectedRole.permission_ids.includes(perm.id)}
onChange={() => handlePermissionToggle(perm.id)}
/>
<div>
<div style={{ fontWeight: 500 }}>{perm.name}</div>
<div style={{ fontSize: '11px', color: '#666' }}>{perm.description}</div>
</div>
</label>
))}
</div>
</div>
))}
</div>
<button
onClick={handleSave}
disabled={saving}
className="btn-primary"
style={{ marginTop: '20px' }}
>
{saving ? 'Saving...' : 'Save Changes'}
</button>
</>
) : (
<div style={{ color: '#888', textAlign: 'center', marginTop: '40px' }}>
Select a role to edit its permissions
</div>
)}
</div>
</div>
</div>
)
}