feat: K8s集群拓扑图接入真实后端数据 — 分层可视化Cluster→Ingress→Service→Deployment→Node + http.ts类型修复
This commit is contained in:
1 parent
4b094f4645
commit
9f65faff6f
17 files changed
+3021
-1205
No files matched your search
@@ -21,6 +21,7 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
@@ -50,9 +51,15 @@ import {
|
||||
History,
|
||||
LogOut,
|
||||
RefreshCw,
|
||||
Upload,
|
||||
Download,
|
||||
Network,
|
||||
Loader2,
|
||||
UserPlus,
|
||||
ChevronDown,
|
||||
} from 'lucide-react';
|
||||
import { roles, operationLogs } from '@/mocks/data';
|
||||
import { userApi } from '@/services/api';
|
||||
import { userApi, ldapApi } from '@/services/api';
|
||||
import type { Role, OperationLog } from '@/types';
|
||||
|
||||
// 后端用户数据转换为前端兼容格式
|
||||
@@ -63,6 +70,7 @@ function mapUser(u: any) {
|
||||
roles: Array.isArray(u.roles) ? u.roles : u.role ? [u.role] : [],
|
||||
lastLoginAt: u.last_login ?? u.lastLoginAt,
|
||||
createdAt: u.created_at ?? u.createdAt,
|
||||
authSource: u.auth_source ?? u.authSource ?? 'local',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,6 +98,44 @@ export function SystemManager() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form, setForm] = useState({ ...emptyForm });
|
||||
|
||||
// 批量导入弹窗
|
||||
const [importDialogOpen, setImportDialogOpen] = useState<'csv' | 'ldap' | null>(null);
|
||||
const [importPreview, setImportPreview] = useState<any[]>([]);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importResult, setImportResult] = useState<any>(null);
|
||||
|
||||
// LDAP 用户列表
|
||||
const [ldapUsers, setLdapUsers] = useState<any[]>([]);
|
||||
const [ldapSelected, setLdapSelected] = useState<Set<string>>(new Set());
|
||||
const [ldapLoading, setLdapLoading] = useState(false);
|
||||
|
||||
// LDAP 配置
|
||||
const [ldapConfig, setLdapConfig] = useState<any>(null);
|
||||
const [ldapForm, setLdapForm] = useState({
|
||||
ldap_host: '', ldap_port: 389, use_ldaps: false,
|
||||
admin_dn: '', admin_password: '', base_dn: '', search_filter: '',
|
||||
});
|
||||
const [ldapSaving, setLdapSaving] = useState(false);
|
||||
const [ldapTesting, setLdapTesting] = useState(false);
|
||||
const [ldapTestResult, setLdapTestResult] = useState<string | null>(null);
|
||||
const [ldapMgmtOpen, setLdapMgmtOpen] = useState(false);
|
||||
const [importFeedback, setImportFeedback] = useState<string | null>(null);
|
||||
const [importResultOpen, setImportResultOpen] = useState(false);
|
||||
|
||||
// LDAP 账号管理
|
||||
const [ldapOUs, setLdapOUs] = useState<any[]>([]);
|
||||
const [selectedOUDN, setSelectedOUDN] = useState('');
|
||||
const [selectedLDAPUsers, setSelectedLDAPUsers] = useState<string[]>([]);
|
||||
const [createOUOpen, setCreateOUOpen] = useState(false);
|
||||
const [createUserOpen, setCreateUserOpen] = useState(false);
|
||||
const [ouForm, setOuForm] = useState({ ou_name: '', parent_dn: '', description: '' });
|
||||
const [newUser, setNewUser] = useState({ username: '', password: '', display_name: '', email: '', ou_dn: '' });
|
||||
const [ldapUserTestOpen, setLdapUserTestOpen] = useState(false);
|
||||
const [ldapTestUser, setLdapTestUser] = useState('');
|
||||
const [ldapTestPass, setLdapTestPass] = useState('');
|
||||
const [ldapUserTesting, setLdapUserTesting] = useState(false);
|
||||
const [ldapUserTestResult, setLdapUserTestResult] = useState<any>(null);
|
||||
|
||||
const loadUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -106,6 +152,7 @@ export function SystemManager() {
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
loadLdapConfig();
|
||||
}, []);
|
||||
|
||||
const filteredUsers = users.filter(
|
||||
@@ -206,6 +253,215 @@ export function SystemManager() {
|
||||
}
|
||||
};
|
||||
|
||||
// CSV 文件解析
|
||||
const handleCsvUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
const text = ev.target?.result as string;
|
||||
const lines = text.split('\n').filter((l) => l.trim() && !l.startsWith('\xEF\xBB\xBF'));
|
||||
if (lines.length < 2) return;
|
||||
// Find column indices from header
|
||||
const headers = lines[0].split(',').map((h) => h.trim().toLowerCase());
|
||||
const idxUsername = headers.findIndex((h) => h.includes('用户名') || h.includes('username'));
|
||||
const idxNickname = headers.findIndex((h) => h.includes('昵称') || h.includes('name') || h.includes('显示名'));
|
||||
const idxEmail = headers.findIndex((h) => h.includes('邮箱') || h.includes('email') || h.includes('mail'));
|
||||
const idxPhone = headers.findIndex((h) => h.includes('手机') || h.includes('phone'));
|
||||
const idxRole = headers.findIndex((h) => h.includes('角色') || h.includes('role'));
|
||||
const idxPassword = headers.findIndex((h) => h.includes('密码') || h.includes('password'));
|
||||
|
||||
const parsed: any[] = [];
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cols = lines[i].split(',').map((c) => c.trim());
|
||||
parsed.push({
|
||||
username: idxUsername >= 0 ? cols[idxUsername] : '',
|
||||
nickname: idxNickname >= 0 ? cols[idxNickname] : '',
|
||||
email: idxEmail >= 0 ? cols[idxEmail] : '',
|
||||
phone: idxPhone >= 0 ? cols[idxPhone] : '',
|
||||
role: idxRole >= 0 ? cols[idxRole] : '',
|
||||
password: idxPassword >= 0 ? cols[idxPassword] : '',
|
||||
});
|
||||
}
|
||||
setImportPreview(parsed);
|
||||
setImportResult(null);
|
||||
};
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
};
|
||||
|
||||
// 执行 CSV 批量导入
|
||||
const handleBatchImport = async () => {
|
||||
setImporting(true);
|
||||
try {
|
||||
const res = await userApi.batchImport({ users: importPreview, skip_existing: true });
|
||||
setImportResult(res);
|
||||
await loadUsers();
|
||||
} catch (error: any) {
|
||||
setImportResult({ code: 500, message: error?.message || '导入失败' });
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载 LDAP 用户列表
|
||||
const loadLdapUsers = async () => {
|
||||
setLdapLoading(true);
|
||||
try {
|
||||
const res = await ldapApi.listUsers();
|
||||
setLdapUsers(Array.isArray(res) ? res : []);
|
||||
setLdapSelected(new Set());
|
||||
} catch {
|
||||
setLdapUsers([]);
|
||||
} finally {
|
||||
setLdapLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 执行 LDAP 导入
|
||||
const handleLdapImport = async () => {
|
||||
let selected: any[] = [];
|
||||
if (selectedLDAPUsers.length > 0) {
|
||||
selected = ldapUsers.filter((u) => selectedLDAPUsers.includes(u.dn));
|
||||
} else {
|
||||
selected = ldapUsers; // 没有选择则导入全部
|
||||
}
|
||||
if (selected.length === 0) return;
|
||||
setImporting(true);
|
||||
try {
|
||||
const mapped = selected.map((u) => ({
|
||||
username: u.uid || '',
|
||||
nickname: u.cn || '',
|
||||
email: u.mail || '',
|
||||
}));
|
||||
const res = await ldapApi.batchImport(mapped);
|
||||
setImportFeedback(res?.message || `成功导入 ${selected.length} 个用户`);
|
||||
setImportResultOpen(true);
|
||||
setSelectedLDAPUsers([]);
|
||||
await loadUsers();
|
||||
} catch (error: any) {
|
||||
setImportFeedback('导入失败: ' + (error?.message || '未知错误'));
|
||||
setImportResultOpen(true);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载 LDAP 配置
|
||||
const loadLdapConfig = async () => {
|
||||
try {
|
||||
const res = await ldapApi.getConfig();
|
||||
if (res && res.ldap_host) {
|
||||
setLdapConfig(res);
|
||||
setLdapForm({
|
||||
ldap_host: res.ldap_host || '',
|
||||
ldap_port: res.ldap_port || 389,
|
||||
use_ldaps: res.use_ldaps || false,
|
||||
admin_dn: res.admin_dn || '',
|
||||
admin_password: '',
|
||||
base_dn: res.base_dn || '',
|
||||
search_filter: res.search_filter || '',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// No config yet
|
||||
}
|
||||
};
|
||||
|
||||
// 保存 LDAP 配置
|
||||
const handleLdapSave = async () => {
|
||||
setLdapSaving(true);
|
||||
try {
|
||||
await ldapApi.updateConfig(ldapForm);
|
||||
await loadLdapConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to save LDAP config:', error);
|
||||
} finally {
|
||||
setLdapSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 测试 LDAP 连接
|
||||
const handleLdapTest = async () => {
|
||||
setLdapTesting(true);
|
||||
setLdapTestResult(null);
|
||||
try {
|
||||
await ldapApi.updateConfig(ldapForm);
|
||||
const res = await ldapApi.testConnection();
|
||||
setLdapTestResult(res?.message || (res?.success ? '连接成功' : '连接失败'));
|
||||
} catch (error: any) {
|
||||
setLdapTestResult(error?.message || '连接失败');
|
||||
} finally {
|
||||
setLdapTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// LDAP 账号管理函数
|
||||
const loadLdapOUs = async () => {
|
||||
try {
|
||||
const ous = await ldapApi.listOUs();
|
||||
setLdapOUs(ous || []);
|
||||
// 同时加载根 DN 下的用户
|
||||
if (!selectedOUDN && ldapConfig?.base_dn) {
|
||||
setSelectedOUDN(ldapConfig.base_dn);
|
||||
await loadLdapOUUsers(ldapConfig.base_dn);
|
||||
}
|
||||
} catch (e) { console.error('loadOUs', e); }
|
||||
};
|
||||
|
||||
const loadLdapOUUsers = async (ouDn?: string) => {
|
||||
try {
|
||||
const users = await ldapApi.listUsers(ouDn);
|
||||
setLdapUsers(users || []);
|
||||
} catch (e) { console.error('loadOUUsers', e); }
|
||||
};
|
||||
|
||||
const handleCreateOU = async () => {
|
||||
setLdapSaving(true);
|
||||
try {
|
||||
await ldapApi.createOU(ouForm);
|
||||
setCreateOUOpen(false);
|
||||
setOuForm({ ou_name: '', parent_dn: '', description: '' });
|
||||
await loadLdapOUs();
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || '';
|
||||
if (msg.includes('Already Exists') || msg.includes('68')) {
|
||||
alert(`OU "${ouForm.ou_name}" 已存在,请使用其他名称`);
|
||||
} else {
|
||||
alert('创建失败: ' + msg);
|
||||
}
|
||||
} finally { setLdapSaving(false); }
|
||||
};
|
||||
|
||||
const handleCreateLDAPUser = async () => {
|
||||
setLdapSaving(true);
|
||||
try {
|
||||
await ldapApi.createUser({ ...newUser, ou_dn: newUser.ou_dn || selectedOUDN || ldapConfig?.base_dn });
|
||||
setCreateUserOpen(false);
|
||||
setNewUser({ username: '', password: '', display_name: '', email: '', ou_dn: '' });
|
||||
await loadLdapOUUsers(selectedOUDN);
|
||||
} catch (e: any) { alert(e?.message || '创建失败'); }
|
||||
finally { setLdapSaving(false); }
|
||||
};
|
||||
|
||||
const handleDeleteLDAPEntry = async (dn: string, silent = false) => {
|
||||
if (!silent && !confirm(`确定删除 ${dn}?`)) return;
|
||||
try {
|
||||
await ldapApi.deleteUser(dn);
|
||||
if (!silent) await loadLdapOUUsers(selectedOUDN);
|
||||
} catch (e: any) { if (!silent) alert(e?.message || '删除失败'); }
|
||||
};
|
||||
|
||||
const handleLdapUserTest = async () => {
|
||||
setLdapUserTesting(true);
|
||||
setLdapUserTestResult(null);
|
||||
try {
|
||||
const res = await ldapApi.testConnection({ test_user: ldapTestUser, test_password: ldapTestPass });
|
||||
setLdapUserTestResult(res);
|
||||
} catch (e: any) {
|
||||
setLdapUserTestResult({ success: false, message: e?.message || '验证失败' });
|
||||
} finally { setLdapUserTesting(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
@@ -249,10 +505,20 @@ export function SystemManager() {
|
||||
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
/>
|
||||
</div>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建用户
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800" onClick={() => setImportDialogOpen('csv')}>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
批量导入用户
|
||||
</Button>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800" onClick={() => setImportDialogOpen('ldap')}>
|
||||
<Network className="h-4 w-4 mr-2" />
|
||||
LDAP 导入
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建用户
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Users Table */}
|
||||
@@ -265,6 +531,7 @@ export function SystemManager() {
|
||||
<TableHead className="text-slate-400">用户信息</TableHead>
|
||||
<TableHead className="text-slate-400">联系方式</TableHead>
|
||||
<TableHead className="text-slate-400">角色</TableHead>
|
||||
<TableHead className="text-slate-400">来源</TableHead>
|
||||
<TableHead className="text-slate-400">状态</TableHead>
|
||||
<TableHead className="text-slate-400">最后登录</TableHead>
|
||||
<TableHead className="text-slate-400 text-right">操作</TableHead>
|
||||
@@ -273,7 +540,7 @@ export function SystemManager() {
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={6} className="text-center py-12">
|
||||
<TableCell colSpan={7} className="text-center py-12">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="h-6 w-6 border-2 border-slate-700 border-t-blue-500 rounded-full animate-spin" />
|
||||
</div>
|
||||
@@ -281,7 +548,7 @@ export function SystemManager() {
|
||||
</TableRow>
|
||||
) : filteredUsers.length === 0 ? (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={6} className="text-center py-12 text-sm text-slate-500">
|
||||
<TableCell colSpan={7} className="text-center py-12 text-sm text-slate-500">
|
||||
暂无用户数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -322,6 +589,17 @@ export function SystemManager() {
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.authSource === 'ldap' ? (
|
||||
<Badge variant="outline" className="border-cyan-500/30 text-cyan-400 bg-cyan-500/10">
|
||||
LDAP
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="border-slate-600/30 text-slate-400 bg-slate-600/10">
|
||||
本地
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{getUserStatusBadge(user.status)}</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">
|
||||
@@ -612,14 +890,6 @@ export function SystemManager() {
|
||||
<span className="text-sm text-slate-400">系统版本</span>
|
||||
<span className="text-sm text-slate-200">v2.5.1</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-400">构建时间</span>
|
||||
<span className="text-sm text-slate-200">2025-03-30</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-400">数据库版本</span>
|
||||
<span className="text-sm text-slate-200">PostgreSQL 15.2</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-400">运行时间</span>
|
||||
<span className="text-sm text-slate-200">30天 12小时</span>
|
||||
@@ -627,6 +897,392 @@ export function SystemManager() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* LDAP 配置 */}
|
||||
<Card className="bg-slate-900 border-slate-800 mt-4">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Network className="h-5 w-5 text-cyan-400" />
|
||||
LDAP / Active Directory
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500 mt-1">配置 LDAP 服务器,实现统一身份认证</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">LDAP 服务器地址</label>
|
||||
<Input
|
||||
value={ldapForm.ldap_host}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, ldap_host: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
placeholder="ldap.example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">端口</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={ldapForm.ldap_port}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, ldap_port: Number(e.target.value) })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200"
|
||||
placeholder="389"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5 col-span-2">
|
||||
<label className="text-xs text-slate-500">Base DN(用户搜索基础)</label>
|
||||
<Input
|
||||
value={ldapForm.base_dn}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, base_dn: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
placeholder="ou=users,dc=example,dc=com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">管理员 Bind DN</label>
|
||||
<Input
|
||||
value={ldapForm.admin_dn}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, admin_dn: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
placeholder="cn=admin,dc=example,dc=com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">管理员密码 {ldapConfig && '(留空不修改)'}</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={ldapForm.admin_password}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, admin_password: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
placeholder="******"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5 col-span-2">
|
||||
<label className="text-xs text-slate-500">搜索过滤器(可选,用 %s 代表用户名)</label>
|
||||
<Input
|
||||
value={ldapForm.search_filter}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, search_filter: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
placeholder="(uid=%s)"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 col-span-2">
|
||||
<Switch
|
||||
checked={ldapForm.use_ldaps}
|
||||
onCheckedChange={(v) => setLdapForm({ ...ldapForm, use_ldaps: v })}
|
||||
className="data-[state=checked]:bg-blue-600"
|
||||
/>
|
||||
<span className="text-sm text-slate-300">启用 LDAPS(SSL 加密)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 测试/保存按钮 */}
|
||||
<div className="flex items-center gap-3 mt-4 pt-4 border-t border-slate-800">
|
||||
{ldapTestResult && (
|
||||
<span className={`text-xs ${ldapTestResult.includes('成功') ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{ldapTestResult}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-slate-700 text-slate-300"
|
||||
onClick={handleLdapTest}
|
||||
disabled={ldapTesting}
|
||||
>
|
||||
{ldapTesting ? <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5 mr-1" />}
|
||||
测试连接
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={handleLdapSave}
|
||||
disabled={ldapSaving}
|
||||
>
|
||||
{ldapSaving ? '保存中...' : '保存配置'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* LDAP 账号管理 - 可折叠 */}
|
||||
{ldapConfig && (
|
||||
<div className="mt-4">
|
||||
{/* 折叠/展开按钮 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
const open = !ldapMgmtOpen;
|
||||
setLdapMgmtOpen(open);
|
||||
if (open && ldapOUs.length === 0) loadLdapOUs();
|
||||
}}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 bg-slate-900/50 hover:bg-slate-800/50 text-slate-400 text-sm transition-colors"
|
||||
>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${ldapMgmtOpen ? '' : '-rotate-90'}`} />
|
||||
<Users className="h-4 w-4 text-amber-400" />
|
||||
<span className="text-slate-300">LDAP 账号管理</span>
|
||||
</button>
|
||||
|
||||
{ldapMgmtOpen && (
|
||||
<div className="bg-slate-900/30 border-t border-slate-800/50 px-4 py-4 space-y-4">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{/* 账号验证测试 */}
|
||||
<Dialog open={ldapUserTestOpen} onOpenChange={setLdapUserTestOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||
<Key className="h-3.5 w-3.5 mr-1" />
|
||||
账号验证
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">LDAP 账号验证测试</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">输入 LDAP 用户凭据进行认证测试</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">用户名</label>
|
||||
<Input
|
||||
value={ldapTestUser}
|
||||
onChange={(e) => setLdapTestUser(e.target.value)}
|
||||
placeholder="zhangsan"
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">密码</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={ldapTestPass}
|
||||
onChange={(e) => setLdapTestPass(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleLdapUserTest()}
|
||||
placeholder="••••••"
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
{ldapUserTestResult && (
|
||||
<div className={`p-3 rounded-lg text-sm ${ldapUserTestResult.success ? 'bg-emerald-500/10 text-emerald-300' : 'bg-red-500/10 text-red-300'}`}>
|
||||
{ldapUserTestResult.message}
|
||||
{ldapUserTestResult.user_info && (
|
||||
<pre className="mt-2 text-xs whitespace-pre-wrap">{JSON.stringify(ldapUserTestResult.user_info, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={handleLdapUserTest}
|
||||
disabled={ldapUserTesting || !ldapTestUser}
|
||||
>
|
||||
{ldapUserTesting ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : null}
|
||||
验证
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200" onClick={loadLdapOUs}>
|
||||
<RefreshCw className="h-3.5 w-3.5 mr-1" />
|
||||
刷新
|
||||
</Button>
|
||||
|
||||
{/* OU 选择 */}
|
||||
<Select value={selectedOUDN} onValueChange={(v) => { setSelectedOUDN(v); loadLdapOUUsers(v); }}>
|
||||
<SelectTrigger className="w-60 h-7 bg-slate-800 border-slate-700 text-slate-200 text-xs">
|
||||
<SelectValue placeholder="选择组织单元" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectItem value={ldapConfig.base_dn}>根 ({ldapConfig.base_dn})</SelectItem>
|
||||
{ldapOUs.map((ou: any) => (
|
||||
<SelectItem key={ou.dn} value={ou.dn}>
|
||||
{ou.description ? `${ou.name} (${ou.description})` : ou.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 新建 OU */}
|
||||
<Dialog open={createOUOpen} onOpenChange={setCreateOUOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
新建 OU
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">新建组织单元</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">在 LDAP 目录中创建新的组织单元 (OU)</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">OU 名称 *</label>
|
||||
<Input value={ouForm.ou_name} onChange={(e) => setOuForm({ ...ouForm, ou_name: e.target.value })} placeholder="dev" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">父级 DN</label>
|
||||
<Input value={ouForm.parent_dn} onChange={(e) => setOuForm({ ...ouForm, parent_dn: e.target.value })} placeholder={ldapConfig.base_dn} className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">描述</label>
|
||||
<Input value={ouForm.description} onChange={(e) => setOuForm({ ...ouForm, description: e.target.value })} placeholder="开发团队" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={handleCreateOU} disabled={!ouForm.ou_name || ldapSaving}>
|
||||
{ldapSaving ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : null}
|
||||
创建
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 添加用户 */}
|
||||
<Dialog open={createUserOpen} onOpenChange={setCreateUserOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||
<UserPlus className="h-3.5 w-3.5 mr-1" />
|
||||
添加用户
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">添加 LDAP 用户</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">在 LDAP 目录中创建新的用户账号</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">用户名 *</label>
|
||||
<Input value={newUser.username} onChange={(e) => setNewUser({ ...newUser, username: e.target.value })} placeholder="zhangsan" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">密码 *</label>
|
||||
<Input type="password" value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} placeholder="••••••" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">显示名称</label>
|
||||
<Input value={newUser.display_name} onChange={(e) => setNewUser({ ...newUser, display_name: e.target.value })} placeholder="张三" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">邮箱</label>
|
||||
<Input value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} placeholder="zs@example.com" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">目标 OU</label>
|
||||
<Input value={newUser.ou_dn || selectedOUDN || ldapConfig.base_dn} onChange={(e) => setNewUser({ ...newUser, ou_dn: e.target.value })} className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={handleCreateLDAPUser} disabled={!newUser.username || !newUser.password || ldapSaving}>
|
||||
{ldapSaving ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : null}
|
||||
创建
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 导入到本地 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-blue-600/50 text-blue-400 hover:bg-blue-600/10 ml-auto"
|
||||
onClick={handleLdapImport}
|
||||
disabled={ldapUsers.length === 0 || importing}
|
||||
>
|
||||
{importing ? <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" /> : <Download className="h-3.5 w-3.5 mr-1" />}
|
||||
{selectedLDAPUsers.length > 0 ? `导入 (${selectedLDAPUsers.length})` : `导入全部 (${ldapUsers.length})`}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{importFeedback && (
|
||||
<Dialog open={importResultOpen} onOpenChange={setImportResultOpen}>
|
||||
<DialogContent className="max-w-sm bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">导入结果</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">LDAP 用户导入到本地用户库的执行结果</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-3 text-sm text-slate-300">
|
||||
{importFeedback}
|
||||
</div>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={() => setImportResultOpen(false)}>
|
||||
确定
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* 用户表格 */}
|
||||
<div className="border border-slate-800 rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableHead className="text-slate-400 w-8">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded bg-slate-800 border-slate-600"
|
||||
checked={selectedLDAPUsers.length > 0 && selectedLDAPUsers.length === ldapUsers.length}
|
||||
onChange={(e) => setSelectedLDAPUsers(e.target.checked ? ldapUsers.map((u: any) => u.dn) : [])}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-slate-400">用户名</TableHead>
|
||||
<TableHead className="text-slate-400">显示名称</TableHead>
|
||||
<TableHead className="text-slate-400">邮箱</TableHead>
|
||||
<TableHead className="text-slate-400">DN</TableHead>
|
||||
<TableHead className="text-slate-400 w-12">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ldapUsers.length === 0 ? (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={6} className="text-center text-slate-500 py-8">
|
||||
点击「刷新」加载 LDAP 用户
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
ldapUsers.map((user: any) => (
|
||||
<TableRow key={user.dn} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded bg-slate-800 border-slate-600"
|
||||
checked={selectedLDAPUsers.includes(user.dn)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) setSelectedLDAPUsers([...selectedLDAPUsers, user.dn]);
|
||||
else setSelectedLDAPUsers(selectedLDAPUsers.filter((d: string) => d !== user.dn));
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-200 font-medium">{user.uid}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{user.cn}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">{user.mail || '-'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs text-slate-500">{user.dn}</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
onClick={() => handleDeleteLDAPEntry(user.dn)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -813,6 +1469,214 @@ export function SystemManager() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* CSV 批量导入 Dialog */}
|
||||
<Dialog open={importDialogOpen === 'csv'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Upload className="h-5 w-5 text-blue-400" />
|
||||
批量导入用户
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
上传 CSV 文件批量创建用户。请先下载模板,按格式填写后上传。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!importResult ? (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={() => userApi.downloadTemplate()}>
|
||||
<Download className="h-4 w-4 mr-1.5" />
|
||||
下载模板
|
||||
</Button>
|
||||
<label className="inline-flex items-center px-3 py-1.5 rounded-md text-sm bg-slate-800 border border-slate-700 text-slate-200 cursor-pointer hover:bg-slate-700 transition-colors">
|
||||
<Upload className="h-4 w-4 mr-1.5" />
|
||||
选择 CSV 文件
|
||||
<input type="file" accept=".csv" className="hidden" onChange={handleCsvUpload} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{importPreview.length > 0 && (
|
||||
<div className="rounded-lg border border-slate-800 overflow-hidden">
|
||||
<div className="px-3 py-2 bg-slate-800/50 text-xs text-slate-400">
|
||||
预览:{importPreview.length} 条数据
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800">
|
||||
<TableHead className="text-slate-400 text-xs">用户名</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">昵称</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">邮箱</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">手机</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{importPreview.slice(0, 5).map((u, i) => (
|
||||
<TableRow key={i} className="border-slate-800">
|
||||
<TableCell className="text-slate-200 text-xs">{u.username}</TableCell>
|
||||
<TableCell className="text-slate-300 text-xs">{u.nickname}</TableCell>
|
||||
<TableCell className="text-slate-400 text-xs">{u.email}</TableCell>
|
||||
<TableCell className="text-slate-400 text-xs">{u.phone}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{importPreview.length > 5 && (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={4} className="text-slate-500 text-xs text-center">
|
||||
还有 {importPreview.length - 5} 条...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setImportPreview([]); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleBatchImport} disabled={importing || importPreview.length === 0}>
|
||||
{importing ? '导入中...' : `确认导入 (${importPreview.length})`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${importResult.code === 200 ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
||||
{importResult.code === 200 ? (
|
||||
<CheckCircle className="h-6 w-6 text-emerald-400 shrink-0" />
|
||||
) : (
|
||||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-slate-200">{importResult.message}</span>
|
||||
</div>
|
||||
{importResult.errors?.length > 0 && (
|
||||
<div className="max-h-32 overflow-auto rounded-lg bg-slate-800/50 p-3 space-y-1">
|
||||
{importResult.errors.map((err: string, i: number) => (
|
||||
<div key={i} className="text-xs text-red-400">{err}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setImportPreview([]); setImportResult(null); }}>
|
||||
完成
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* LDAP 导入 Dialog */}
|
||||
<Dialog open={importDialogOpen === 'ldap'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Network className="h-5 w-5 text-cyan-400" />
|
||||
从 LDAP 导入用户
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
从 LDAP 服务器检索用户列表,选择需要导入的用户
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!importResult ? (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-slate-500">
|
||||
{ldapUsers.length > 0 ? `找到 ${ldapUsers.length} 个用户` : '点击刷新从 LDAP 获取用户列表'}
|
||||
</p>
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={loadLdapUsers} disabled={ldapLoading}>
|
||||
{ldapLoading ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-1.5" />}
|
||||
刷新列表
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{ldapUsers.length > 0 && (
|
||||
<div className="max-h-80 overflow-auto rounded-lg border border-slate-800">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 sticky top-0 bg-slate-900">
|
||||
<TableHead className="w-10 text-slate-400 text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-600"
|
||||
checked={ldapSelected.size === ldapUsers.length && ldapUsers.length > 0}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setLdapSelected(new Set(ldapUsers.map((u) => u.dn)));
|
||||
} else {
|
||||
setLdapSelected(new Set());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">用户名</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">显示名</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">邮箱</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">DN</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ldapUsers.map((u) => (
|
||||
<TableRow key={u.dn} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-600"
|
||||
checked={ldapSelected.has(u.dn)}
|
||||
onChange={(e) => {
|
||||
const next = new Set(ldapSelected);
|
||||
if (e.target.checked) next.add(u.dn);
|
||||
else next.delete(u.dn);
|
||||
setLdapSelected(next);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-slate-200 text-xs">{u.uid}</TableCell>
|
||||
<TableCell className="text-slate-300 text-xs">{u.cn}</TableCell>
|
||||
<TableCell className="text-slate-400 text-xs">{u.mail}</TableCell>
|
||||
<TableCell className="text-slate-500 text-xs font-mono truncate max-w-[200px]">{u.dn}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleLdapImport} disabled={importing || ldapSelected.size === 0}>
|
||||
{importing ? '导入中...' : `导入选中 (${ldapSelected.size})`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${importResult.code === 200 ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
||||
{importResult.code === 200 ? (
|
||||
<CheckCircle className="h-6 w-6 text-emerald-400 shrink-0" />
|
||||
) : (
|
||||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-slate-200">{importResult.message}</span>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); setImportResult(null); }}>
|
||||
完成
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user