import { useState, useEffect, useCallback, useMemo } from 'react'; import { Plus, Search, Edit2, Trash2, Server, Users, ChevronRight, Eye, PenLine, ChevronLeft, ChevronRight as ChevRight, X, } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { instanceGroupApi, type InstanceGroup } from '@/api/instanceGroup'; import { instanceApi } from '@/api/instance'; import { userApi } from '@/api/user'; import { useActionPermission } from '@/hooks/useAuth'; import { ConfirmDialog } from '@/components/ConfirmDialog'; import { ModalOverlay } from '@/components/ModalOverlay'; /* ───────── Constants ───────── */ const DB_TYPE_COLORS: Record = { mysql: 'bg-[rgba(0,117,143,0.15)] text-[#26a69a]', pg: 'bg-[rgba(51,103,145,0.15)] text-[#42a5f5]', postgres: 'bg-[rgba(51,103,145,0.15)] text-[#42a5f5]', redis: 'bg-[rgba(220,50,32,0.15)] text-[#ef5350]', mongo: 'bg-[rgba(71,162,72,0.15)] text-[#66bb6a]', }; /* ───────── Transfer Component ───────── */ interface TransferItem { label: string; value: string; tag?: string; tagColor?: string; accessMode?: string } interface TransferProps { title: string; items: TransferItem[]; selected: string[]; onChange: (v: string[]) => void; emptyText?: string; } function Transfer({ title, items, selected, onChange, emptyText = '请从左侧选择实例' }: TransferProps) { const [search, setSearch] = useState(''); const [pendingAdd, setPendingAdd] = useState>(new Set()); const [pendingRemove, setPendingRemove] = useState>(new Set()); const unselected = items.filter(i => !selected.includes(i.value) && (i.label.toLowerCase().includes(search.toLowerCase()) || i.value.includes(search)) ); const selectedFiltered = items.filter(i => selected.includes(i.value) && (i.label.toLowerCase().includes(search.toLowerCase()) || i.value.includes(search)) ); const addSelected = () => { onChange([...selected, ...pendingAdd]); setPendingAdd(new Set()); }; const removeSelected = () => { onChange(selected.filter(v => !pendingRemove.has(v))); setPendingRemove(new Set()); }; const toggleAdd = (v: string) => setPendingAdd(p => { const n = new Set(p); n.has(v) ? n.delete(v) : n.add(v); return n; }); const toggleRemove = (v: string) => setPendingRemove(p => { const n = new Set(p); n.has(v) ? n.delete(v) : n.add(v); return n; }); const row = (active: boolean, color: string) => `flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer text-xs transition-colors ${active ? color : 'hover:bg-[var(--dg-bg-hover)]'}`; return (
可选实例 {unselected.length}
setSearch(e.target.value)} />
{unselected.length === 0 &&
无可用项
} {unselected.map(item => ( ))}
{title} {selectedFiltered.length}
{selectedFiltered.length === 0 &&
{emptyText}
} {selectedFiltered.map(item => ( ))}
); } /* ───────── Main ───────── */ export default function InstanceGroupPage() { const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [selectedId, setSelectedId] = useState(null); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showUserModal, setShowUserModal] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState<{msg: string; onConfirm: () => void} | null>(null); const canManage = useActionPermission('instance-group.manage'); // Form state — shared by create & edit const [formName, setFormName] = useState(''); const [formDesc, setFormDesc] = useState(''); const [formIsActive, setFormIsActive] = useState(true); const [readonlyIds, setReadonlyIds] = useState([]); const [writeIds, setWriteIds] = useState([]); const [allInstances, setAllInstances] = useState([]); const [allUsers, setAllUsers] = useState([]); const [selectedUserIds, setSelectedUserIds] = useState([]); const [userSearch, setUserSearch] = useState(''); const fetchGroups = useCallback(async () => { try { setGroups(await instanceGroupApi.list()); } catch {} finally { setLoading(false); } }, []); useEffect(() => { fetchGroups(); }, [fetchGroups]); useEffect(() => { if (showCreateModal || showEditModal) { instanceApi.list().then((res: any) => { setAllInstances(Array.isArray(res) ? res : (res?.items || [])); }).catch(() => {}); } }, [showCreateModal, showEditModal]); useEffect(() => { if (showUserModal) { userApi.list().then((d: any) => { setAllUsers(Array.isArray(d?.items || d) ? (d?.items || d) : []); }).catch(() => {}); } }, [showUserModal]); const transferItems = useMemo(() => allInstances.map((i: any) => ({ label: `${i.name} (${i.host}:${i.port})`, value: String(i.id), tag: i.db_type, tagColor: DB_TYPE_COLORS[i.db_type] || '', accessMode: i.access_mode || 'readwrite', })), [allInstances] ); // 查询授权:只匹配 read + readwrite 实例 const readonlyTransferItems = useMemo(() => transferItems.filter(i => i.accessMode === 'read' || i.accessMode === 'readwrite'), [transferItems] ); // 变更授权:只匹配 write + readwrite 实例 const writeTransferItems = useMemo(() => transferItems.filter(i => i.accessMode === 'write' || i.accessMode === 'readwrite'), [transferItems] ); const filteredGroups = useMemo(() => groups.filter(g => !search || g.name.includes(search)), [groups, search]); const filteredUsers = useMemo(() => allUsers.filter(u => !userSearch || (u.display_name || u.username).toLowerCase().includes(userSearch.toLowerCase())), [allUsers, userSearch]); const resetForm = () => { setFormName(''); setFormDesc(''); setFormIsActive(true); setReadonlyIds([]); setWriteIds([]); }; /* ── Create ── */ const handleCreate = async () => { if (!formName.trim()) return; await instanceGroupApi.create({ name: formName.trim(), description: formDesc.trim() || undefined, db_type: 'all', is_active: formIsActive, readonly_instance_ids: readonlyIds.map(Number), write_instance_ids: writeIds.map(Number), }); setShowCreateModal(false); resetForm(); fetchGroups(); }; /* ── Edit ── */ const openEditModal = (group: InstanceGroup) => { setFormName(group.name); setFormDesc(group.description || ''); setFormIsActive(group.is_active); setReadonlyIds((group.readonly_instance_ids || []).map((i: any) => String(typeof i === 'object' ? i.id : i))); setWriteIds((group.write_instance_ids || []).map((i: any) => String(typeof i === 'object' ? i.id : i))); setSelectedId(group.id); setShowEditModal(true); }; const handleEdit = async () => { if (!selectedId || !formName.trim()) return; await instanceGroupApi.update(selectedId, { name: formName.trim(), description: formDesc.trim() || undefined, is_active: formIsActive, readonly_instance_ids: readonlyIds.map(Number), write_instance_ids: writeIds.map(Number), }); setShowEditModal(false); resetForm(); fetchGroups(); }; /* ── Delete ── */ const handleDelete = async (id: number) => { setDeleteConfirm({msg: '确定删除该权限组?', onConfirm: async () => { await instanceGroupApi.delete(id); if (selectedId === id) setSelectedId(null); fetchGroups(); }}); }; /* ── Assign Users ── */ const handleAssignUsers = async () => { if (!selectedId || selectedUserIds.length === 0) return; await instanceGroupApi.assignUsers(selectedId, selectedUserIds); setShowUserModal(false); setSelectedUserIds([]); fetchGroups(); }; const selectedGroup = groups.find(g => g.id === selectedId); if (loading) return
加载中...
; /* ── Instance Row ── */ const InstanceList = ({ instances }: { instances: any[] }) => { if (instances.length === 0) return
暂无
; return (
{instances.map(inst => (
{inst.name} {inst.host}:{inst.port}
{inst.db_type}
))}
); }; /* ── Form Modal shared close/submit ── */ const formClose = () => { setShowCreateModal(false); setShowEditModal(false); resetForm(); }; const formMode = showCreateModal ? 'create' as const : showEditModal ? 'edit' as const : null; const formSubmit = formMode === 'create' ? handleCreate : handleEdit; return (
{/* Header */}

数据库权限组

每个策略包含「查询授权」和「变更授权」两类实例

{canManage && ( )}
{/* Left list */}
setSearch(e.target.value)} />
{filteredGroups.length === 0 &&
暂无权限组
} {filteredGroups.map(group => { const ro = group.readonly_instance_count || 0; const wr = group.write_instance_count || 0; return (
setSelectedId(group.id)} className={`p-3 rounded-lg cursor-pointer transition-colors ${selectedId === group.id ? 'bg-blue-500/10 border border-blue-500/30' : 'bg-[var(--dg-bg-secondary)] border border-transparent hover:bg-[var(--dg-bg-hover)]'}`}>
{group.name} {!group.is_active && 禁用}
{ro} 只读 {wr} 写入 {group.user_count || 0}
); })}
{/* Right detail */}
{selectedGroup ? (() => { const sg = selectedGroup; const roInst = sg.readonly_instances || []; const wrInst = sg.write_instances || []; const roCount = sg.readonly_instance_count ?? (sg.readonly_instance_ids || []).length; const wrCount = sg.write_instance_count ?? (sg.write_instance_ids || []).length; return (

{sg.name}

{!sg.is_active && 禁用}
{sg.description &&

{sg.description}

}
{roCount} 只读实例 {wrCount} 写入实例 {sg.user_count || 0} 用户
{canManage && (
)}
{/* Readonly instances */}

查询授权 ({roInst.length})

{/* Write instances */}

变更授权 ({wrInst.length})

{/* Users */}

关联用户 ({sg.users?.length || 0})

{canManage && }
{sg.users && sg.users.length > 0 ? (
{sg.users.map(u => (
{(u.display_name || u.username).charAt(0)}
{u.display_name || u.username} {u.role}
))}
) :
暂未关联用户
}
); })() : (

选择左侧权限组查看详情

)}
{/* Form Modal (create & edit) */} {formMode && (

{formMode === 'create' ? '新建权限策略' : '编辑权限策略'}

setFormName(e.target.value)} placeholder="如:生产数据库权限" />
setFormDesc(e.target.value)} placeholder="可选" />
查询授权 — 查询数据的实例
变更授权 — 数据变更工单的实例
)} { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }} onCancel={() => setDeleteConfirm(null)} /> {/* User Modal */} {showUserModal && ( setShowUserModal(false)} width="max-w-md">

关联用户

setUserSearch(e.target.value)} placeholder="搜索用户..." className="w-full pl-10 pr-4 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" />
{filteredUsers.map(u => ( ))}
{/* end px-6 py-4 */}
)}
); }