diff --git a/backend/internal/models/sql_audit.go b/backend/internal/models/sql_audit.go new file mode 100644 index 0000000..28ea9ab --- /dev/null +++ b/backend/internal/models/sql_audit.go @@ -0,0 +1,64 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// SQLAuditRecord SQL审计记录 // Logan.Li +type SQLAuditRecord struct { + ID uint `gorm:"primarykey" json:"id"` + SQLContent string `gorm:"type:text;not null" json:"sql_content"` + SQLType string `gorm:"size:50" json:"sql_type"` // DDL/DML/DQL/DCL + DatabaseName string `gorm:"size:255" json:"database_name"` + TableName string `gorm:"size:255" json:"table_name"` + RiskLevel string `gorm:"size:20;default:safe" json:"risk_level"` // high/medium/low/safe + RiskRules string `gorm:"type:text" json:"risk_rules"` // JSON 数组 + Status string `gorm:"size:20;default:pending" json:"status"` // pending/approved/rejected/executed + Executor string `gorm:"size:255" json:"executor"` + Approver string `gorm:"size:255" json:"approver"` + ExecuteTime *time.Time `json:"execute_time"` + AffectedRows int64 `gorm:"default:0" json:"affected_rows"` + ExecutionTime int64 `gorm:"default:0" json:"execution_time"` // 毫秒 + SourceIP string `gorm:"size:100" json:"source_ip"` + Application string `gorm:"size:255" json:"application"` + TicketID string `gorm:"size:100" json:"ticket_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +func (SQLAuditRecord) TableName() string { return "sql_audit_records" } + +// SQLAuditRule SQL审计规则 // Logan.Li +type SQLAuditRule struct { + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:255;not null" json:"name"` + Description string `gorm:"type:text" json:"description"` + Pattern string `gorm:"type:text" json:"pattern"` // 正则/关键词匹配 + Enabled bool `gorm:"default:true" json:"enabled"` + Severity string `gorm:"size:20;default:medium" json:"severity"` // high/medium/low + Category string `gorm:"size:100" json:"category"` // DDL/DML/DROP/TRUNCATE/etc + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +func (SQLAuditRule) TableName() string { return "sql_audit_rules" } + +// AnalyzeSQLRequest SQL分析请求 // Logan.Li +type AnalyzeSQLRequest struct { + SQL string `json:"sql"` + DatabaseName string `json:"database_name"` + Application string `json:"application"` +} + +// AnalyzeSQLResponse SQL分析响应 // Logan.Li +type AnalyzeSQLResponse struct { + ID int64 `json:"id"` + RiskLevel string `json:"risk_level"` + RiskRules []string `json:"risk_rules"` + SQLType string `json:"sql_type"` + TableName string `json:"table_name"` +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 03e36ce..b895e2f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -30,11 +30,8 @@ import { AIAlertAnalyzer } from '@/components/aiops/AIAlertAnalyzer'; import { AIScriptGenerator } from '@/components/aiops/AIScriptGenerator'; import { AIFaultDiagnosis } from '@/components/aiops/AIFaultDiagnosis'; import { AIPredictiveAnalytics } from '@/components/aiops/AIPredictiveAnalytics'; -import SQLAuditManager from '@/components/audit/SQLAuditManager'; import LogManager from '@/components/monitor/logs/LogManager'; import AlertCenter from '@/components/monitor/alert-center/AlertCenter'; -import TraceManager from '@/components/monitor/trace/TraceManager'; -import GrafanaDashboard from '@/components/monitor/dashboard/GrafanaDashboard'; import { AgentMonitor } from '@/components/agent/AgentMonitor'; import { AgentAlerts } from '@/components/agent/AgentAlerts'; @@ -76,8 +73,6 @@ function App() { } /> } /> } /> - } /> - } /> } /> } /> } /> @@ -94,7 +89,6 @@ function App() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/frontend/src/components/aiops/AIFaultDiagnosis.tsx b/frontend/src/components/aiops/AIFaultDiagnosis.tsx index 0a334d8..2887915 100644 --- a/frontend/src/components/aiops/AIFaultDiagnosis.tsx +++ b/frontend/src/components/aiops/AIFaultDiagnosis.tsx @@ -20,6 +20,7 @@ import { AccordionItem, AccordionTrigger, } from '@/components/ui/accordion'; +import { toast } from 'sonner'; import { Brain, Search, @@ -78,62 +79,6 @@ interface DiagnosisResult { }[]; } -const mockDiagnosisHistory: DiagnosisResult[] = [ - { - id: 'DG-001', - problem: 'Web服务响应缓慢', - symptoms: ['页面加载时间超过5秒', 'CPU使用率飙升至90%', '部分请求超时'], - rootCauses: [ - { - description: '数据库连接池耗尽,导致请求排队等待', - probability: 85, - evidence: ['数据库连接数达到上限100', '连接等待队列长度15', '平均连接等待时间200ms'], - }, - { - description: 'Redis缓存命中率下降', - probability: 60, - evidence: ['缓存命中率从85%降至45%', '大量请求穿透到数据库'], - }, - ], - solutions: [ - { - title: '增加数据库连接池大小', - steps: ['修改配置文件,将max_connections从100调整为200', '重启应用服务', '监控连接数变化'], - difficulty: 'easy', - estimatedTime: '5分钟', - }, - { - title: '优化Redis缓存策略', - steps: ['分析缓存key的访问模式', '调整过期时间策略', '增加热点数据预热'], - difficulty: 'medium', - estimatedTime: '30分钟', - }, - ], - relatedLogs: [ - { - timestamp: '2025-04-14 14:30:15', - level: 'ERROR', - message: 'Connection pool exhausted, waiting for available connection', - source: 'database-pool', - }, - { - timestamp: '2025-04-14 14:30:12', - level: 'WARN', - message: 'Request timeout after 5000ms', - source: 'http-server', - }, - ], - similarCases: [ - { - id: 'CASE-2025-032', - title: '数据库连接池耗尽问题', - resolution: '增加连接池大小并优化连接释放逻辑', - date: '2025-03-15', - }, - ], - }, -]; - const commonProblems = [ { id: 'slow', name: '服务响应缓慢', icon: Clock }, { id: 'crash', name: '应用崩溃/重启', icon: XCircle }, @@ -150,19 +95,11 @@ export function AIFaultDiagnosis() { const [isDiagnosing, setIsDiagnosing] = useState(false); const [diagnosisResult, setDiagnosisResult] = useState(null); const [selectedProblem, setSelectedProblem] = useState(null); - const [diagnosisHistory, setDiagnosisHistory] = useState(mockDiagnosisHistory); + const [diagnosisHistory, setDiagnosisHistory] = useState([]); const handleDiagnose = () => { if (!symptomInput.trim()) return; - - setIsDiagnosing(true); - setDiagnosisResult(null); - - // 模拟AI诊断 - setTimeout(() => { - setDiagnosisResult(mockDiagnosisHistory[0]); - setIsDiagnosing(false); - }, 2500); + toast.info('AI 诊断功能需配置 LLM 服务'); }; const handleProblemSelect = (problemId: string) => { @@ -458,32 +395,39 @@ export function AIFaultDiagnosis() { 诊断历史 -
- {diagnosisHistory.map((item) => ( -
setDiagnosisResult(item)} - > -
-
-
- -
-
-

{item.problem}

-

- {item.symptoms.length} 个症状 · {item.rootCauses.length} 个根因 · {item.solutions.length} 个方案 -

+ {diagnosisHistory.length === 0 ? ( +
+ +

暂无诊断记录

+
+ ) : ( +
+ {diagnosisHistory.map((item) => ( +
setDiagnosisResult(item)} + > +
+
+
+ +
+
+

{item.problem}

+

+ {item.symptoms.length} 个症状 · {item.rootCauses.length} 个根因 · {item.solutions.length} 个方案 +

+
+
-
-
- ))} -
+ ))} +
+ )} diff --git a/frontend/src/components/aiops/AIOpsAssistant.tsx b/frontend/src/components/aiops/AIOpsAssistant.tsx index 262d4c2..9fd9ac1 100644 --- a/frontend/src/components/aiops/AIOpsAssistant.tsx +++ b/frontend/src/components/aiops/AIOpsAssistant.tsx @@ -23,6 +23,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { toast } from 'sonner'; import { Bot, Send, @@ -65,23 +66,8 @@ interface LLMConfig { maxTokens: number; } -const mockLLMProviders = [ - { id: 'openai', name: 'OpenAI', models: ['gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo'] }, - { id: 'anthropic', name: 'Anthropic', models: ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku'] }, - { id: 'azure', name: 'Azure OpenAI', models: ['gpt-4', 'gpt-35-turbo'] }, - { id: 'ollama', name: 'Ollama (本地)', models: ['llama2', 'codellama', 'mistral'] }, - { id: 'custom', name: '自定义', models: ['custom'] }, -]; - -const mockConversations: Message[] = [ - { - id: '1', - role: 'assistant', - content: '你好!我是 EZOps AI 智能助手,可以帮助你:\n\n🔍 **故障诊断** - 分析告警原因,提供解决方案\n📊 **性能分析** - 识别系统瓶颈,优化建议\n📝 **运维知识** - 解答技术问题,最佳实践\n🤖 **自动化建议** - 推荐自动化脚本和方案\n\n请问有什么可以帮助你的?', - timestamp: '2025-04-14 10:00:00', - suggestions: ['查看今日告警', '分析服务器性能', '生成巡检报告'], - }, -]; +// LLM 提供商列表(需后端配置后填充) +const llmProviders: { id: string; name: string; models: string[] }[] = []; const quickActions = [ { id: 'alerts', name: '分析今日告警', icon: AlertTriangle, description: '分析今日所有告警事件' }, @@ -91,13 +77,13 @@ const quickActions = [ ]; export function AIOpsAssistant() { - const [messages, setMessages] = useState(mockConversations); + const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isConfigOpen, setIsConfigOpen] = useState(false); const [llmConfig, setLlmConfig] = useState({ - provider: 'openai', - model: 'gpt-4', + provider: '', + model: '', apiKey: '', endpoint: '', temperature: 0.7, @@ -125,39 +111,16 @@ export function AIOpsAssistant() { setMessages((prev) => [...prev, userMessage]); setInput(''); - setIsLoading(true); - // Mock AI response - setTimeout(() => { - const responses: Record = { - '告警': '根据今日告警数据分析:\n\n**严重告警 (2)**\n- 服务器 web-server-03 CPU 使用率超过 95%\n- 数据库主库连接数接近上限\n\n**建议处理方案:**\n1. 立即扩容 web-server-03 或迁移部分流量\n2. 检查数据库慢查询,优化连接池配置\n3. 考虑启用读写分离', - '性能': '系统性能分析结果:\n\n**CPU 使用率**\n- 平均: 45%\n- 峰值: 92% (14:30)\n- 建议: 高峰期需要扩容\n\n**内存使用**\n- 平均: 68%\n- 建议: 正常范围内\n\n**磁盘 I/O**\n- 数据库服务器 I/O 等待较高\n- 建议: 考虑使用 SSD 或优化查询', - '报告': '## 每日运维报告 (2025-04-14)\n\n### 📊 系统状态\n- 服务器总数: 156\n- 运行中: 142\n- 告警事件: 23\n\n### 🚨 告警统计\n- 严重: 2\n- 警告: 8\n- 提示: 13\n\n### 📈 今日发布\n- 成功: 5\n- 失败: 1\n- 回滚: 0', - '优化': '资源优化建议:\n\n**成本优化**\n1. 关闭 5 台闲置测试服务器,预计月节省 ¥2,400\n2. 调整 3 台服务器规格,预计月节省 ¥1,800\n\n**性能优化**\n1. 数据库添加索引,预计查询性能提升 40%\n2. Redis 缓存命中率仅 65%,建议优化缓存策略\n\n**自动化建议**\n1. 部署自动扩缩容策略\n2. 配置告警自动处理规则', - }; - - let responseContent = '我已收到你的问题。让我分析一下...\n\n'; - for (const [key, value] of Object.entries(responses)) { - if (input.includes(key)) { - responseContent = value; - break; - } - } - if (responseContent === '我已收到你的问题。让我分析一下...\n\n') { - responseContent += '这是一个很好的问题。根据当前系统状态,我建议:\n\n1. 查看相关监控指标\n2. 分析日志信息\n3. 制定处理方案\n\n需要我提供更详细的分析吗?'; - } - - const assistantMessage: Message = { - id: (Date.now() + 1).toString(), - role: 'assistant', - content: responseContent, - timestamp: new Date().toISOString(), - suggestions: ['查看更多详情', '生成处理脚本', '创建工单'], - }; - - setMessages((prev) => [...prev, assistantMessage]); - setIsLoading(false); - }, 1500); + // AI 功能需配置 LLM 接口,暂不返回模拟回复 + toast.info('AI 功能需配置 LLM 接口,请在「模型配置」中完成设置'); + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: '当前未配置 LLM 服务,无法提供智能回复。请在右上角「模型配置」中配置 LLM 接口后重试。', + timestamp: new Date().toISOString(), + }; + setMessages((prev) => [...prev, assistantMessage]); }; const handleQuickAction = (actionId: string) => { @@ -260,6 +223,13 @@ export function AIOpsAssistant() {
+ {messages.length === 0 && !isLoading && ( +
+ +

暂无对话记录

+

输入问题开始与 AI 助手对话

+
+ )} {messages.map((message) => (
- {mockLLMProviders.map((p) => ( + {llmProviders.map((p) => ( {p.name} ))} + {llmProviders.length === 0 && ( +
暂无可用的 LLM 提供商
+ )}
@@ -372,13 +345,16 @@ export function AIOpsAssistant() { - {mockLLMProviders + {llmProviders .find((p) => p.id === llmConfig.provider) ?.models.map((m) => ( {m} ))} + {!llmProviders.find((p) => p.id === llmConfig.provider)?.models?.length && ( +
请先选择提供商
+ )}
diff --git a/frontend/src/components/aiops/AIScriptGenerator.tsx b/frontend/src/components/aiops/AIScriptGenerator.tsx index b892721..7d47192 100644 --- a/frontend/src/components/aiops/AIScriptGenerator.tsx +++ b/frontend/src/components/aiops/AIScriptGenerator.tsx @@ -15,6 +15,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { toast } from 'sonner'; import { Dialog, DialogContent, @@ -109,226 +110,18 @@ const scriptTemplates: ScriptTemplate[] = [ }, ]; -const mockGeneratedScripts: GeneratedScript[] = [ - { - id: '1', - name: '服务器巡检脚本', - description: '自动检查服务器CPU、内存、磁盘状态', - language: 'bash', - code: `#!/bin/bash -# 服务器健康检查脚本 -# 生成时间: 2025-04-14 - -echo "========== 服务器健康检查报告 ==========" -echo "检查时间: $(date)" -echo "主机名: $(hostname)" -echo "" - -# CPU检查 -echo "【CPU 状态】" -CPU_USAGE=\$(top -bn1 | grep "Cpu(s)" | awk '{print \$2}' | cut -d'%' -f1) -echo "CPU使用率: \${CPU_USAGE}%" -if (( \$(echo "\$CPU_USAGE > 80" | bc -l) )); then - echo "⚠️ 警告: CPU使用率过高!" -fi -echo "" - -# 内存检查 -echo "【内存 状态】" -free -h -echo "" - -# 磁盘检查 -echo "【磁盘 状态】" -df -h | grep -E '^/dev' -echo "" - -# 网络检查 -echo "【网络连接】" -echo "活动连接数: $(netstat -an | grep ESTABLISHED | wc -l)" -echo "" - -echo "========== 检查完成 =========="`, - createdAt: '2025-04-14 10:30:00', - tags: ['巡检', '监控', 'Bash'], - }, - { - id: '2', - name: 'MySQL慢查询分析', - description: '分析MySQL慢查询日志', - language: 'python', - code: `#!/usr/bin/env python3 -# MySQL慢查询分析脚本 - -import re -from collections import Counter -from datetime import datetime - -def analyze_slow_query_log(log_file): - """分析MySQL慢查询日志""" - queries = [] - - with open(log_file, 'r') as f: - content = f.read() - - # 解析查询时间 - pattern = r'# Query_time: ([\d.]+).*?SELECT.*?FROM.*?WHERE(.*?);' - matches = re.findall(pattern, content, re.DOTALL) - - for query_time, where_clause in matches: - queries.append({ - 'time': float(query_time), - 'where': where_clause.strip() - }) - - # 统计最慢的查询 - queries.sort(key=lambda x: x['time'], reverse=True) - - print(f"总共发现 {len(queries)} 条慢查询") - print("\n最慢的5条查询:") - for i, q in enumerate(queries[:5], 1): - print(f"{i}. 耗时: {q['time']}s") - print(f" WHERE: {q['where'][:100]}...") - -if __name__ == '__main__': - analyze_slow_query_log('/var/log/mysql/slow.log')`, - createdAt: '2025-04-14 11:15:00', - tags: ['MySQL', 'Python', '性能分析'], - }, -]; - export function AIScriptGenerator() { const [prompt, setPrompt] = useState(''); const [isGenerating, setIsGenerating] = useState(false); const [generatedScript, setGeneratedScript] = useState(null); const [selectedLanguage, setSelectedLanguage] = useState('bash'); - const [savedScripts, setSavedScripts] = useState(mockGeneratedScripts); + const [savedScripts, setSavedScripts] = useState([]); const [copied, setCopied] = useState(false); const [activeTab, setActiveTab] = useState('generate'); const handleGenerate = async () => { if (!prompt.trim()) return; - - setIsGenerating(true); - - // 模拟AI生成脚本 - setTimeout(() => { - const mockScripts: Record = { - bash: { - id: Date.now().toString(), - name: '自动生成的运维脚本', - description: prompt, - language: 'bash', - code: `#!/bin/bash -# AI生成的运维脚本 -# 描述: ${prompt} -# 生成时间: ${new Date().toLocaleString()} - -echo "🚀 开始执行运维任务..." - -# 检查系统信息 -echo "系统信息:" -echo "- 主机名: $(hostname)" -echo "- 操作系统: $(uname -o)" -echo "- 内核版本: $(uname -r)" - -# 根据用户需求定制的功能 -echo "" -echo "📊 执行任务: ${prompt}" - -# TODO: 在这里添加具体的业务逻辑 -# 这是一个AI生成的脚本框架,请根据实际需求修改 - -echo "" -echo "✅ 任务执行完成"`, - createdAt: new Date().toISOString(), - tags: ['AI生成', 'Bash'], - }, - python: { - id: Date.now().toString(), - name: '自动生成的Python脚本', - description: prompt, - language: 'python', - code: `#!/usr/bin/env python3 -# AI生成的运维脚本 -# 描述: ${prompt} -# 生成时间: ${new Date().toLocaleString()} - -import os -import sys -import subprocess -from datetime import datetime - -def main(): - """主函数""" - print("🚀 开始执行运维任务...") - - # 系统信息 - print("\\n系统信息:") - print(f"- 当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - print(f"- 工作目录: {os.getcwd()}") - print(f"- Python版本: {sys.version}") - - # 执行任务 - print(f"\\n📊 执行任务: ${prompt}") - - # TODO: 在这里添加具体的业务逻辑 - # 这是一个AI生成的脚本框架,请根据实际需求修改 - - print("\\n✅ 任务执行完成") - -if __name__ == '__main__': - main()`, - createdAt: new Date().toISOString(), - tags: ['AI生成', 'Python'], - }, - ansible: { - id: Date.now().toString(), - name: '自动生成的Ansible Playbook', - description: prompt, - language: 'yaml', - code: `--- -# AI生成的Ansible Playbook -# 描述: ${prompt} -# 生成时间: ${new Date().toLocaleString()} - -- name: AI生成的运维任务 - hosts: all - become: yes - gather_facts: yes - - vars: - task_description: "${prompt}" - - tasks: - - name: 显示任务信息 - debug: - msg: "开始执行: {{ task_description }}" - - - name: 收集系统信息 - setup: - - - name: 显示主机信息 - debug: - msg: - - "主机名: {{ ansible_hostname }}" - - "操作系统: {{ ansible_distribution }} {{ ansible_distribution_version }}" - - "IP地址: {{ ansible_default_ipv4.address }}" - - # TODO: 在这里添加具体的任务 - # 这是一个AI生成的Playbook框架,请根据实际需求修改 - - - name: 任务完成 - debug: - msg: "✅ 任务执行完成"`, - createdAt: new Date().toISOString(), - tags: ['AI生成', 'Ansible'], - }, - }; - - setGeneratedScript(mockScripts[selectedLanguage] || mockScripts.bash); - setIsGenerating(false); - }, 2000); + toast.info('AI 脚本生成功能需配置 LLM 服务'); }; const handleTemplateClick = (template: ScriptTemplate) => { @@ -533,39 +326,46 @@ if __name__ == '__main__': 历史生成的脚本 -
- {savedScripts.map((script) => ( -
-
-
-
- {getLanguageIcon(script.language)} -
-
-

{script.name}

-

{script.description}

-
- - {script.language.toUpperCase()} - - {script.createdAt} + {savedScripts.length === 0 ? ( +
+ +

暂无生成的脚本

+
+ ) : ( +
+ {savedScripts.map((script) => ( +
+
+
+
+ {getLanguageIcon(script.language)} +
+
+

{script.name}

+

{script.description}

+
+ + {script.language.toUpperCase()} + + {script.createdAt} +
-
-
- {script.tags.map((tag) => ( - - {tag} - - ))} +
+ {script.tags.map((tag) => ( + + {tag} + + ))} +
-
- ))} -
+ ))} +
+ )} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 7c7ba79..b7dad1e 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -28,12 +28,10 @@ import { Key, Compass, Brain, - ScrollText, FileTerminal, Activity, BarChart3, GitBranch, - Monitor, Bot, AlertTriangle, } from 'lucide-react'; @@ -92,8 +90,6 @@ const menuItems: MenuItem[] = [ { id: 'metrics', name: '指标监控', icon: Cpu, path: '/metrics' }, { id: 'alert-center', name: '告警中心', icon: Bell, path: '/alert-center' }, { id: 'logs-monitor', name: '日志能力', icon: FileCode, path: '/logs-monitor' }, - { id: 'trace', name: '链路追踪', icon: GitBranch, path: '/trace' }, - { id: 'grafana', name: '监控大屏', icon: Monitor, path: '/grafana' }, { id: 'agent-alerts', name: 'Agent告警', icon: AlertTriangle, path: '/agent-alerts' }, ], }, @@ -120,7 +116,6 @@ const menuItems: MenuItem[] = [ { id: 'ai-script-generator', name: 'AI脚本生成', icon: FileCode, path: '/ai-script-generator' }, { id: 'ai-fault-diagnosis', name: '故障智能诊断', icon: Activity, path: '/ai-fault-diagnosis' }, { id: 'ai-predictive', name: 'AI预测分析', icon: BarChart3, path: '/ai-predictive' }, - { id: 'sql-audit', name: 'SQL审计', icon: ScrollText, path: '/sql-audit' }, ], }, { diff --git a/frontend/src/components/monitor/alert-center/AlertCenter.tsx b/frontend/src/components/monitor/alert-center/AlertCenter.tsx index 4d80155..31ce844 100644 --- a/frontend/src/components/monitor/alert-center/AlertCenter.tsx +++ b/frontend/src/components/monitor/alert-center/AlertCenter.tsx @@ -121,7 +121,7 @@ const AlertCenter: React.FC = () => { const fetchRules = async () => { try { const res = await alertCenterApi.getRules(); - setRules(res.data || []); + setRules(res.list || []); } catch (error) { toast.error('获取告警规则失败'); } diff --git a/frontend/src/components/server/BatchImport.tsx b/frontend/src/components/server/BatchImport.tsx index 2f0c530..a7499f1 100644 --- a/frontend/src/components/server/BatchImport.tsx +++ b/frontend/src/components/server/BatchImport.tsx @@ -1,4 +1,5 @@ import { useState, useCallback } from 'react'; +import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -62,82 +63,13 @@ export function BatchImport() { const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); - // 模拟文件解析 - generateMockRecords(); + toast.info('文件解析功能开发中'); }, []); - const generateMockRecords = () => { - const mockRecords: ImportRecord[] = [ - { - id: '1', - hostname: 'web-server-03', - ip: '192.168.1.111', - os: 'CentOS 7.9', - cpu: '16', - memory: '64', - disk: '500', - region: '华北-北京', - tags: 'web,production', - status: 'valid', - }, - { - id: '2', - hostname: 'app-server-03', - ip: '192.168.1.112', - os: 'Ubuntu 22.04 LTS', - cpu: '32', - memory: '128', - disk: '1000', - region: '华东-上海', - tags: 'app,production', - status: 'valid', - }, - { - id: '3', - hostname: 'cache-server-02', - ip: '192.168.1.113', - os: 'Ubuntu 20.04 LTS', - cpu: '16', - memory: '64', - disk: '200', - region: '华南-广州', - tags: 'cache,redis', - status: 'valid', - }, - { - id: '4', - hostname: '', - ip: '192.168.1.114', - os: 'CentOS 8.5', - cpu: '8', - memory: '32', - disk: '500', - region: '华北-北京', - tags: 'test', - status: 'invalid', - message: '主机名不能为空', - }, - { - id: '5', - hostname: 'invalid-server', - ip: 'invalid-ip', - os: 'Ubuntu 22.04 LTS', - cpu: '16', - memory: '64', - disk: '500', - region: '华东-上海', - tags: 'dev', - status: 'invalid', - message: 'IP地址格式不正确', - }, - ]; - setRecords(mockRecords); - }; - const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { - generateMockRecords(); + toast.info('文件解析功能开发中'); } }; @@ -238,6 +170,15 @@ export function BatchImport() { + {/* Empty State */} + {records.length === 0 && ( +
+ +

暂无导入记录

+

上传文件后将在此预览数据

+
+ )} + {/* Import Stats */} {records.length > 0 && (
diff --git a/frontend/src/components/server/CabinetView.tsx b/frontend/src/components/server/CabinetView.tsx index 93d9f3f..5ff8d1a 100644 --- a/frontend/src/components/server/CabinetView.tsx +++ b/frontend/src/components/server/CabinetView.tsx @@ -1,9 +1,8 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, @@ -23,135 +22,73 @@ import { Server, Plus, Search, - MapPin, - Zap, Grid3X3, List, + MapPin, + Cpu, + MemoryStick, HardDrive, - Network, - Router, - Shield, - Database, - Monitor, - Edit, - Trash2, Eye, - ArrowUp, - ArrowDown, + Edit, } from 'lucide-react'; -import { cabinets, devices } from '@/mocks/data'; -import type { Cabinet, Device } from '@/types'; +import { serverApi } from '@/services/api'; +import type { Server as ServerType } from '@/types'; export function CabinetView() { - const [viewMode, setViewMode] = useState<'room' | 'cabinet' | 'list'>('room'); - const [selectedRoom, setSelectedRoom] = useState('all'); - const [selectedCabinet, setSelectedCabinet] = useState(null); - const [selectedDevice, setSelectedDevice] = useState(null); + const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); + const [statusFilter, setStatusFilter] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); + const [servers, setServers] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedServer, setSelectedServer] = useState(null); - const rooms = Array.from(new Set(cabinets.map((c) => c.room))); - - const filteredCabinets = cabinets.filter((cabinet) => { - const matchesRoom = selectedRoom === 'all' || cabinet.room === selectedRoom; - const matchesSearch = cabinet.name.toLowerCase().includes(searchQuery.toLowerCase()); - return matchesRoom && matchesSearch; + // 拉取服务器列表 // Logan.Li + const fetchServers = async () => { + setLoading(true); + try { + const resp = await serverApi.getServers({ page: 1, page_size: 200 }); + setServers((resp?.list ?? []) as ServerType[]); + } catch { + setServers([]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchServers(); + }, []); + + const filteredServers = servers.filter((srv) => { + const matchesStatus = statusFilter === 'all' || srv.status === statusFilter; + const matchesSearch = + srv.hostname?.toLowerCase().includes(searchQuery.toLowerCase()) || + srv.ip?.toLowerCase().includes(searchQuery.toLowerCase()); + return matchesStatus && matchesSearch; }); - const getCabinetDevices = (cabinetId: string) => { - return devices.filter((d) => d.cabinetId === cabinetId).sort((a, b) => b.startU - a.startU); - }; - - const getDeviceIcon = (type: Device['type']) => { - const icons = { - server: , - switch: , - router: , - firewall: , - storage: , - other: , + const getStatusColor = (status: string) => { + const colors: Record = { + running: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10', + stopped: 'border-red-500/30 text-red-400 bg-red-500/10', + maintenance: 'border-amber-500/30 text-amber-400 bg-amber-500/10', + offline: 'border-slate-500/30 text-slate-400 bg-slate-500/10', }; - return icons[type]; + return colors[status] ?? colors.offline; }; - const getDeviceColor = (type: Device['type']) => { - const colors = { - server: 'bg-blue-500/20 text-blue-400 border-blue-500/30', - switch: 'bg-purple-500/20 text-purple-400 border-purple-500/30', - router: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30', - firewall: 'bg-red-500/20 text-red-400 border-red-500/30', - storage: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', - other: 'bg-slate-500/20 text-slate-400 border-slate-500/30', + const getStatusText = (status: string) => { + const map: Record = { + running: '运行中', + stopped: '已停止', + maintenance: '维护中', + offline: '离线', }; - return colors[type]; + return map[status] ?? status; }; - const getStatusColor = (status: Cabinet['status']) => { - const colors = { - active: 'border-emerald-500/50 bg-emerald-500/5', - empty: 'border-slate-600 bg-slate-800/30', - disabled: 'border-slate-700 bg-slate-800/50 opacity-50', - }; - return colors[status]; - }; - - // 渲染机柜正面视图 - const renderCabinetFront = (cabinet: Cabinet) => { - const cabinetDevices = getCabinetDevices(cabinet.id); - const uSlots: (Device | null)[] = new Array(cabinet.totalU).fill(null); - - cabinetDevices.forEach((device) => { - for (let i = 0; i < device.sizeU; i++) { - const uIndex = cabinet.totalU - device.startU - i; - if (uIndex >= 0 && uIndex < cabinet.totalU) { - uSlots[uIndex] = device; - } - } - }); - - return ( -
- {/* U位编号 */} -
- {Array.from({ length: cabinet.totalU }, (_, i) => ( -
- {cabinet.totalU - i}U -
- ))} -
- - {/* 机柜 */} -
- {uSlots.map((device, index) => { - const uNumber = cabinet.totalU - index; - if (device) { - const isFirstU = uNumber === device.startU + device.sizeU - 1; - if (!isFirstU) return null; - - return ( -
setSelectedDevice(device)} - > - {getDeviceIcon(device.type)} - {device.name} - {device.ip} -
- ); - } - - return ( -
- ); - })} -
-
- ); - }; + const getUsageColor = (usage: number) => + usage > 80 ? 'bg-red-400' : usage > 60 ? 'bg-amber-400' : 'bg-emerald-400'; return (
@@ -159,18 +96,12 @@ export function CabinetView() {

机房机柜视图

-

数据中心机房与机柜设备管理

-
-
- - +

数据中心服务器资产概览

+
{/* Stats Cards */} @@ -179,36 +110,10 @@ export function CabinetView() {
-

机房数

-

{rooms.length}

+

服务器总数

+

{servers.length}

- -
-
-
- - - -
-
-

机柜数

-

{cabinets.length}

-
-
- -
-
-
-
- - -
-
-

设备数

-

{devices.length}

-
-
@@ -218,13 +123,43 @@ export function CabinetView() {
-

总功率

+

运行中

+

+ {servers.filter((s) => s.status === 'running').length} +

+
+
+ +
+
+
+
+ + +
+
+

维护中

- {cabinets.reduce((acc, c) => acc + c.powerUsed, 0).toFixed(1)}kW + {servers.filter((s) => s.status === 'maintenance').length}

- + +
+
+
+
+ + +
+
+

离线

+

+ {servers.filter((s) => s.status === 'offline' || s.status === 'stopped').length} +

+
+
+
@@ -237,44 +172,34 @@ export function CabinetView() {
setSearchQuery(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" />
- - + - 全部机房 - {rooms.map((room) => ( - - {room} - - ))} + 全部状态 + 运行中 + 已停止 + 维护中 + 离线
- -
- - - {renderCabinetFront(selectedCabinet)} - - -
-
- - - 机柜信息 - - -
-
-

机柜编号

-

{selectedCabinet.name}

-
-
-

所属机房

-

{selectedCabinet.room}

-
-
-

位置

-

第{selectedCabinet.row}排 第{selectedCabinet.column}列

-
-
-

状态

- - {selectedCabinet.status === 'active' ? '使用中' : selectedCabinet.status === 'empty' ? '空闲' : '停用'} - -
+ )}
-
+ ))}
- )} - - {viewMode === 'list' && ( + ) : (
- - - - + + + + - {devices.map((device) => ( - + {filteredServers.map((srv) => ( + + + + + - - - - @@ -275,35 +271,45 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) { - {mockCloudServers.map((server) => ( - - - - - - - - + - ))} + ) : ( + cloudServers.map((server) => ( + + + + + + + + + + )) + )}
设备名称类型机柜U位主机名 IP地址系统区域配置 状态 操作
- {getDeviceIcon(device.type)} - {device.name} + + {srv.hostname || '--'}
{srv.ip || '--'}{srv.os || '--'}{srv.region || '--'} + {srv.cpu ?? 0}核 / {srv.memory ?? 0}GB + - - {device.type} - - - {cabinets.find((c) => c.id === device.cabinetId)?.name} - - {device.startU}U - {device.startU + device.sizeU - 1}U ({device.sizeU}U) - {device.ip || '--'} - - {device.status === 'online' ? '在线' : device.status === 'offline' ? '离线' : '维护中'} + + {getStatusText(srv.status)}
- 已选 {selectedServers.length} 台 @@ -261,8 +257,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
- 0} + 0} onCheckedChange={handleSelectAll} />
- handleServerToggle(server.id)} - /> - {server.name}{server.hostname}{server.ip}{server.region}{server.spec} - - {server.status === 'running' ? '运行中' : '已停止'} - + {cloudServers.length === 0 ? ( +
+ +

暂无云主机

+

云主机发现功能尚未实现,请手动添加服务器

+ handleServerToggle(server.id)} + /> + {server.name}{server.hostname}{server.ip}{server.region}{server.spec} + + {server.status === 'running' ? '运行中' : '已停止'} + +
diff --git a/frontend/src/components/server/ExcelServerImport.tsx b/frontend/src/components/server/ExcelServerImport.tsx index c2e538c..75a2246 100644 --- a/frontend/src/components/server/ExcelServerImport.tsx +++ b/frontend/src/components/server/ExcelServerImport.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -20,7 +20,13 @@ import { ChevronRight, ChevronLeft, } from 'lucide-react'; -import { regions } from '@/mocks/data'; +import { regionApi } from '@/services/api'; + +interface RegionOption { + id: string; + name: string; + code: string; +} interface ExcelServerImportProps { open: boolean; @@ -32,13 +38,10 @@ const connectionMethods = [ { id: 'key', name: '密钥认证', description: '使用SSH密钥连接' }, ]; -const mockPreviewData = [ - { row: 2, hostname: 'web-server-01', ip: '192.168.1.101', port: 22, username: 'root', status: 'valid', message: '验证通过' }, - { row: 3, hostname: 'web-server-02', ip: '192.168.1.102', port: 22, username: 'root', status: 'valid', message: '验证通过' }, - { row: 4, hostname: 'db-server-01', ip: '192.168.1.201', port: 22, username: 'admin', status: 'warning', message: 'IP格式正确' }, - { row: 5, hostname: '', ip: '192.168.1.999', port: 22, username: 'root', status: 'error', message: '主机名不能为空' }, - { row: 6, hostname: 'cache-server-01', ip: '192.168.1.103', port: 22, username: 'root', status: 'valid', message: '验证通过' }, -]; +type PreviewRow = { + row: number; hostname: string; ip: string; port: number; + username: string; status: string; message: string; +}; export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) { const [step, setStep] = useState(1); @@ -46,14 +49,40 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) { const [connectionMethod, setConnectionMethod] = useState('password'); const [selectedFile, setSelectedFile] = useState(null); const [isUploading, setIsUploading] = useState(false); - const [previewData, setPreviewData] = useState([]); + const [previewData, setPreviewData] = useState([]); + const [regions, setRegions] = useState([]); + + // 对话框打开时拉取区域列表 // Logan.Li + useEffect(() => { + if (!open) return; + let cancelled = false; + (async () => { + try { + const resp = await regionApi.list(); + if (cancelled) return; + const list = resp?.list ?? []; + setRegions( + list.map((r: any) => ({ + id: String(r.id ?? ''), + name: r.name ?? '', + code: r.code ?? '', + })) + ); + } catch { + if (!cancelled) setRegions([]); + } + })(); + return () => { + cancelled = true; + }; + }, [open]); const handleNext = () => { if (step === 2 && selectedFile) { setIsUploading(true); setTimeout(() => { setIsUploading(false); - setPreviewData(mockPreviewData); + setPreviewData([]); // 真实解析由后端处理 setStep(step + 1); }, 1500); } else { diff --git a/frontend/src/components/system/SystemLogsTab.tsx b/frontend/src/components/system/SystemLogsTab.tsx index 4bcf77a..fc7697a 100644 --- a/frontend/src/components/system/SystemLogsTab.tsx +++ b/frontend/src/components/system/SystemLogsTab.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -11,7 +12,7 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; -import { operationLogs } from '@/mocks/data'; +import { auditApi } from '@/services/api'; import type { OperationLog } from '@/types'; interface SystemLogsTabProps { @@ -19,8 +20,45 @@ interface SystemLogsTabProps { setSearchQuery: (v: string) => void; } +// 将后端审计日志规范化为前端 OperationLog 类型 // Logan.Li +function normalizeLog(raw: any): OperationLog { + return { + id: String(raw.id ?? ''), + userId: String(raw.user_id ?? raw.userId ?? ''), + username: raw.username ?? '', + module: raw.module ?? raw.resource ?? '', + action: raw.action ?? '', + resource: raw.resource ?? '', + ip: raw.ip ?? '', + userAgent: raw.user_agent ?? raw.userAgent ?? '', + result: raw.result === 'failure' ? 'failure' : 'success', + message: raw.message ?? raw.detail, + createdAt: raw.createdAt ?? raw.created_at ?? '', + }; +} + export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProps) { - const filteredLogs = operationLogs.filter( + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchLogs = async () => { + setLoading(true); + try { + const resp = await auditApi.getLogs({ page: 1, page_size: 50 }); + const list = resp?.list ?? []; + setLogs(list.map(normalizeLog)); + } catch { + setLogs([]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchLogs(); + }, []); + + const filteredLogs = logs.filter( (log) => log.username.toLowerCase().includes(searchQuery.toLowerCase()) || log.module.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -52,7 +90,11 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" />
- @@ -62,48 +104,59 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp
- - - - 时间 - 用户 - 模块 - 操作 - 资源 - IP地址 - 结果 - - - - {filteredLogs.map((log) => ( - - - - {new Date(log.createdAt).toLocaleString()} - - - - {log.username} - - - {log.module} - - - {log.action} - - - - {log.resource} - - - - {log.ip} - - {getLogResultBadge(log.result)} + {loading ? ( +
+
+
+ ) : filteredLogs.length === 0 ? ( +
+ +

暂无操作日志

+
+ ) : ( +
+ + + 时间 + 用户 + 模块 + 操作 + 资源 + IP地址 + 结果 - ))} - -
+ + + {filteredLogs.map((log) => ( + + + + {log.createdAt ? new Date(log.createdAt).toLocaleString() : '--'} + + + + {log.username || '--'} + + + {log.module || '--'} + + + {log.action || '--'} + + + + {log.resource || '--'} + + + + {log.ip || '--'} + + {getLogResultBadge(log.result)} + + ))} + + + )}