import { useState, useEffect } from 'react'; import { Plus, Trash2, Pencil, RefreshCw, Users, UserPlus, UserX, Shield } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { PageShell } from '@/components/layout/PageShell'; import { PageEmpty } from '@/components/layout/PageEmpty'; import { StatStrip } from '@/components/ui/stat-strip'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { deployApi } from '@/api'; import type { JsonData } from '@/types/common'; import i18n from '@/i18n'; interface Group { id: number; name: string; description: string; members?: GroupMember[]; created_by: string; created_at: string; } interface GroupMember { id: number; group_id: number; username: string; created_at: string; } export default function UserGroupsManager({ embedded = false }: { embedded?: boolean }) { const _confirm = useConfirm(); const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); const [expanded, setExpanded] = useState>({}); // Group form const [formOpen, setFormOpen] = useState(false); const [editId, setEditId] = useState(null); const [formName, setFormName] = useState(''); const [formDesc, setFormDesc] = useState(''); const [saving, setSaving] = useState(false); // Add member — per-group input state const [memberUsers, setMemberUsers] = useState>({}); const loadGroups = async () => { setLoading(true); try { const res = await deployApi.getGroups(); setGroups((res as JsonData)?.list || []); } catch { toast.error(i18n.t('kubernetes.msg.loadFailed')); } finally { setLoading(false); } }; useEffect(() => { loadGroups(); }, []); const handleSave = async () => { if (!formName.trim()) { toast.error(i18n.t('deploy.rbac.groupPlaceholder')); return; } setSaving(true); try { if (editId) await deployApi.updateGroup(editId, { name: formName, description: formDesc }); else await deployApi.createGroup({ name: formName, description: formDesc }); toast.success(editId ? i18n.t('deploy.toast.updated') : i18n.t('kubernetes.event.created')); setFormOpen(false); loadGroups(); } catch { toast.error(i18n.t('cloud.operationFailed')); } finally { setSaving(false); } }; const handleDelete = async (g: Group) => { const ok = await _confirm({ title: i18n.t('deploy.rbac.deleteGroup'), description: `确定删除「${g.name}」?所有成员和流水线授权将同时移除。`, variant: 'destructive' }); if (!ok) return; try { await deployApi.deleteGroup(g.id); toast.success(i18n.t('monitor.silenceRules.deleted')); loadGroups(); } catch { toast.error(i18n.t('kubernetes.msg.deleteFailed2')); } }; const handleAddMember = async (groupId: number) => { const username = (memberUsers[groupId] || '').trim(); if (!username) { toast.error(i18n.t('pipeline.usernamePlaceholder')); return; } try { await deployApi.addGroupMember(groupId, { username }); toast.success(i18n.t('deploy.toast.added')); loadGroups(); setMemberUsers(prev => ({ ...prev, [groupId]: '' })); } catch { toast.error(i18n.t('monitor.dashboard.custom.toastPanelFailed')); } }; const handleRemoveMember = async (groupId: number, memberId: number, username: string) => { const ok = await _confirm({ title: i18n.t('pipeline.removeMember'), description: `确定从组中移除「${username}」?` }); if (!ok) return; try { await deployApi.removeGroupMember(groupId, memberId); toast.success(i18n.t('deploy.toast.removed')); loadGroups(); } catch { toast.error(i18n.t('deploy.toast.removeFailed')); } }; const toggleExpand = (gid: number) => setExpanded(p => ({ ...p, [gid]: !p[gid] })); const memberCount = groups.reduce((sum, g) => sum + (g.members || []).length, 0); const toolbar = embedded ? (
) : null; const listBody = !loading && groups.length === 0 ? ( } title={i18n.t('deploy.userGroup.noGroups')} description={i18n.t('deploy.userGroup.createFirstHint')} action={( )} /> ) : (
{groups.map(g => (
toggleExpand(g.id)}>
{g.name}
{g.description || i18n.t('system.roleDescFallback')} · {(g.members || []).length} 位成员
{expanded[g.id] && (
{/* Add member */}
setMemberUsers(prev => ({ ...prev, [g.id]: e.target.value }))} placeholder={i18n.t('pipeline.usernamePlaceholder')} className="h-8 text-sm" onKeyDown={e => e.key === 'Enter' && handleAddMember(g.id)} />
{/* Members list */} {(g.members || []).length === 0 ? (
{i18n.t('deploy.rbac.noMembers')}
) : (
{(g.members || []).map(m => ( {m.username} ))}
)}
)}
))}
); const formDialog = ( {editId ? i18n.t('deploy.rbac.groupEditTitle') : i18n.t('pipeline.newGroup')}
setFormName(e.target.value)} placeholder={i18n.t('deploy.userGroup.namePlaceholder')} />
setFormDesc(e.target.value)} placeholder={i18n.t('rbac.jobRole.placeholder.description')} />
); if (embedded) { return ( <> {toolbar} {listBody} {formDialog} ); } return ( } loading={loading} metrics={( )} actions={( <> )} contentClassName="space-y-4" > {listBody} {formDialog} ); }