AuditLogsPage 暗色主题 P0 修复: - ACTION_COLORS: bg-blue-100→bg-blue-500/15, text-blue-800→text-blue-400 等 8 项 - 表格容器: bg-white→dg-card, bg-gray-50→bg-[var(--dg-bg-tertiary)] - 所有 text-gray-* → --dg-text-* 变量 - 所有 bg-gray-* → --dg-bg-* 变量 - 分隔线: divide-gray-100→divide-[var(--dg-border)] - 分页按钮: 全部改用 dg 变量 - grep 验证: 零残留硬编码亮色 InstanceGroupPage 权限项对齐: - 前端权限项从 18 个精简到 9 个,与后端 PERMISSION_ACTIONS 一致 - 查询: query:select, query:write - 工单: ticket:create, ticket:approve, ticket:execute - DDL: ddl:execute - 运维: slowlog:view, backup:manage, instance:detail
499 lines
26 KiB
TypeScript
499 lines
26 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { Plus, Search, Edit2, Trash2, Server, Users, ChevronRight, 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';
|
|
|
|
// 权限项 — 与后端 PERMISSION_ACTIONS 保持一致
|
|
var PERMISSION_CATEGORIES = [
|
|
{
|
|
label: '查询权限',
|
|
items: [
|
|
{ code: 'query:select', name: 'SELECT 查询' },
|
|
{ code: 'query:write', name: '写入操作 (INSERT/UPDATE/DELETE)' },
|
|
],
|
|
},
|
|
{
|
|
label: '工单权限',
|
|
items: [
|
|
{ code: 'ticket:create', name: '创建工单' },
|
|
{ code: 'ticket:approve', name: '审批工单' },
|
|
{ code: 'ticket:execute', name: '执行工单' },
|
|
],
|
|
},
|
|
{
|
|
label: 'DDL 权限',
|
|
items: [
|
|
{ code: 'ddl:execute', name: 'DDL 执行' },
|
|
],
|
|
},
|
|
{
|
|
label: '运维权限',
|
|
items: [
|
|
{ code: 'slowlog:view', name: '查看慢日志' },
|
|
{ code: 'backup:manage', name: '备份管理' },
|
|
{ code: 'instance:detail', name: '实例详情' },
|
|
],
|
|
},
|
|
];
|
|
|
|
var DB_TYPE_OPTIONS = [
|
|
{ value: 'all', label: '全部类型' },
|
|
{ value: 'mysql', label: 'MySQL' },
|
|
{ value: 'pg', label: 'PostgreSQL' },
|
|
{ value: 'redis', label: 'Redis' },
|
|
{ value: 'mongo', label: 'MongoDB' },
|
|
];
|
|
|
|
var DB_TYPE_COLORS: Record<string, string> = {
|
|
mysql: 'bg-[rgba(0,117,143,0.15)] text-[#26a69a]',
|
|
pg: '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]',
|
|
all: 'bg-[rgba(148,163,184,0.15)] text-[var(--dg-text-secondary)]',
|
|
};
|
|
|
|
export default function InstanceGroupPage() {
|
|
var [groups, setGroups] = useState<InstanceGroup[]>([]);
|
|
var [loading, setLoading] = useState(true);
|
|
var [search, setSearch] = useState('');
|
|
var [selectedId, setSelectedId] = useState<number | null>(null);
|
|
var [showCreateModal, setShowCreateModal] = useState(false);
|
|
var [showEditModal, setShowEditModal] = useState(false);
|
|
var [showInstanceModal, setShowInstanceModal] = useState(false);
|
|
var [showUserModal, setShowUserModal] = useState(false);
|
|
var canManage = useActionPermission('instance-group.manage');
|
|
|
|
// 表单状态
|
|
var [formName, setFormName] = useState('');
|
|
var [formDesc, setFormDesc] = useState('');
|
|
var [formDbType, setFormDbType] = useState('all');
|
|
var [formPermissions, setFormPermissions] = useState<string[]>([]);
|
|
var [formIsActive, setFormIsActive] = useState(true);
|
|
|
|
// 关联数据
|
|
var [allInstances, setAllInstances] = useState<any[]>([]);
|
|
var [allUsers, setAllUsers] = useState<any[]>([]);
|
|
var [selectedInstanceIds, setSelectedInstanceIds] = useState<number[]>([]);
|
|
var [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
|
|
|
var selectedGroup = groups.find(g => g.id === selectedId);
|
|
|
|
var fetchGroups = useCallback(async () => {
|
|
try {
|
|
var data = await instanceGroupApi.list();
|
|
setGroups(data);
|
|
setLoading(false);
|
|
} catch {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { fetchGroups(); }, [fetchGroups]);
|
|
|
|
// 加载关联数据
|
|
useEffect(() => {
|
|
if (showInstanceModal) {
|
|
instanceApi.list().then(setAllInstances).catch(() => {});
|
|
}
|
|
if (showUserModal) {
|
|
userApi.list().then(setAllUsers).catch(() => {});
|
|
}
|
|
}, [showInstanceModal, showUserModal]);
|
|
|
|
var filteredGroups = groups.filter(g =>
|
|
!search || g.name.includes(search) || (g.description || '').includes(search)
|
|
);
|
|
|
|
var resetForm = () => {
|
|
setFormName('');
|
|
setFormDesc('');
|
|
setFormDbType('all');
|
|
setFormPermissions([]);
|
|
setFormIsActive(true);
|
|
};
|
|
|
|
var handleCreate = async () => {
|
|
if (!formName.trim()) return;
|
|
await instanceGroupApi.create({
|
|
name: formName,
|
|
description: formDesc || undefined,
|
|
db_type: formDbType,
|
|
permissions: formPermissions.length > 0 ? formPermissions : undefined,
|
|
is_active: formIsActive,
|
|
});
|
|
setShowCreateModal(false);
|
|
resetForm();
|
|
fetchGroups();
|
|
};
|
|
|
|
var handleEdit = async () => {
|
|
if (!selectedId || !formName.trim()) return;
|
|
await instanceGroupApi.update(selectedId, {
|
|
name: formName,
|
|
description: formDesc || undefined,
|
|
db_type: formDbType,
|
|
permissions: formPermissions.length > 0 ? formPermissions : undefined,
|
|
is_active: formIsActive,
|
|
});
|
|
setShowEditModal(false);
|
|
resetForm();
|
|
fetchGroups();
|
|
};
|
|
|
|
var handleDelete = async (id: number) => {
|
|
if (!confirm('确定删除该权限组?')) return;
|
|
await instanceGroupApi.delete(id);
|
|
if (selectedId === id) setSelectedId(null);
|
|
fetchGroups();
|
|
};
|
|
|
|
var openEditModal = (group: InstanceGroup) => {
|
|
setFormName(group.name);
|
|
setFormDesc(group.description || '');
|
|
setFormDbType(group.db_type);
|
|
setFormPermissions(group.permissions || []);
|
|
setFormIsActive(group.is_active);
|
|
setShowEditModal(true);
|
|
};
|
|
|
|
var handleAssignInstances = async () => {
|
|
if (!selectedId || selectedInstanceIds.length === 0) return;
|
|
await instanceGroupApi.assignInstances(selectedId, selectedInstanceIds);
|
|
setShowInstanceModal(false);
|
|
setSelectedInstanceIds([]);
|
|
fetchGroups();
|
|
};
|
|
|
|
var handleAssignUsers = async () => {
|
|
if (!selectedId || selectedUserIds.length === 0) return;
|
|
await instanceGroupApi.assignUsers(selectedId, selectedUserIds);
|
|
setShowUserModal(false);
|
|
setSelectedUserIds([]);
|
|
fetchGroups();
|
|
};
|
|
|
|
var togglePermission = (code: string) => {
|
|
setFormPermissions(prev =>
|
|
prev.includes(code) ? prev.filter(p => p !== code) : [...prev, code]
|
|
);
|
|
};
|
|
|
|
var toggleCategoryAll = (items: { code: string }[], checked: boolean) => {
|
|
var codes = items.map(i => i.code);
|
|
if (checked) {
|
|
setFormPermissions(prev => [...new Set([...prev, ...codes])]);
|
|
} else {
|
|
setFormPermissions(prev => prev.filter(p => !codes.includes(p)));
|
|
}
|
|
};
|
|
|
|
// 渲染权限组表单
|
|
var renderGroupForm = (onSubmit: () => void, submitLabel: string) => (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1.5">名称 <span className="text-red-400">*</span></label>
|
|
<input className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" value={formName} onChange={e => setFormName(e.target.value)} placeholder="权限组名称" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1.5">描述</label>
|
|
<textarea className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none resize-none" rows={2} value={formDesc} onChange={e => setFormDesc(e.target.value)} placeholder="可选描述" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1.5">数据库类型</label>
|
|
<select className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" value={formDbType} onChange={e => setFormDbType(e.target.value)}>
|
|
{DB_TYPE_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1.5">状态</label>
|
|
<select className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" value={formIsActive ? 'true' : 'false'} onChange={e => setFormIsActive(e.target.value === 'true')}>
|
|
<option value="true">启用</option>
|
|
<option value="false">禁用</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">权限配置</label>
|
|
<div className="space-y-3 max-h-60 overflow-y-auto pr-2">
|
|
{PERMISSION_CATEGORIES.map(cat => {
|
|
var allChecked = cat.items.every(i => formPermissions.includes(i.code));
|
|
return (
|
|
<div key={cat.label}>
|
|
<label className="flex items-center gap-2 text-xs font-semibold text-[var(--dg-text-secondary)] mb-1.5 cursor-pointer">
|
|
<input type="checkbox" checked={allChecked} onChange={e => toggleCategoryAll(cat.items, e.target.checked)} className="rounded border-[var(--dg-border)]" />
|
|
{cat.label}
|
|
</label>
|
|
<div className="flex flex-wrap gap-1.5 ml-5">
|
|
{cat.items.map(item => (
|
|
<label key={item.code} className={`flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs cursor-pointer transition-colors ${formPermissions.includes(item.code) ? 'bg-blue-500/15 text-blue-400' : 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'}`}>
|
|
<input type="checkbox" checked={formPermissions.includes(item.code)} onChange={() => togglePermission(item.code)} className="hidden" />
|
|
{item.name}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end gap-3 pt-2">
|
|
<button onClick={() => { setShowCreateModal(false); setShowEditModal(false); resetForm(); }} className="px-4 py-2 text-sm text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-colors">取消</button>
|
|
<button onClick={onSubmit} disabled={!formName.trim()} className="px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">{submitLabel}</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (loading) return <div className="flex items-center justify-center h-64 text-[var(--dg-text-muted)]">加载中...</div>;
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold">实例权限组</h1>
|
|
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">管理实例访问权限,按组分配用户和操作权限</p>
|
|
</div>
|
|
{canManage && (
|
|
<button onClick={() => { resetForm(); setShowCreateModal(true); }} className="flex items-center gap-2 px-4 py-2.5 bg-blue-500 rounded-lg text-sm font-medium text-white hover:bg-blue-600 transition-colors">
|
|
<Plus className="w-4 h-4" /> 新建权限组
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* 统计卡片 */}
|
|
<div className="grid grid-cols-4 gap-4">
|
|
{[
|
|
{ label: '权限组总数', value: groups.length, color: 'text-blue-400', bg: 'bg-blue-400/10' },
|
|
{ label: '活跃权限组', value: groups.filter(g => g.is_active).length, color: 'text-green-400', bg: 'bg-green-400/10' },
|
|
{ label: '关联实例数', value: groups.reduce((sum, g) => sum + (g.instance_count || 0), 0), color: 'text-purple-400', bg: 'bg-purple-400/10' },
|
|
{ label: '关联用户数', value: groups.reduce((sum, g) => sum + (g.user_count || 0), 0), color: 'text-orange-400', bg: 'bg-orange-400/10' },
|
|
].map((s, i) => (
|
|
<Card key={i} className="dg-card">
|
|
<CardContent className="p-4 flex items-center gap-3">
|
|
<div className={`w-10 h-10 rounded-lg ${s.bg} flex items-center justify-center`}>
|
|
{[Server, Server, Users, Users][i] && (() => { var Icon = [Server, Server, Users, Users][i]; return <Icon className={`w-5 h-5 ${s.color}`} />; })()}
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-bold">{s.value}</div>
|
|
<div className="text-xs text-[var(--dg-text-muted)]">{s.label}</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex gap-5">
|
|
{/* 左侧列表 */}
|
|
<div className="w-80 flex-shrink-0 space-y-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
|
|
<input className="w-full pl-10 pr-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none" placeholder="搜索权限组" value={search} onChange={e => setSearch(e.target.value)} />
|
|
</div>
|
|
<div className="space-y-2 max-h-[calc(100vh-380px)] overflow-y-auto">
|
|
{filteredGroups.length === 0 && (
|
|
<div className="text-center py-10 text-[var(--dg-text-muted)] text-sm">暂无权限组</div>
|
|
)}
|
|
{filteredGroups.map(group => (
|
|
<div key={group.id} onClick={() => setSelectedId(group.id)} className={`p-3.5 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)]'}`}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2.5">
|
|
<span className="font-medium text-sm">{group.name}</span>
|
|
<span className={`px-1.5 py-0.5 text-[10px] rounded ${DB_TYPE_COLORS[group.db_type] || DB_TYPE_COLORS.all}`}>{DB_TYPE_OPTIONS.find(o => o.value === group.db_type)?.label || group.db_type}</span>
|
|
</div>
|
|
{!group.is_active && <span className="text-[10px] px-1.5 py-0.5 rounded bg-red-500/15 text-red-400">禁用</span>}
|
|
</div>
|
|
{group.description && <p className="text-xs text-[var(--dg-text-muted)] mt-1 line-clamp-1">{group.description}</p>}
|
|
<div className="flex items-center gap-3 mt-2 text-xs text-[var(--dg-text-muted)]">
|
|
<span className="flex items-center gap-1"><Server className="w-3 h-3" />{group.instance_count || 0} 实例</span>
|
|
<span className="flex items-center gap-1"><Users className="w-3 h-3" />{group.user_count || 0} 用户</span>
|
|
<span>{(group.permissions || []).length} 权限</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 右侧详情 */}
|
|
<div className="flex-1 min-w-0">
|
|
{selectedGroup ? (() => {
|
|
var sg = selectedGroup!;
|
|
return (
|
|
<div className="space-y-4">
|
|
<Card className="dg-card">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className="flex items-center gap-3">
|
|
<h2 className="text-lg font-semibold">{sg.name}</h2>
|
|
<span className={`px-2 py-0.5 text-xs rounded ${DB_TYPE_COLORS[sg.db_type] || DB_TYPE_COLORS.all}`}>{DB_TYPE_OPTIONS.find(o => o.value === sg.db_type)?.label}</span>
|
|
<span className={`px-2 py-0.5 text-xs rounded ${sg.is_active ? 'bg-green-500/15 text-green-400' : 'bg-red-500/15 text-red-400'}`}>{sg.is_active ? '启用' : '禁用'}</span>
|
|
</div>
|
|
{sg.description && <p className="text-sm text-[var(--dg-text-secondary)] mt-1">{sg.description}</p>}
|
|
</div>
|
|
{canManage && (
|
|
<div className="flex items-center gap-2">
|
|
<button onClick={() => openEditModal(sg)} className="p-2 rounded-lg hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-colors"><Edit2 className="w-4 h-4" /></button>
|
|
<button onClick={() => handleDelete(sg.id)} className="p-2 rounded-lg hover:bg-red-500/10 text-[var(--dg-text-secondary)] hover:text-red-400 transition-colors"><Trash2 className="w-4 h-4" /></button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* 权限标签 */}
|
|
{sg.permissions && sg.permissions.length > 0 && (
|
|
<div className="mt-4">
|
|
<div className="text-xs font-medium text-[var(--dg-text-secondary)] mb-2">已配置权限</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{sg.permissions.map(p => {
|
|
var parts = p.split(':');
|
|
return <span key={p} className="px-2 py-1 text-xs bg-blue-500/10 text-blue-400 rounded-md">{parts[0]} · {parts[1]}</span>;
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 关联实例 */}
|
|
<Card className="dg-card">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h3 className="text-sm font-semibold flex items-center gap-2"><Server className="w-4 h-4 text-[var(--dg-text-muted)]" />关联实例 ({sg.instances?.length || 0})</h3>
|
|
{canManage && (
|
|
<button onClick={() => { setSelectedInstanceIds([]); setShowInstanceModal(true); }} className="text-xs text-blue-400 hover:text-blue-300 transition-colors">添加实例</button>
|
|
)}
|
|
</div>
|
|
{sg.instances && sg.instances.length > 0 ? (
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{sg.instances.map(inst => (
|
|
<div key={inst.id} className="flex items-center justify-between px-3 py-2 bg-[var(--dg-bg-tertiary)] rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<span className={`w-2 h-2 rounded-full ${inst.status === 'connected' ? 'bg-green-400' : 'bg-[var(--dg-text-muted)]'}`} />
|
|
<span className="text-sm">{inst.name}</span>
|
|
<span className="text-xs text-[var(--dg-text-muted)]">{inst.host}:{inst.port}</span>
|
|
</div>
|
|
{canManage && (
|
|
<button onClick={async () => { await instanceGroupApi.removeInstance(sg.id, inst.id); fetchGroups(); }} className="text-[var(--dg-text-muted)] hover:text-red-400 transition-colors"><X className="w-3.5 h-3.5" /></button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-6 text-sm text-[var(--dg-text-muted)]">暂未关联实例</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 关联用户 */}
|
|
<Card className="dg-card">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h3 className="text-sm font-semibold flex items-center gap-2"><Users className="w-4 h-4 text-[var(--dg-text-muted)]" />关联用户 ({sg.users?.length || 0})</h3>
|
|
{canManage && (
|
|
<button onClick={() => { setSelectedUserIds([]); setShowUserModal(true); }} className="text-xs text-blue-400 hover:text-blue-300 transition-colors">添加用户</button>
|
|
)}
|
|
</div>
|
|
{sg.users && sg.users.length > 0 ? (
|
|
<div className="space-y-1.5">
|
|
{sg.users.map(u => (
|
|
<div key={u.id} className="flex items-center justify-between px-3 py-2 bg-[var(--dg-bg-tertiary)] rounded-lg">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="w-7 h-7 rounded-full bg-blue-500/15 text-blue-400 flex items-center justify-center text-xs font-medium">{(u.display_name || u.username).charAt(0)}</div>
|
|
<span className="text-sm">{u.display_name || u.username}</span>
|
|
<span className="text-xs text-[var(--dg-text-muted)]">{u.role}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-6 text-sm text-[var(--dg-text-muted)]">暂未关联用户</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
})() : (
|
|
<div className="flex flex-col items-center justify-center h-96 text-[var(--dg-text-muted)]">
|
|
<ChevronRight className="w-8 h-8 mb-2 rotate-90 opacity-50" />
|
|
<p className="text-sm">选择左侧权限组查看详情</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 创建弹框 */}
|
|
{showCreateModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => { setShowCreateModal(false); resetForm(); }}>
|
|
<div className="w-full max-w-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-xl p-6 shadow-2xl" onClick={e => e.stopPropagation()}>
|
|
<h3 className="text-lg font-semibold mb-4">新建权限组</h3>
|
|
{renderGroupForm(handleCreate, '创建')}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 编辑弹框 */}
|
|
{showEditModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => { setShowEditModal(false); resetForm(); }}>
|
|
<div className="w-full max-w-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-xl p-6 shadow-2xl" onClick={e => e.stopPropagation()}>
|
|
<h3 className="text-lg font-semibold mb-4">编辑权限组</h3>
|
|
{renderGroupForm(handleEdit, '保存')}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 关联实例弹框 */}
|
|
{showInstanceModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setShowInstanceModal(false)}>
|
|
<div className="w-full max-w-md bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-xl p-6 shadow-2xl" onClick={e => e.stopPropagation()}>
|
|
<h3 className="text-lg font-semibold mb-4">关联实例</h3>
|
|
<div className="space-y-2 max-h-72 overflow-y-auto mb-4">
|
|
{allInstances.length === 0 && <div className="text-center py-6 text-sm text-[var(--dg-text-muted)]">暂无可选实例</div>}
|
|
{allInstances.map(inst => (
|
|
<label key={inst.id} className={`flex items-center gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-colors ${selectedInstanceIds.includes(inst.id) ? 'bg-blue-500/10' : 'hover:bg-[var(--dg-bg-hover)]'}`}>
|
|
<input type="checkbox" checked={selectedInstanceIds.includes(inst.id)} onChange={e => setSelectedInstanceIds(e.target.checked ? [...selectedInstanceIds, inst.id] : selectedInstanceIds.filter(id => id !== inst.id))} className="rounded" />
|
|
<div className="flex-1">
|
|
<span className="text-sm">{inst.name}</span>
|
|
<span className="text-xs text-[var(--dg-text-muted)] ml-2">{inst.host}:{inst.port}</span>
|
|
</div>
|
|
<span className={`px-1.5 py-0.5 text-[10px] rounded ${DB_TYPE_COLORS[inst.db_type] || DB_TYPE_COLORS.all}`}>{inst.db_type}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
<div className="flex justify-end gap-3">
|
|
<button onClick={() => setShowInstanceModal(false)} className="px-4 py-2 text-sm text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-colors">取消</button>
|
|
<button onClick={handleAssignInstances} disabled={selectedInstanceIds.length === 0} className="px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 transition-colors">确认关联 ({selectedInstanceIds.length})</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 关联用户弹框 */}
|
|
{showUserModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setShowUserModal(false)}>
|
|
<div className="w-full max-w-md bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-xl p-6 shadow-2xl" onClick={e => e.stopPropagation()}>
|
|
<h3 className="text-lg font-semibold mb-4">关联用户</h3>
|
|
<div className="space-y-2 max-h-72 overflow-y-auto mb-4">
|
|
{allUsers.length === 0 && <div className="text-center py-6 text-sm text-[var(--dg-text-muted)]">暂无可选用户</div>}
|
|
{allUsers.map(u => (
|
|
<label key={u.id} className={`flex items-center gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-colors ${selectedUserIds.includes(u.id) ? 'bg-blue-500/10' : 'hover:bg-[var(--dg-bg-hover)]'}`}>
|
|
<input type="checkbox" checked={selectedUserIds.includes(u.id)} onChange={e => setSelectedUserIds(e.target.checked ? [...selectedUserIds, u.id] : selectedUserIds.filter(id => id !== u.id))} className="rounded" />
|
|
<div className="w-7 h-7 rounded-full bg-blue-500/15 text-blue-400 flex items-center justify-center text-xs font-medium">{(u.display_name || u.username).charAt(0)}</div>
|
|
<div className="flex-1">
|
|
<span className="text-sm">{u.display_name || u.username}</span>
|
|
<span className="text-xs text-[var(--dg-text-muted)] ml-2">{u.role}</span>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
<div className="flex justify-end gap-3">
|
|
<button onClick={() => setShowUserModal(false)} className="px-4 py-2 text-sm text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-colors">取消</button>
|
|
<button onClick={handleAssignUsers} disabled={selectedUserIds.length === 0} className="px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 transition-colors">确认关联 ({selectedUserIds.length})</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|