import { useState, useEffect, useMemo } from 'react'; import { Shield, Plus, Trash2, Loader2, Users, GitBranch, UserPlus, Check, X } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Label } from '@/components/ui/label'; import { toast } from 'sonner'; import { deployApi, userApi, pipelineApi } from '@/api'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { PageShell } from '@/components/layout/PageShell'; import { PageEmpty } from '@/components/layout/PageEmpty'; import { useSearchParams } from 'react-router-dom'; import UserGroupsManager from './UserGroupsManager'; import { JobRoleCreateDialog } from './deploy-rbac/JobRoleCreateDialog'; import { matchPipelines, pipelineDisplayLabel, type PipelineMatchItem } from './deploy-rbac/matchPipeline'; import type { JsonData } from '@/types/common'; interface ItemRole { id: number; name: string; code: string; pattern: string; role: string; description: string; status: number; created_at: string; } interface AssignedUser { id: number; user_id: number; item_role_id: number; item_role?: ItemRole; } const ROLE_LABEL: Record = { owner: '拥有者', editor: '编辑者', viewer: '查看者', }; const RBAC_TABS = ['roles', 'users', 'preview', 'groups'] as const; // 发布 Job 权限 — 正则匹配 code / job_name,Jenkins Role Strategy 风格 // Logan.Li export default function DeployRBAC() { const [searchParams] = useSearchParams(); const tabFromUrl = searchParams.get('tab'); const [roles, setRoles] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [activeTab, setActiveTab] = useState( tabFromUrl && (RBAC_TABS as readonly string[]).includes(tabFromUrl) ? tabFromUrl : 'roles', ); const [pipelines, setPipelines] = useState([]); const [previewRoleId, setPreviewRoleId] = useState(null); const [users, setUsers] = useState([]); const [assignments, setAssignments] = useState([]); const [selectedUserId, setSelectedUserId] = useState(''); const [assignRoleId, setAssignRoleId] = useState(''); const [assignLoading, setAssignLoading] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [newName, setNewName] = useState(''); const [newCode, setNewCode] = useState(''); const [newPattern, setNewPattern] = useState(''); const [newRole, setNewRole] = useState('editor'); const [newDesc, setNewDesc] = useState(''); const [editing, setEditing] = useState(null); const [editPattern, setEditPattern] = useState(''); const [editRole, setEditRole] = useState(''); const _confirm = useConfirm(); const loadData = async () => { setLoading(true); try { const [rolesRes, pipesRes, usersRes] = await Promise.all([ deployApi.getItemRoles(), pipelineApi.getPipelines({ page_size: 200 }), userApi.getUsers({ page_size: 500 }), ]); setRoles(Array.isArray(rolesRes) ? rolesRes : (rolesRes as JsonData)?.list || []); setPipelines(Array.isArray(pipesRes) ? pipesRes : (pipesRes as JsonData)?.list || []); setUsers(Array.isArray(usersRes) ? usersRes : (usersRes as JsonData)?.list || []); } catch { /* silent */ } finally { setLoading(false); } }; useEffect(() => { loadData(); }, []); const loadAssignments = async () => { if (!selectedUserId) { setAssignments([]); return; } try { const res: JsonData = await deployApi.getUserItemRoles(Number(selectedUserId)); setAssignments(Array.isArray(res) ? res : res?.list || []); } catch { setAssignments([]); } }; useEffect(() => { loadAssignments(); }, [selectedUserId]); const matchCount = useMemo(() => { const map: Record = {}; roles.forEach((r) => { map[r.id] = matchPipelines(r.pattern, pipelines); }); return map; }, [roles, pipelines]); const previewMatches = previewRoleId ? (matchCount[previewRoleId] || []) : pipelines; const handleCreate = async () => { if (!newName.trim() || !newCode.trim() || !newPattern.trim()) { toast.error('请填写名称、标识与正则'); return; } setSaving(true); try { await deployApi.createItemRole({ name: newName.trim(), code: newCode.trim(), pattern: newPattern.trim(), role: newRole, description: newDesc, }); toast.success('已创建'); setCreateOpen(false); setNewName(''); setNewCode(''); setNewPattern(''); setNewRole('editor'); setNewDesc(''); loadData(); } catch (e: unknown) { toast.error((e as Error)?.message || '创建失败'); } finally { setSaving(false); } }; const handleUpdate = async (id: number) => { if (!editPattern.trim()) { toast.error('正则不能为空'); return; } setSaving(true); try { await deployApi.updateItemRole(id, { pattern: editPattern, role: editRole }); toast.success('已更新'); setEditing(null); loadData(); } catch (e: unknown) { toast.error((e as Error)?.message || '更新失败'); } finally { setSaving(false); } }; const handleDelete = async (role: ItemRole) => { const ok = await _confirm({ title: '删除权限', description: `确定删除「${role.name}」?将影响 ${matchCount[role.id]?.length || 0} 个发布任务`, variant: 'destructive', }); if (!ok) return; try { await deployApi.deleteItemRole(role.id); toast.success('已删除'); loadData(); } catch { toast.error('删除失败'); } }; const handleToggleStatus = async (role: ItemRole) => { try { await deployApi.updateItemRole(role.id, { status: role.status ? 0 : 1 }); toast.success(role.status ? '已禁用' : '已启用'); loadData(); } catch { toast.error('操作失败'); } }; const handleAssign = async () => { if (!selectedUserId || !assignRoleId) { toast.error('请选择用户和权限'); return; } setAssignLoading(true); try { await deployApi.assignItemRole(Number(selectedUserId), { user_id: Number(selectedUserId), item_role_id: Number(assignRoleId), }); toast.success('已分配'); setAssignRoleId(''); loadAssignments(); } catch (e: unknown) { toast.error((e as Error)?.message || '分配失败'); } finally { setAssignLoading(false); } }; const handleRevoke = async (id: number) => { try { await deployApi.removeItemRole(Number(selectedUserId), id); toast.success('已撤销'); loadAssignments(); } catch { toast.error('撤销失败'); } }; const selectedUser = users.find((u) => String(u.id) === selectedUserId); const pageTabs = [ { id: 'roles', label: '权限策略', badge: roles.length || undefined }, { id: 'users', label: '用户授权' }, { id: 'groups', label: '用户组' }, { id: 'preview', label: '命中预览' }, ]; return ( } loading={loading} tabs={pageTabs} activeTab={activeTab} onTabChange={setActiveTab} actions={activeTab === 'groups' ? undefined : ( )} contentClassName="space-y-4" > {activeTab === 'roles' && ( roles.length === 0 ? ( } title="暂无 Job 权限策略" description="创建策略后,用户将按正则自动获得匹配流水线的访问权限" action={} /> ) : (
{roles.map((role) => (
{role.name} {ROLE_LABEL[role.role] || role.role} {role.status === 0 && 已禁用} { setActiveTab('preview'); setPreviewRoleId(role.id); }} > {matchCount[role.id]?.length || 0} 个 Job

{role.pattern} {role.description ? ` · ${role.description}` : ''}

{editing === role.id ? ( <> setEditPattern(e.target.value)} className="h-7 w-32 text-xs font-mono dg-input" /> ) : ( <> )}
))}
) )} {activeTab === 'users' && (
{selectedUser ? ( assignments.length === 0 ? (

该用户尚未分配 Job 权限

) : (
{assignments.map((a) => { const roleInfo = a.item_role || roles.find((r) => r.id === a.item_role_id); return (
{ROLE_LABEL[roleInfo?.role || ''] || roleInfo?.role} {roleInfo?.name} {roleInfo?.pattern}
); })}
) ) : (

选择用户后查看已分配权限

)}
)} {activeTab === 'groups' && } {activeTab === 'preview' && (
{roles.map((r) => ( ))}
{previewMatches.map((p) => ( {pipelineDisplayLabel(p)} ))} {previewRoleId !== null && previewMatches.length === 0 && ( 无匹配 — 请调整正则 )}
)}
); }