feat: AI预测分析接入真实后端数据 + 修复CloudBilling文件损坏
- AIPredictiveAnalytics: 替换3组硬编码mock为实时数据驱动预测 - 指标预测: 基于dashboard metrics时序数据(cpu/memory/disk)线性回归计算斜率与R² - 容量规划: 基于日均增长率推算阈值触及时间(天),动态生成AI扩容建议 - 异常预测: 基于告警规则阈值+当前趋势+事件频率计算异常触发概率 - 概览卡片数据驱动化(置信度基于R²拟合度,高风险/容量预警计数实时计算) - CloudBilling.tsx: 修复read_file导致的行号前缀损坏(501行全部含N|前缀)
This commit is contained in:
1 parent
0ebe22d142
commit
2da4f52209
17 files changed
+2299
-322
No files matched your search
@@ -1,5 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -85,6 +87,7 @@ const emptyForm = {
|
||||
};
|
||||
|
||||
export function SystemManager() {
|
||||
const confirm = useConfirm();
|
||||
const [activeTab, setActiveTab] = useState('users');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
@@ -108,6 +111,8 @@ export function SystemManager() {
|
||||
const [ldapUsers, setLdapUsers] = useState<any[]>([]);
|
||||
const [ldapSelected, setLdapSelected] = useState<Set<string>>(new Set());
|
||||
const [ldapLoading, setLdapLoading] = useState(false);
|
||||
const [ldapImportOUs, setLdapImportOUs] = useState<any[]>([]);
|
||||
const [ldapImportOUDN, setLdapImportOUDN] = useState('');
|
||||
|
||||
// LDAP 配置
|
||||
const [ldapConfig, setLdapConfig] = useState<any>(null);
|
||||
@@ -135,6 +140,8 @@ export function SystemManager() {
|
||||
const [ldapTestPass, setLdapTestPass] = useState('');
|
||||
const [ldapUserTesting, setLdapUserTesting] = useState(false);
|
||||
const [ldapUserTestResult, setLdapUserTestResult] = useState<any>(null);
|
||||
const [editLdapUser, setEditLdapUser] = useState<any>(null);
|
||||
const [editLdapForm, setEditLdapForm] = useState({ cn: '', mail: '', phone: '', title: '' });
|
||||
|
||||
const loadUsers = async () => {
|
||||
setLoading(true);
|
||||
@@ -155,6 +162,14 @@ export function SystemManager() {
|
||||
loadLdapConfig();
|
||||
}, []);
|
||||
|
||||
// LDAP 导入 Dialog 打开时加载 OU 和用户
|
||||
useEffect(() => {
|
||||
if (importDialogOpen === 'ldap') {
|
||||
ldapApi.listOUs().then(ous => setLdapImportOUs(Array.isArray(ous) ? ous : [])).catch(() => {});
|
||||
loadLdapUsers('');
|
||||
}
|
||||
}, [importDialogOpen]);
|
||||
|
||||
const filteredUsers = users.filter(
|
||||
(u) =>
|
||||
(u.username || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
@@ -244,7 +259,8 @@ export function SystemManager() {
|
||||
};
|
||||
|
||||
const handleDelete = async (user: any) => {
|
||||
if (!window.confirm(`确认删除用户「${user.nickname || user.username}」吗?`)) return;
|
||||
const ok = await confirm({ title: '删除用户', description: `确认删除用户「${user.nickname || user.username}」吗?`, confirmText: '删除', variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try {
|
||||
await userApi.deleteUser(user.id);
|
||||
await loadUsers();
|
||||
@@ -304,10 +320,10 @@ export function SystemManager() {
|
||||
};
|
||||
|
||||
// 加载 LDAP 用户列表
|
||||
const loadLdapUsers = async () => {
|
||||
const loadLdapUsers = async (ouDn?: string) => {
|
||||
setLdapLoading(true);
|
||||
try {
|
||||
const res = await ldapApi.listUsers();
|
||||
const res = await ldapApi.listUsers(ouDn);
|
||||
setLdapUsers(Array.isArray(res) ? res : []);
|
||||
setLdapSelected(new Set());
|
||||
} catch {
|
||||
@@ -425,9 +441,9 @@ export function SystemManager() {
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || '';
|
||||
if (msg.includes('Already Exists') || msg.includes('68')) {
|
||||
alert(`OU "${ouForm.ou_name}" 已存在,请使用其他名称`);
|
||||
toast.error(`OU "${ouForm.ou_name}" 已存在,请使用其他名称`);
|
||||
} else {
|
||||
alert('创建失败: ' + msg);
|
||||
toast.error('创建失败: ' + msg);
|
||||
}
|
||||
} finally { setLdapSaving(false); }
|
||||
};
|
||||
@@ -439,16 +455,30 @@ export function SystemManager() {
|
||||
setCreateUserOpen(false);
|
||||
setNewUser({ username: '', password: '', display_name: '', email: '', ou_dn: '' });
|
||||
await loadLdapOUUsers(selectedOUDN);
|
||||
} catch (e: any) { alert(e?.message || '创建失败'); }
|
||||
} catch (e: any) { toast.error(e?.message || '创建失败'); }
|
||||
finally { setLdapSaving(false); }
|
||||
};
|
||||
|
||||
const handleDeleteLDAPEntry = async (dn: string, silent = false) => {
|
||||
if (!silent && !confirm(`确定删除 ${dn}?`)) return;
|
||||
if (!silent) { const ok = await confirm({ title: '删除 LDAP 条目', description: `确定删除 ${dn}?`, confirmText: '删除', variant: 'destructive' }); if (!ok) return; }
|
||||
try {
|
||||
await ldapApi.deleteUser(dn);
|
||||
if (!silent) await loadLdapOUUsers(selectedOUDN);
|
||||
} catch (e: any) { if (!silent) alert(e?.message || '删除失败'); }
|
||||
} catch (e: any) { if (!silent) toast.error(e?.message || '删除失败'); }
|
||||
};
|
||||
|
||||
const openEditLdapUser = (user: any) => {
|
||||
setEditLdapUser(user);
|
||||
setEditLdapForm({ cn: user.cn || '', mail: user.mail || '', phone: user.phone || '', title: user.title || '' });
|
||||
};
|
||||
|
||||
const handleUpdateLdapUser = async () => {
|
||||
if (!editLdapUser) return;
|
||||
try {
|
||||
await ldapApi.updateUser({ dn: editLdapUser.dn, ...editLdapForm });
|
||||
setEditLdapUser(null);
|
||||
await loadLdapOUUsers(selectedOUDN);
|
||||
} catch (e: any) { toast.error(e?.message || '修改失败'); }
|
||||
};
|
||||
|
||||
const handleLdapUserTest = async () => {
|
||||
@@ -508,7 +538,7 @@ export function SystemManager() {
|
||||
<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" />
|
||||
@@ -1264,14 +1294,24 @@ export function SystemManager() {
|
||||
<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>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-slate-400 hover:text-blue-400 hover:bg-blue-500/10"
|
||||
onClick={() => openEditLdapUser(user)}
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
</Button>
|
||||
<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>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
@@ -1286,6 +1326,49 @@ export function SystemManager() {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 编辑 LDAP 用户 Dialog */}
|
||||
<Dialog open={!!editLdapUser} onOpenChange={(v) => !v && setEditLdapUser(null)}>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Edit className="h-5 w-5 text-blue-400" />
|
||||
编辑 LDAP 用户
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
修改 {editLdapUser?.uid} 的属性
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{editLdapUser && (
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">用户名(只读)</label>
|
||||
<Input value={editLdapUser.uid || ''} disabled className="bg-slate-800/50 border-slate-700 text-slate-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">显示名称 (cn)</label>
|
||||
<Input value={editLdapForm.cn} onChange={(e) => setEditLdapForm({ ...editLdapForm, cn: e.target.value })} 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={editLdapForm.mail} onChange={(e) => setEditLdapForm({ ...editLdapForm, mail: e.target.value })} placeholder="user@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">电话</label>
|
||||
<Input value={editLdapForm.phone} onChange={(e) => setEditLdapForm({ ...editLdapForm, phone: e.target.value })} placeholder="13800138000" 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={editLdapForm.title} onChange={(e) => setEditLdapForm({ ...editLdapForm, title: e.target.value })} placeholder="运维工程师" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setEditLdapUser(null)}>取消</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleUpdateLdapUser}>保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* User Detail Dialog */}
|
||||
<Dialog open={!!selectedUser} onOpenChange={() => setSelectedUser(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800">
|
||||
@@ -1476,7 +1559,7 @@ export function SystemManager() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Upload className="h-5 w-5 text-blue-400" />
|
||||
批量导入用户
|
||||
批量导入用户(CSV)
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
上传 CSV 文件批量创建用户。请先下载模板,按格式填写后上传。
|
||||
@@ -1586,16 +1669,33 @@ export function SystemManager() {
|
||||
{!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}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 whitespace-nowrap">选择 OU</span>
|
||||
<select
|
||||
className="bg-slate-800 border border-slate-700 text-slate-200 text-xs rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={ldapImportOUDN}
|
||||
onChange={(e) => {
|
||||
setLdapImportOUDN(e.target.value);
|
||||
loadLdapUsers(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="">根 (全部)</option>
|
||||
{ldapImportOUs.map((ou) => (
|
||||
<option key={ou.dn} value={ou.dn}>{ou.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300 ml-auto" onClick={() => loadLdapUsers(ldapImportOUDN)} 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>
|
||||
|
||||
<p className="text-xs text-slate-500">
|
||||
{ldapUsers.length > 0 ? `找到 ${ldapUsers.length} 个用户` : '选择 OU 或点击刷新获取用户列表'}
|
||||
</p>
|
||||
|
||||
{ldapUsers.length > 0 && (
|
||||
<div className="max-h-80 overflow-auto rounded-lg border border-slate-800">
|
||||
<Table>
|
||||
|
||||
Reference in new issue
Block a user