feat: 清除全部mock数据,Ansible/日志/告警中心/角色权限接真实API;移除SQL审计(不属于项目定位)
This commit is contained in:
1 parent
73b5a48df9
commit
df697e0b25
12 files changed
+586
-896
No files matched your search
@@ -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"`
|
||||
}
|
||||
@@ -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() {
|
||||
<Route path="metrics" element={<AlertManager />} />
|
||||
<Route path="alert-center" element={<AlertCenter />} />
|
||||
<Route path="logs-monitor" element={<LogManager />} />
|
||||
<Route path="trace" element={<TraceManager />} />
|
||||
<Route path="grafana" element={<GrafanaDashboard />} />
|
||||
<Route path="agent-alerts" element={<AgentAlerts />} />
|
||||
<Route path="alerts" element={<AlertManager />} />
|
||||
<Route path="logs" element={<AlertManager />} />
|
||||
@@ -94,7 +89,6 @@ function App() {
|
||||
<Route path="ai-script-generator" element={<AIScriptGenerator />} />
|
||||
<Route path="ai-fault-diagnosis" element={<AIFaultDiagnosis />} />
|
||||
<Route path="ai-predictive" element={<AIPredictiveAnalytics />} />
|
||||
<Route path="sql-audit" element={<SQLAuditManager />} />
|
||||
<Route path="users" element={<SystemManager />} />
|
||||
<Route path="roles" element={<SystemManager />} />
|
||||
<Route path="logs-audit" element={<SystemManager />} />
|
||||
|
||||
@@ -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<DiagnosisResult | null>(null);
|
||||
const [selectedProblem, setSelectedProblem] = useState<string | null>(null);
|
||||
const [diagnosisHistory, setDiagnosisHistory] = useState<DiagnosisResult[]>(mockDiagnosisHistory);
|
||||
const [diagnosisHistory, setDiagnosisHistory] = useState<DiagnosisResult[]>([]);
|
||||
|
||||
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() {
|
||||
<CardTitle className="text-slate-100">诊断历史</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{diagnosisHistory.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-4 rounded-lg border border-slate-700 bg-slate-800/50 hover:border-slate-600 cursor-pointer"
|
||||
onClick={() => setDiagnosisResult(item)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center">
|
||||
<Brain className="h-5 w-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-100">{item.problem}</h4>
|
||||
<p className="text-sm text-slate-400">
|
||||
{item.symptoms.length} 个症状 · {item.rootCauses.length} 个根因 · {item.solutions.length} 个方案
|
||||
</p>
|
||||
{diagnosisHistory.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<History className="h-10 w-10 mb-3 opacity-30" />
|
||||
<p>暂无诊断记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{diagnosisHistory.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-4 rounded-lg border border-slate-700 bg-slate-800/50 hover:border-slate-600 cursor-pointer"
|
||||
onClick={() => setDiagnosisResult(item)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center">
|
||||
<Brain className="h-5 w-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-100">{item.problem}</h4>
|
||||
<p className="text-sm text-slate-400">
|
||||
{item.symptoms.length} 个症状 · {item.rootCauses.length} 个根因 · {item.solutions.length} 个方案
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400">
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400">
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
@@ -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<Message[]>(mockConversations);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
||||
const [llmConfig, setLlmConfig] = useState<LLMConfig>({
|
||||
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<string, string> = {
|
||||
'告警': '根据今日告警数据分析:\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() {
|
||||
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div className="space-y-4">
|
||||
{messages.length === 0 && !isLoading && (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<MessageSquare className="h-10 w-10 mb-3 opacity-30" />
|
||||
<p>暂无对话记录</p>
|
||||
<p className="text-xs mt-1">输入问题开始与 AI 助手对话</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
@@ -354,11 +324,14 @@ export function AIOpsAssistant() {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
{mockLLMProviders.map((p) => (
|
||||
{llmProviders.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id} className="text-slate-100">
|
||||
{p.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{llmProviders.length === 0 && (
|
||||
<div className="px-3 py-2 text-sm text-slate-500">暂无可用的 LLM 提供商</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -372,13 +345,16 @@ export function AIOpsAssistant() {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
{mockLLMProviders
|
||||
{llmProviders
|
||||
.find((p) => p.id === llmConfig.provider)
|
||||
?.models.map((m) => (
|
||||
<SelectItem key={m} value={m} className="text-slate-100">
|
||||
{m}
|
||||
</SelectItem>
|
||||
))}
|
||||
{!llmProviders.find((p) => p.id === llmConfig.provider)?.models?.length && (
|
||||
<div className="px-3 py-2 text-sm text-slate-500">请先选择提供商</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -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<GeneratedScript | null>(null);
|
||||
const [selectedLanguage, setSelectedLanguage] = useState('bash');
|
||||
const [savedScripts, setSavedScripts] = useState<GeneratedScript[]>(mockGeneratedScripts);
|
||||
const [savedScripts, setSavedScripts] = useState<GeneratedScript[]>([]);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('generate');
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!prompt.trim()) return;
|
||||
|
||||
setIsGenerating(true);
|
||||
|
||||
// 模拟AI生成脚本
|
||||
setTimeout(() => {
|
||||
const mockScripts: Record<string, GeneratedScript> = {
|
||||
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__':
|
||||
<CardTitle className="text-slate-100">历史生成的脚本</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{savedScripts.map((script) => (
|
||||
<div
|
||||
key={script.id}
|
||||
className="p-4 rounded-lg border border-slate-700 bg-slate-800/50 hover:border-slate-600 cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-slate-700 flex items-center justify-center">
|
||||
{getLanguageIcon(script.language)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-100">{script.name}</h4>
|
||||
<p className="text-sm text-slate-400">{script.description}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline" className="text-xs border-slate-600 text-slate-400">
|
||||
{script.language.toUpperCase()}
|
||||
</Badge>
|
||||
<span className="text-xs text-slate-500">{script.createdAt}</span>
|
||||
{savedScripts.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<History className="h-10 w-10 mb-3 opacity-30" />
|
||||
<p>暂无生成的脚本</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{savedScripts.map((script) => (
|
||||
<div
|
||||
key={script.id}
|
||||
className="p-4 rounded-lg border border-slate-700 bg-slate-800/50 hover:border-slate-600 cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-slate-700 flex items-center justify-center">
|
||||
{getLanguageIcon(script.language)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-100">{script.name}</h4>
|
||||
<p className="text-sm text-slate-400">{script.description}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline" className="text-xs border-slate-600 text-slate-400">
|
||||
{script.language.toUpperCase()}
|
||||
</Badge>
|
||||
<span className="text-xs text-slate-500">{script.createdAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{script.tags.map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="text-xs border-slate-600 text-slate-400">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
<div className="flex gap-1">
|
||||
{script.tags.map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="text-xs border-slate-600 text-slate-400">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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('获取告警规则失败');
|
||||
}
|
||||
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
generateMockRecords();
|
||||
toast.info('文件解析功能开发中');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -238,6 +170,15 @@ export function BatchImport() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Empty State */}
|
||||
{records.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-slate-500">
|
||||
<Server className="h-10 w-10 mb-3 opacity-30" />
|
||||
<p>暂无导入记录</p>
|
||||
<p className="text-xs mt-1">上传文件后将在此预览数据</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Import Stats */}
|
||||
{records.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
|
||||
@@ -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<string>('all');
|
||||
const [selectedCabinet, setSelectedCabinet] = useState<Cabinet | null>(null);
|
||||
const [selectedDevice, setSelectedDevice] = useState<Device | null>(null);
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [servers, setServers] = useState<ServerType[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedServer, setSelectedServer] = useState<ServerType | null>(null);
|
||||
|
||||
const rooms = Array.from(new Set(cabinets.map((c) => c.room)));
|
||||
// 拉取服务器列表 // 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);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCabinets = cabinets.filter((cabinet) => {
|
||||
const matchesRoom = selectedRoom === 'all' || cabinet.room === selectedRoom;
|
||||
const matchesSearch = cabinet.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchesRoom && matchesSearch;
|
||||
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: <Server className="h-4 w-4" />,
|
||||
switch: <Network className="h-4 w-4" />,
|
||||
router: <Router className="h-4 w-4" />,
|
||||
firewall: <Shield className="h-4 w-4" />,
|
||||
storage: <Database className="h-4 w-4" />,
|
||||
other: <Monitor className="h-4 w-4" />,
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
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 (
|
||||
<div className="flex gap-4">
|
||||
{/* U位编号 */}
|
||||
<div className="flex flex-col text-xs text-slate-500">
|
||||
{Array.from({ length: cabinet.totalU }, (_, i) => (
|
||||
<div key={i} className="h-6 flex items-center justify-end pr-2">
|
||||
{cabinet.totalU - i}U
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 机柜 */}
|
||||
<div className="flex-1 border-2 border-slate-700 rounded-lg overflow-hidden bg-slate-900">
|
||||
{uSlots.map((device, index) => {
|
||||
const uNumber = cabinet.totalU - index;
|
||||
if (device) {
|
||||
const isFirstU = uNumber === device.startU + device.sizeU - 1;
|
||||
if (!isFirstU) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`device-${device.id}`}
|
||||
className={`border-b border-slate-800 flex items-center px-3 cursor-pointer hover:opacity-80 transition-opacity ${getDeviceColor(device.type)}`}
|
||||
style={{ height: `${device.sizeU * 24}px` }}
|
||||
onClick={() => setSelectedDevice(device)}
|
||||
>
|
||||
{getDeviceIcon(device.type)}
|
||||
<span className="ml-2 text-sm font-medium truncate">{device.name}</span>
|
||||
<span className="ml-auto text-xs opacity-70">{device.ip}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`empty-${index}`}
|
||||
className="h-6 border-b border-slate-800/50 hover:bg-slate-800/30 transition-colors"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const getUsageColor = (usage: number) =>
|
||||
usage > 80 ? 'bg-red-400' : usage > 60 ? 'bg-amber-400' : 'bg-emerald-400';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -159,18 +96,12 @@ export function CabinetView() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">机房机柜视图</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">数据中心机房与机柜设备管理</p>
|
||||
</div>
|
||||
<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">
|
||||
<MapPin className="h-4 w-4 mr-2" />
|
||||
机房管理
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
添加设备
|
||||
</Button>
|
||||
<p className="text-sm text-slate-500 mt-1">数据中心服务器资产概览</p>
|
||||
</div>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
添加设备
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
@@ -179,36 +110,10 @@ export function CabinetView() {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">机房数</p>
|
||||
<p className="text-2xl font-bold text-slate-100">{rooms.length}</p>
|
||||
<p className="text-sm text-slate-500">服务器总数</p>
|
||||
<p className="text-2xl font-bold text-slate-100">{servers.length}</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
|
||||
<MapPin className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">机柜数</p>
|
||||
<p className="text-2xl font-bold text-slate-100">{cabinets.length}</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-purple-500/10 text-purple-400">
|
||||
<Grid3X3 className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">设备数</p>
|
||||
<p className="text-2xl font-bold text-emerald-400">{devices.length}</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
|
||||
<Server className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,13 +123,43 @@ export function CabinetView() {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">总功率</p>
|
||||
<p className="text-sm text-slate-500">运行中</p>
|
||||
<p className="text-2xl font-bold text-emerald-400">
|
||||
{servers.filter((s) => s.status === 'running').length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
|
||||
<Grid3X3 className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">维护中</p>
|
||||
<p className="text-2xl font-bold text-amber-400">
|
||||
{cabinets.reduce((acc, c) => acc + c.powerUsed, 0).toFixed(1)}kW
|
||||
{servers.filter((s) => s.status === 'maintenance').length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-amber-500/10 text-amber-400">
|
||||
<Zap className="h-5 w-5" />
|
||||
<Cpu className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">离线</p>
|
||||
<p className="text-2xl font-bold text-slate-400">
|
||||
{servers.filter((s) => s.status === 'offline' || s.status === 'stopped').length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-slate-500/10 text-slate-400">
|
||||
<HardDrive className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -237,44 +172,34 @@ export function CabinetView() {
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Input
|
||||
placeholder="搜索机柜或设备..."
|
||||
placeholder="搜索主机名或 IP..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
/>
|
||||
</div>
|
||||
<Select value={selectedRoom} onValueChange={setSelectedRoom}>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-40 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectValue placeholder="选择机房" />
|
||||
<SelectValue placeholder="选择状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectItem value="all">全部机房</SelectItem>
|
||||
{rooms.map((room) => (
|
||||
<SelectItem key={room} value={room}>
|
||||
{room}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="running">运行中</SelectItem>
|
||||
<SelectItem value="stopped">已停止</SelectItem>
|
||||
<SelectItem value="maintenance">维护中</SelectItem>
|
||||
<SelectItem value="offline">离线</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-slate-800 rounded-lg p-1">
|
||||
<Button
|
||||
variant={viewMode === 'room' ? 'secondary' : 'ghost'}
|
||||
variant={viewMode === 'grid' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
className="h-7"
|
||||
onClick={() => setViewMode('room')}
|
||||
>
|
||||
<MapPin className="h-4 w-4 mr-1" />
|
||||
机房视图
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'cabinet' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
className="h-7"
|
||||
onClick={() => setViewMode('cabinet')}
|
||||
onClick={() => setViewMode('grid')}
|
||||
>
|
||||
<Grid3X3 className="h-4 w-4 mr-1" />
|
||||
机柜视图
|
||||
网格视图
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
|
||||
@@ -289,176 +214,143 @@ export function CabinetView() {
|
||||
</div>
|
||||
|
||||
{/* View Content */}
|
||||
{viewMode === 'room' && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
{filteredCabinets.map((cabinet) => {
|
||||
const usagePercent = (cabinet.usedU / cabinet.totalU) * 100;
|
||||
const powerPercent = (cabinet.powerUsed / cabinet.powerCapacity) * 100;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={cabinet.id}
|
||||
className={`bg-slate-900 border-2 cursor-pointer hover:shadow-lg transition-all ${getStatusColor(cabinet.status)}`}
|
||||
onClick={() => setSelectedCabinet(cabinet)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-slate-200">{cabinet.name}</span>
|
||||
{cabinet.status === 'active' && (
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-slate-500 mb-1">
|
||||
<span>U位</span>
|
||||
<span>{cabinet.usedU}/{cabinet.totalU}</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${
|
||||
usagePercent > 80 ? 'bg-red-400' : usagePercent > 60 ? 'bg-amber-400' : 'bg-emerald-400'
|
||||
}`}
|
||||
style={{ width: `${usagePercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : filteredServers.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-slate-500">
|
||||
<MapPin className="h-10 w-10 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无服务器数据</p>
|
||||
</div>
|
||||
) : viewMode === 'grid' ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredServers.map((srv) => (
|
||||
<Card
|
||||
key={srv.id}
|
||||
className="bg-slate-900 border-slate-800 cursor-pointer hover:border-slate-700 transition-all"
|
||||
onClick={() => setSelectedServer(srv)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10">
|
||||
<Server className="h-4 w-4 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-slate-500 mb-1">
|
||||
<span>功率</span>
|
||||
<span>{cabinet.powerUsed}/{cabinet.powerCapacity}kW</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${
|
||||
powerPercent > 80 ? 'bg-red-400' : powerPercent > 60 ? 'bg-amber-400' : 'bg-emerald-400'
|
||||
}`}
|
||||
style={{ width: `${powerPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-slate-200">{srv.hostname || '--'}</p>
|
||||
<p className="text-xs text-slate-500">{srv.ip || '--'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-1">
|
||||
<Badge variant="outline" className={getStatusColor(srv.status)}>
|
||||
{getStatusText(srv.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-slate-500 mb-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<Cpu className="h-3 w-3" /> CPU
|
||||
</span>
|
||||
<span>{Math.round(srv.cpuUsage ?? 0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${getUsageColor(srv.cpuUsage ?? 0)}`}
|
||||
style={{ width: `${srv.cpuUsage ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-slate-500 mb-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<MemoryStick className="h-3 w-3" /> 内存
|
||||
</span>
|
||||
<span>{Math.round(srv.memoryUsage ?? 0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${getUsageColor(srv.memoryUsage ?? 0)}`}
|
||||
style={{ width: `${srv.memoryUsage ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-slate-500 mb-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<HardDrive className="h-3 w-3" /> 磁盘
|
||||
</span>
|
||||
<span>{Math.round(srv.diskUsage ?? 0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${getUsageColor(srv.diskUsage ?? 0)}`}
|
||||
style={{ width: `${srv.diskUsage ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-1">
|
||||
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
|
||||
{srv.cpu ?? 0} 核
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
|
||||
{srv.memory ?? 0} GB
|
||||
</Badge>
|
||||
{srv.region && (
|
||||
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
|
||||
{getCabinetDevices(cabinet.id).length} 设备
|
||||
{srv.region}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === 'cabinet' && selectedCabinet && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-slate-100">{selectedCabinet.name}</CardTitle>
|
||||
<CardDescription className="text-slate-500">
|
||||
{selectedCabinet.room} · {selectedCabinet.usedU}/{selectedCabinet.totalU}U · {selectedCabinet.powerUsed}/{selectedCabinet.powerCapacity}kW
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedCabinet(null)}>
|
||||
返回
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{renderCabinetFront(selectedCabinet)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-slate-100 text-base">机柜信息</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">机柜编号</p>
|
||||
<p className="text-sm text-slate-200">{selectedCabinet.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">所属机房</p>
|
||||
<p className="text-sm text-slate-200">{selectedCabinet.room}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">位置</p>
|
||||
<p className="text-sm text-slate-200">第{selectedCabinet.row}排 第{selectedCabinet.column}列</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">状态</p>
|
||||
<Badge variant="outline" className={
|
||||
selectedCabinet.status === 'active'
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
: 'border-slate-500/30 text-slate-400 bg-slate-500/10'
|
||||
}>
|
||||
{selectedCabinet.status === 'active' ? '使用中' : selectedCabinet.status === 'empty' ? '空闲' : '停用'}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === 'list' && (
|
||||
) : (
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-0">
|
||||
<div className="border-t border-slate-800">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-800">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">设备名称</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">类型</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">机柜</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">U位</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">主机名</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">IP地址</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">系统</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">区域</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">配置</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-slate-400">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
{devices.map((device) => (
|
||||
<tr key={device.id} className="hover:bg-slate-800/50">
|
||||
{filteredServers.map((srv) => (
|
||||
<tr key={srv.id} className="hover:bg-slate-800/50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{getDeviceIcon(device.type)}
|
||||
<span className="text-sm font-medium text-slate-200">{device.name}</span>
|
||||
<Server className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-sm font-medium text-slate-200">{srv.hostname || '--'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{srv.ip || '--'}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{srv.os || '--'}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{srv.region || '--'}</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-400">
|
||||
{srv.cpu ?? 0}核 / {srv.memory ?? 0}GB
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant="outline" className="border-slate-700 text-slate-400 text-xs">
|
||||
{device.type}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">
|
||||
{cabinets.find((c) => c.id === device.cabinetId)?.name}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">
|
||||
{device.startU}U - {device.startU + device.sizeU - 1}U ({device.sizeU}U)
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">{device.ip || '--'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant="outline" className={
|
||||
device.status === 'online'
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
: device.status === 'offline'
|
||||
? 'border-red-500/30 text-red-400 bg-red-500/10'
|
||||
: 'border-amber-500/30 text-amber-400 bg-amber-500/10'
|
||||
}>
|
||||
{device.status === 'online' ? '在线' : device.status === 'offline' ? '离线' : '维护中'}
|
||||
<Badge variant="outline" className={getStatusColor(srv.status)}>
|
||||
{getStatusText(srv.status)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||||
onClick={() => setSelectedServer(srv)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100">
|
||||
@@ -475,43 +367,39 @@ export function CabinetView() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Device Detail Dialog */}
|
||||
<Dialog open={!!selectedDevice} onOpenChange={() => setSelectedDevice(null)}>
|
||||
{/* Server Detail Dialog */}
|
||||
<Dialog open={!!selectedServer} onOpenChange={() => setSelectedServer(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
{selectedDevice && getDeviceIcon(selectedDevice.type)}
|
||||
{selectedDevice?.name}
|
||||
<Server className="h-5 w-5 text-blue-400" />
|
||||
{selectedServer?.hostname}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
{selectedDevice?.type} · {selectedDevice?.assetNo}
|
||||
{selectedServer?.ip} · {selectedServer?.os}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-slate-800/50">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">序列号</p>
|
||||
<p className="text-sm text-slate-200">{selectedDevice?.sn || '--'}</p>
|
||||
<p className="text-xs text-slate-500 mb-1">IP 地址</p>
|
||||
<p className="text-sm text-slate-200">{selectedServer?.ip || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">资产编号</p>
|
||||
<p className="text-sm text-slate-200">{selectedDevice?.assetNo || '--'}</p>
|
||||
<p className="text-xs text-slate-500 mb-1">操作系统</p>
|
||||
<p className="text-sm text-slate-200">{selectedServer?.os || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">IP地址</p>
|
||||
<p className="text-sm text-slate-200">{selectedDevice?.ip || '--'}</p>
|
||||
<p className="text-xs text-slate-500 mb-1">配置</p>
|
||||
<p className="text-sm text-slate-200">
|
||||
{selectedServer?.cpu ?? 0} 核 / {selectedServer?.memory ?? 0} GB
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">状态</p>
|
||||
<Badge variant="outline" className={
|
||||
selectedDevice?.status === 'online'
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
: selectedDevice?.status === 'offline'
|
||||
? 'border-red-500/30 text-red-400 bg-red-500/10'
|
||||
: 'border-amber-500/30 text-amber-400 bg-amber-500/10'
|
||||
}>
|
||||
{selectedDevice?.status === 'online' ? '在线' : selectedDevice?.status === 'offline' ? '离线' : '维护中'}
|
||||
<Badge variant="outline" className={selectedServer ? getStatusColor(selectedServer.status) : ''}>
|
||||
{selectedServer ? getStatusText(selectedServer.status) : ''}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,21 +35,13 @@ const cloudProviders = [
|
||||
{ id: 'azure', name: 'Azure', color: 'bg-blue-600', icon: 'AZ' },
|
||||
];
|
||||
|
||||
// Mock cloud servers data
|
||||
const mockCloudServers = [
|
||||
{ id: 'ecs-001', name: 'i-bp1a2b3c4d5e6f7g8', hostname: 'web-server-01', ip: '47.98.123.45', region: 'cn-hangzhou', status: 'running', os: 'CentOS 7.9', spec: '2核4G' },
|
||||
{ id: 'ecs-002', name: 'i-bp2b3c4d5e6f7g8h9', hostname: 'web-server-02', ip: '47.98.123.46', region: 'cn-hangzhou', status: 'running', os: 'CentOS 7.9', spec: '2核4G' },
|
||||
{ id: 'ecs-003', name: 'i-bp3c4d5e6f7g8h9i0', hostname: 'app-server-01', ip: '47.98.123.47', region: 'cn-shanghai', status: 'stopped', os: 'Ubuntu 20.04', spec: '4核8G' },
|
||||
{ id: 'ecs-004', name: 'i-bp4d5e6f7g8h9i0j1', hostname: 'db-server-01', ip: '47.98.123.48', region: 'cn-beijing', status: 'running', os: 'CentOS 8.2', spec: '8核16G' },
|
||||
{ id: 'ecs-005', name: 'i-bp5e6f7g8h9i0j1k2', hostname: 'cache-server-01', ip: '47.98.123.49', region: 'cn-hangzhou', status: 'running', os: 'CentOS 7.9', spec: '2核8G' },
|
||||
];
|
||||
|
||||
export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
const [cloudAccounts, setCloudAccounts] = useState<any[]>([]);
|
||||
const [step, setStep] = useState(1);
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>('');
|
||||
const [selectedAccount, setSelectedAccount] = useState<string>('');
|
||||
const [selectedServers, setSelectedServers] = useState<string[]>([]);
|
||||
const [cloudServers, setCloudServers] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const loadAccounts = async () => {
|
||||
@@ -73,7 +65,11 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
if (step < 3) {
|
||||
if (step === 2) {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => setIsLoading(false), 1000);
|
||||
// 云主机发现功能尚未实现,暂以空列表展示
|
||||
setTimeout(() => {
|
||||
setCloudServers([]);
|
||||
setIsLoading(false);
|
||||
}, 600);
|
||||
}
|
||||
setStep(step + 1);
|
||||
}
|
||||
@@ -94,10 +90,10 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedServers.length === mockCloudServers.length) {
|
||||
if (selectedServers.length === cloudServers.length && cloudServers.length > 0) {
|
||||
setSelectedServers([]);
|
||||
} else {
|
||||
setSelectedServers(mockCloudServers.map(s => s.id));
|
||||
setSelectedServers(cloudServers.map(s => s.id));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,8 +237,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-slate-400 text-sm">请选择要导入的云主机</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={handleSelectAll}>
|
||||
{selectedServers.length === mockCloudServers.length ? '取消全选' : '全选'}
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={handleSelectAll} disabled={cloudServers.length === 0}>
|
||||
{selectedServers.length === cloudServers.length && cloudServers.length > 0 ? '取消全选' : '全选'}
|
||||
</Button>
|
||||
<Badge className="bg-blue-500/20 text-blue-400 border-0">
|
||||
已选 {selectedServers.length} 台
|
||||
@@ -262,7 +258,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400 w-10">
|
||||
<Checkbox
|
||||
checked={selectedServers.length === mockCloudServers.length && mockCloudServers.length > 0}
|
||||
checked={selectedServers.length === cloudServers.length && cloudServers.length > 0}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
@@ -275,35 +271,45 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
{mockCloudServers.map((server) => (
|
||||
<tr
|
||||
key={server.id}
|
||||
className={`hover:bg-slate-800/50 ${selectedServers.includes(server.id) ? 'bg-blue-500/10' : ''}`}
|
||||
>
|
||||
<td className="px-3 py-2">
|
||||
<Checkbox
|
||||
checked={selectedServers.includes(server.id)}
|
||||
onCheckedChange={() => handleServerToggle(server.id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm text-slate-300">{server.name}</td>
|
||||
<td className="px-3 py-2 text-sm text-slate-200">{server.hostname}</td>
|
||||
<td className="px-3 py-2 text-sm text-slate-400">{server.ip}</td>
|
||||
<td className="px-3 py-2 text-sm text-slate-400">{server.region}</td>
|
||||
<td className="px-3 py-2 text-sm text-slate-400">{server.spec}</td>
|
||||
<td className="px-3 py-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={server.status === 'running'
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
: 'border-slate-500/30 text-slate-400 bg-slate-500/10'
|
||||
}
|
||||
>
|
||||
{server.status === 'running' ? '运行中' : '已停止'}
|
||||
</Badge>
|
||||
{cloudServers.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-3 py-12 text-center">
|
||||
<Server className="h-10 w-10 mx-auto mb-3 text-slate-600" />
|
||||
<p className="text-slate-500">暂无云主机</p>
|
||||
<p className="text-xs text-slate-600 mt-1">云主机发现功能尚未实现,请手动添加服务器</p>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
) : (
|
||||
cloudServers.map((server) => (
|
||||
<tr
|
||||
key={server.id}
|
||||
className={`hover:bg-slate-800/50 ${selectedServers.includes(server.id) ? 'bg-blue-500/10' : ''}`}
|
||||
>
|
||||
<td className="px-3 py-2">
|
||||
<Checkbox
|
||||
checked={selectedServers.includes(server.id)}
|
||||
onCheckedChange={() => handleServerToggle(server.id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm text-slate-300">{server.name}</td>
|
||||
<td className="px-3 py-2 text-sm text-slate-200">{server.hostname}</td>
|
||||
<td className="px-3 py-2 text-sm text-slate-400">{server.ip}</td>
|
||||
<td className="px-3 py-2 text-sm text-slate-400">{server.region}</td>
|
||||
<td className="px-3 py-2 text-sm text-slate-400">{server.spec}</td>
|
||||
<td className="px-3 py-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={server.status === 'running'
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
: 'border-slate-500/30 text-slate-400 bg-slate-500/10'
|
||||
}
|
||||
>
|
||||
{server.status === 'running' ? '运行中' : '已停止'}
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -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<string>('password');
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [previewData, setPreviewData] = useState<typeof mockPreviewData>([]);
|
||||
const [previewData, setPreviewData] = useState<PreviewRow[]>([]);
|
||||
const [regions, setRegions] = useState<RegionOption[]>([]);
|
||||
|
||||
// 对话框打开时拉取区域列表 // 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 {
|
||||
|
||||
@@ -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<OperationLog[]>([]);
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
|
||||
onClick={fetchLogs}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
@@ -62,48 +104,59 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-0">
|
||||
<div className="border-t border-slate-800">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<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">IP地址</TableHead>
|
||||
<TableHead className="text-slate-400">结果</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredLogs.map((log) => (
|
||||
<TableRow key={log.id} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">
|
||||
{new Date(log.createdAt).toLocaleString()}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-200">{log.username}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{log.module}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{log.action}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs text-slate-400 bg-slate-800 px-2 py-0.5 rounded">
|
||||
{log.resource}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">{log.ip}</span>
|
||||
</TableCell>
|
||||
<TableCell>{getLogResultBadge(log.result)}</TableCell>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : filteredLogs.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<Search className="h-8 w-8 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无操作日志</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<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">IP地址</TableHead>
|
||||
<TableHead className="text-slate-400">结果</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredLogs.map((log) => (
|
||||
<TableRow key={log.id} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">
|
||||
{log.createdAt ? new Date(log.createdAt).toLocaleString() : '--'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-200">{log.username || '--'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{log.module || '--'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{log.action || '--'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs text-slate-400 bg-slate-800 px-2 py-0.5 rounded">
|
||||
{log.resource || '--'}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">{log.ip || '--'}</span>
|
||||
</TableCell>
|
||||
<TableCell>{getLogResultBadge(log.result)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in new issue
Block a user