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 { AIScriptGenerator } from '@/components/aiops/AIScriptGenerator';
|
||||||
import { AIFaultDiagnosis } from '@/components/aiops/AIFaultDiagnosis';
|
import { AIFaultDiagnosis } from '@/components/aiops/AIFaultDiagnosis';
|
||||||
import { AIPredictiveAnalytics } from '@/components/aiops/AIPredictiveAnalytics';
|
import { AIPredictiveAnalytics } from '@/components/aiops/AIPredictiveAnalytics';
|
||||||
import SQLAuditManager from '@/components/audit/SQLAuditManager';
|
|
||||||
import LogManager from '@/components/monitor/logs/LogManager';
|
import LogManager from '@/components/monitor/logs/LogManager';
|
||||||
import AlertCenter from '@/components/monitor/alert-center/AlertCenter';
|
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 { AgentMonitor } from '@/components/agent/AgentMonitor';
|
||||||
import { AgentAlerts } from '@/components/agent/AgentAlerts';
|
import { AgentAlerts } from '@/components/agent/AgentAlerts';
|
||||||
|
|
||||||
@@ -76,8 +73,6 @@ function App() {
|
|||||||
<Route path="metrics" element={<AlertManager />} />
|
<Route path="metrics" element={<AlertManager />} />
|
||||||
<Route path="alert-center" element={<AlertCenter />} />
|
<Route path="alert-center" element={<AlertCenter />} />
|
||||||
<Route path="logs-monitor" element={<LogManager />} />
|
<Route path="logs-monitor" element={<LogManager />} />
|
||||||
<Route path="trace" element={<TraceManager />} />
|
|
||||||
<Route path="grafana" element={<GrafanaDashboard />} />
|
|
||||||
<Route path="agent-alerts" element={<AgentAlerts />} />
|
<Route path="agent-alerts" element={<AgentAlerts />} />
|
||||||
<Route path="alerts" element={<AlertManager />} />
|
<Route path="alerts" element={<AlertManager />} />
|
||||||
<Route path="logs" element={<AlertManager />} />
|
<Route path="logs" element={<AlertManager />} />
|
||||||
@@ -94,7 +89,6 @@ function App() {
|
|||||||
<Route path="ai-script-generator" element={<AIScriptGenerator />} />
|
<Route path="ai-script-generator" element={<AIScriptGenerator />} />
|
||||||
<Route path="ai-fault-diagnosis" element={<AIFaultDiagnosis />} />
|
<Route path="ai-fault-diagnosis" element={<AIFaultDiagnosis />} />
|
||||||
<Route path="ai-predictive" element={<AIPredictiveAnalytics />} />
|
<Route path="ai-predictive" element={<AIPredictiveAnalytics />} />
|
||||||
<Route path="sql-audit" element={<SQLAuditManager />} />
|
|
||||||
<Route path="users" element={<SystemManager />} />
|
<Route path="users" element={<SystemManager />} />
|
||||||
<Route path="roles" element={<SystemManager />} />
|
<Route path="roles" element={<SystemManager />} />
|
||||||
<Route path="logs-audit" element={<SystemManager />} />
|
<Route path="logs-audit" element={<SystemManager />} />
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from '@/components/ui/accordion';
|
} from '@/components/ui/accordion';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
Brain,
|
Brain,
|
||||||
Search,
|
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 = [
|
const commonProblems = [
|
||||||
{ id: 'slow', name: '服务响应缓慢', icon: Clock },
|
{ id: 'slow', name: '服务响应缓慢', icon: Clock },
|
||||||
{ id: 'crash', name: '应用崩溃/重启', icon: XCircle },
|
{ id: 'crash', name: '应用崩溃/重启', icon: XCircle },
|
||||||
@@ -150,19 +95,11 @@ export function AIFaultDiagnosis() {
|
|||||||
const [isDiagnosing, setIsDiagnosing] = useState(false);
|
const [isDiagnosing, setIsDiagnosing] = useState(false);
|
||||||
const [diagnosisResult, setDiagnosisResult] = useState<DiagnosisResult | null>(null);
|
const [diagnosisResult, setDiagnosisResult] = useState<DiagnosisResult | null>(null);
|
||||||
const [selectedProblem, setSelectedProblem] = useState<string | null>(null);
|
const [selectedProblem, setSelectedProblem] = useState<string | null>(null);
|
||||||
const [diagnosisHistory, setDiagnosisHistory] = useState<DiagnosisResult[]>(mockDiagnosisHistory);
|
const [diagnosisHistory, setDiagnosisHistory] = useState<DiagnosisResult[]>([]);
|
||||||
|
|
||||||
const handleDiagnose = () => {
|
const handleDiagnose = () => {
|
||||||
if (!symptomInput.trim()) return;
|
if (!symptomInput.trim()) return;
|
||||||
|
toast.info('AI 诊断功能需配置 LLM 服务');
|
||||||
setIsDiagnosing(true);
|
|
||||||
setDiagnosisResult(null);
|
|
||||||
|
|
||||||
// 模拟AI诊断
|
|
||||||
setTimeout(() => {
|
|
||||||
setDiagnosisResult(mockDiagnosisHistory[0]);
|
|
||||||
setIsDiagnosing(false);
|
|
||||||
}, 2500);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProblemSelect = (problemId: string) => {
|
const handleProblemSelect = (problemId: string) => {
|
||||||
@@ -458,32 +395,39 @@ export function AIFaultDiagnosis() {
|
|||||||
<CardTitle className="text-slate-100">诊断历史</CardTitle>
|
<CardTitle className="text-slate-100">诊断历史</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
{diagnosisHistory.length === 0 ? (
|
||||||
{diagnosisHistory.map((item) => (
|
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||||
<div
|
<History className="h-10 w-10 mb-3 opacity-30" />
|
||||||
key={item.id}
|
<p>暂无诊断记录</p>
|
||||||
className="p-4 rounded-lg border border-slate-700 bg-slate-800/50 hover:border-slate-600 cursor-pointer"
|
</div>
|
||||||
onClick={() => setDiagnosisResult(item)}
|
) : (
|
||||||
>
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
{diagnosisHistory.map((item) => (
|
||||||
<div className="flex items-center gap-3">
|
<div
|
||||||
<div className="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center">
|
key={item.id}
|
||||||
<Brain className="h-5 w-5 text-purple-400" />
|
className="p-4 rounded-lg border border-slate-700 bg-slate-800/50 hover:border-slate-600 cursor-pointer"
|
||||||
</div>
|
onClick={() => setDiagnosisResult(item)}
|
||||||
<div>
|
>
|
||||||
<h4 className="font-medium text-slate-100">{item.problem}</h4>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-slate-400">
|
<div className="flex items-center gap-3">
|
||||||
{item.symptoms.length} 个症状 · {item.rootCauses.length} 个根因 · {item.solutions.length} 个方案
|
<div className="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center">
|
||||||
</p>
|
<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>
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" className="text-slate-400">
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" className="text-slate-400">
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
Send,
|
Send,
|
||||||
@@ -65,23 +66,8 @@ interface LLMConfig {
|
|||||||
maxTokens: number;
|
maxTokens: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockLLMProviders = [
|
// LLM 提供商列表(需后端配置后填充)
|
||||||
{ id: 'openai', name: 'OpenAI', models: ['gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo'] },
|
const llmProviders: { id: string; name: string; models: string[] }[] = [];
|
||||||
{ 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: ['查看今日告警', '分析服务器性能', '生成巡检报告'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const quickActions = [
|
const quickActions = [
|
||||||
{ id: 'alerts', name: '分析今日告警', icon: AlertTriangle, description: '分析今日所有告警事件' },
|
{ id: 'alerts', name: '分析今日告警', icon: AlertTriangle, description: '分析今日所有告警事件' },
|
||||||
@@ -91,13 +77,13 @@ const quickActions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function AIOpsAssistant() {
|
export function AIOpsAssistant() {
|
||||||
const [messages, setMessages] = useState<Message[]>(mockConversations);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
||||||
const [llmConfig, setLlmConfig] = useState<LLMConfig>({
|
const [llmConfig, setLlmConfig] = useState<LLMConfig>({
|
||||||
provider: 'openai',
|
provider: '',
|
||||||
model: 'gpt-4',
|
model: '',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
endpoint: '',
|
endpoint: '',
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
@@ -125,39 +111,16 @@ export function AIOpsAssistant() {
|
|||||||
|
|
||||||
setMessages((prev) => [...prev, userMessage]);
|
setMessages((prev) => [...prev, userMessage]);
|
||||||
setInput('');
|
setInput('');
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
// Mock AI response
|
// AI 功能需配置 LLM 接口,暂不返回模拟回复
|
||||||
setTimeout(() => {
|
toast.info('AI 功能需配置 LLM 接口,请在「模型配置」中完成设置');
|
||||||
const responses: Record<string, string> = {
|
const assistantMessage: Message = {
|
||||||
'告警': '根据今日告警数据分析:\n\n**严重告警 (2)**\n- 服务器 web-server-03 CPU 使用率超过 95%\n- 数据库主库连接数接近上限\n\n**建议处理方案:**\n1. 立即扩容 web-server-03 或迁移部分流量\n2. 检查数据库慢查询,优化连接池配置\n3. 考虑启用读写分离',
|
id: (Date.now() + 1).toString(),
|
||||||
'性能': '系统性能分析结果:\n\n**CPU 使用率**\n- 平均: 45%\n- 峰值: 92% (14:30)\n- 建议: 高峰期需要扩容\n\n**内存使用**\n- 平均: 68%\n- 建议: 正常范围内\n\n**磁盘 I/O**\n- 数据库服务器 I/O 等待较高\n- 建议: 考虑使用 SSD 或优化查询',
|
role: 'assistant',
|
||||||
'报告': '## 每日运维报告 (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',
|
content: '当前未配置 LLM 服务,无法提供智能回复。请在右上角「模型配置」中配置 LLM 接口后重试。',
|
||||||
'优化': '资源优化建议:\n\n**成本优化**\n1. 关闭 5 台闲置测试服务器,预计月节省 ¥2,400\n2. 调整 3 台服务器规格,预计月节省 ¥1,800\n\n**性能优化**\n1. 数据库添加索引,预计查询性能提升 40%\n2. Redis 缓存命中率仅 65%,建议优化缓存策略\n\n**自动化建议**\n1. 部署自动扩缩容策略\n2. 配置告警自动处理规则',
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
setMessages((prev) => [...prev, assistantMessage]);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQuickAction = (actionId: string) => {
|
const handleQuickAction = (actionId: string) => {
|
||||||
@@ -260,6 +223,13 @@ export function AIOpsAssistant() {
|
|||||||
|
|
||||||
<ScrollArea className="flex-1 p-4">
|
<ScrollArea className="flex-1 p-4">
|
||||||
<div className="space-y-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) => (
|
{messages.map((message) => (
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
@@ -354,11 +324,14 @@ export function AIOpsAssistant() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700">
|
<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">
|
<SelectItem key={p.id} value={p.id} className="text-slate-100">
|
||||||
{p.name}
|
{p.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
{llmProviders.length === 0 && (
|
||||||
|
<div className="px-3 py-2 text-sm text-slate-500">暂无可用的 LLM 提供商</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -372,13 +345,16 @@ export function AIOpsAssistant() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700">
|
<SelectContent className="bg-slate-800 border-slate-700">
|
||||||
{mockLLMProviders
|
{llmProviders
|
||||||
.find((p) => p.id === llmConfig.provider)
|
.find((p) => p.id === llmConfig.provider)
|
||||||
?.models.map((m) => (
|
?.models.map((m) => (
|
||||||
<SelectItem key={m} value={m} className="text-slate-100">
|
<SelectItem key={m} value={m} className="text-slate-100">
|
||||||
{m}
|
{m}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
{!llmProviders.find((p) => p.id === llmConfig.provider)?.models?.length && (
|
||||||
|
<div className="px-3 py-2 text-sm text-slate-500">请先选择提供商</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
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() {
|
export function AIScriptGenerator() {
|
||||||
const [prompt, setPrompt] = useState('');
|
const [prompt, setPrompt] = useState('');
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
const [generatedScript, setGeneratedScript] = useState<GeneratedScript | null>(null);
|
const [generatedScript, setGeneratedScript] = useState<GeneratedScript | null>(null);
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState('bash');
|
const [selectedLanguage, setSelectedLanguage] = useState('bash');
|
||||||
const [savedScripts, setSavedScripts] = useState<GeneratedScript[]>(mockGeneratedScripts);
|
const [savedScripts, setSavedScripts] = useState<GeneratedScript[]>([]);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState('generate');
|
const [activeTab, setActiveTab] = useState('generate');
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!prompt.trim()) return;
|
if (!prompt.trim()) return;
|
||||||
|
toast.info('AI 脚本生成功能需配置 LLM 服务');
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTemplateClick = (template: ScriptTemplate) => {
|
const handleTemplateClick = (template: ScriptTemplate) => {
|
||||||
@@ -533,39 +326,46 @@ if __name__ == '__main__':
|
|||||||
<CardTitle className="text-slate-100">历史生成的脚本</CardTitle>
|
<CardTitle className="text-slate-100">历史生成的脚本</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
{savedScripts.length === 0 ? (
|
||||||
{savedScripts.map((script) => (
|
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||||
<div
|
<History className="h-10 w-10 mb-3 opacity-30" />
|
||||||
key={script.id}
|
<p>暂无生成的脚本</p>
|
||||||
className="p-4 rounded-lg border border-slate-700 bg-slate-800/50 hover:border-slate-600 cursor-pointer"
|
</div>
|
||||||
>
|
) : (
|
||||||
<div className="flex items-start justify-between">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-3">
|
{savedScripts.map((script) => (
|
||||||
<div className="w-10 h-10 rounded-lg bg-slate-700 flex items-center justify-center">
|
<div
|
||||||
{getLanguageIcon(script.language)}
|
key={script.id}
|
||||||
</div>
|
className="p-4 rounded-lg border border-slate-700 bg-slate-800/50 hover:border-slate-600 cursor-pointer"
|
||||||
<div>
|
>
|
||||||
<h4 className="font-medium text-slate-100">{script.name}</h4>
|
<div className="flex items-start justify-between">
|
||||||
<p className="text-sm text-slate-400">{script.description}</p>
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="w-10 h-10 rounded-lg bg-slate-700 flex items-center justify-center">
|
||||||
<Badge variant="outline" className="text-xs border-slate-600 text-slate-400">
|
{getLanguageIcon(script.language)}
|
||||||
{script.language.toUpperCase()}
|
</div>
|
||||||
</Badge>
|
<div>
|
||||||
<span className="text-xs text-slate-500">{script.createdAt}</span>
|
<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>
|
||||||
</div>
|
<div className="flex gap-1">
|
||||||
<div className="flex gap-1">
|
{script.tags.map((tag) => (
|
||||||
{script.tags.map((tag) => (
|
<Badge key={tag} variant="outline" className="text-xs border-slate-600 text-slate-400">
|
||||||
<Badge key={tag} variant="outline" className="text-xs border-slate-600 text-slate-400">
|
{tag}
|
||||||
{tag}
|
</Badge>
|
||||||
</Badge>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -28,12 +28,10 @@ import {
|
|||||||
Key,
|
Key,
|
||||||
Compass,
|
Compass,
|
||||||
Brain,
|
Brain,
|
||||||
ScrollText,
|
|
||||||
FileTerminal,
|
FileTerminal,
|
||||||
Activity,
|
Activity,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Monitor,
|
|
||||||
Bot,
|
Bot,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -92,8 +90,6 @@ const menuItems: MenuItem[] = [
|
|||||||
{ id: 'metrics', name: '指标监控', icon: Cpu, path: '/metrics' },
|
{ id: 'metrics', name: '指标监控', icon: Cpu, path: '/metrics' },
|
||||||
{ id: 'alert-center', name: '告警中心', icon: Bell, path: '/alert-center' },
|
{ id: 'alert-center', name: '告警中心', icon: Bell, path: '/alert-center' },
|
||||||
{ id: 'logs-monitor', name: '日志能力', icon: FileCode, path: '/logs-monitor' },
|
{ 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' },
|
{ 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-script-generator', name: 'AI脚本生成', icon: FileCode, path: '/ai-script-generator' },
|
||||||
{ id: 'ai-fault-diagnosis', name: '故障智能诊断', icon: Activity, path: '/ai-fault-diagnosis' },
|
{ id: 'ai-fault-diagnosis', name: '故障智能诊断', icon: Activity, path: '/ai-fault-diagnosis' },
|
||||||
{ id: 'ai-predictive', name: 'AI预测分析', icon: BarChart3, path: '/ai-predictive' },
|
{ 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 () => {
|
const fetchRules = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await alertCenterApi.getRules();
|
const res = await alertCenterApi.getRules();
|
||||||
setRules(res.data || []);
|
setRules(res.list || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('获取告警规则失败');
|
toast.error('获取告警规则失败');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -62,82 +63,13 @@ export function BatchImport() {
|
|||||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
// 模拟文件解析
|
toast.info('文件解析功能开发中');
|
||||||
generateMockRecords();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
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 handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
generateMockRecords();
|
toast.info('文件解析功能开发中');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -238,6 +170,15 @@ export function BatchImport() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 */}
|
{/* Import Stats */}
|
||||||
{records.length > 0 && (
|
{records.length > 0 && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<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 { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -23,135 +22,73 @@ import {
|
|||||||
Server,
|
Server,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
MapPin,
|
|
||||||
Zap,
|
|
||||||
Grid3X3,
|
Grid3X3,
|
||||||
List,
|
List,
|
||||||
|
MapPin,
|
||||||
|
Cpu,
|
||||||
|
MemoryStick,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Network,
|
|
||||||
Router,
|
|
||||||
Shield,
|
|
||||||
Database,
|
|
||||||
Monitor,
|
|
||||||
Edit,
|
|
||||||
Trash2,
|
|
||||||
Eye,
|
Eye,
|
||||||
ArrowUp,
|
Edit,
|
||||||
ArrowDown,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cabinets, devices } from '@/mocks/data';
|
import { serverApi } from '@/services/api';
|
||||||
import type { Cabinet, Device } from '@/types';
|
import type { Server as ServerType } from '@/types';
|
||||||
|
|
||||||
export function CabinetView() {
|
export function CabinetView() {
|
||||||
const [viewMode, setViewMode] = useState<'room' | 'cabinet' | 'list'>('room');
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
const [selectedRoom, setSelectedRoom] = useState<string>('all');
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
const [selectedCabinet, setSelectedCabinet] = useState<Cabinet | null>(null);
|
|
||||||
const [selectedDevice, setSelectedDevice] = useState<Device | null>(null);
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
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) => {
|
useEffect(() => {
|
||||||
const matchesRoom = selectedRoom === 'all' || cabinet.room === selectedRoom;
|
fetchServers();
|
||||||
const matchesSearch = cabinet.name.toLowerCase().includes(searchQuery.toLowerCase());
|
}, []);
|
||||||
return matchesRoom && matchesSearch;
|
|
||||||
|
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) => {
|
const getStatusColor = (status: string) => {
|
||||||
return devices.filter((d) => d.cabinetId === cabinetId).sort((a, b) => b.startU - a.startU);
|
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',
|
||||||
const getDeviceIcon = (type: Device['type']) => {
|
maintenance: 'border-amber-500/30 text-amber-400 bg-amber-500/10',
|
||||||
const icons = {
|
offline: 'border-slate-500/30 text-slate-400 bg-slate-500/10',
|
||||||
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" />,
|
|
||||||
};
|
};
|
||||||
return icons[type];
|
return colors[status] ?? colors.offline;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDeviceColor = (type: Device['type']) => {
|
const getStatusText = (status: string) => {
|
||||||
const colors = {
|
const map: Record<string, string> = {
|
||||||
server: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
running: '运行中',
|
||||||
switch: 'bg-purple-500/20 text-purple-400 border-purple-500/30',
|
stopped: '已停止',
|
||||||
router: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30',
|
maintenance: '维护中',
|
||||||
firewall: 'bg-red-500/20 text-red-400 border-red-500/30',
|
offline: '离线',
|
||||||
storage: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
|
|
||||||
other: 'bg-slate-500/20 text-slate-400 border-slate-500/30',
|
|
||||||
};
|
};
|
||||||
return colors[type];
|
return map[status] ?? status;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusColor = (status: Cabinet['status']) => {
|
const getUsageColor = (usage: number) =>
|
||||||
const colors = {
|
usage > 80 ? 'bg-red-400' : usage > 60 ? 'bg-amber-400' : 'bg-emerald-400';
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -159,18 +96,12 @@ export function CabinetView() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-slate-100">机房机柜视图</h1>
|
<h1 className="text-2xl font-bold text-slate-100">机房机柜视图</h1>
|
||||||
<p className="text-sm text-slate-500 mt-1">数据中心机房与机柜设备管理</p>
|
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
添加设备
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
@@ -179,36 +110,10 @@ export function CabinetView() {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-slate-500">机房数</p>
|
<p className="text-sm text-slate-500">服务器总数</p>
|
||||||
<p className="text-2xl font-bold text-slate-100">{rooms.length}</p>
|
<p className="text-2xl font-bold text-slate-100">{servers.length}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
|
<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" />
|
<Server className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,13 +123,43 @@ export function CabinetView() {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-lg bg-amber-500/10 text-amber-400">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -237,44 +172,34 @@ export function CabinetView() {
|
|||||||
<div className="relative w-64">
|
<div className="relative w-64">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索机柜或设备..."
|
placeholder="搜索主机名或 IP..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
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"
|
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<SelectTrigger className="w-40 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
|
||||||
<SelectValue placeholder="选择机房" />
|
<SelectValue placeholder="选择状态" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700">
|
<SelectContent className="bg-slate-800 border-slate-700">
|
||||||
<SelectItem value="all">全部机房</SelectItem>
|
<SelectItem value="all">全部状态</SelectItem>
|
||||||
{rooms.map((room) => (
|
<SelectItem value="running">运行中</SelectItem>
|
||||||
<SelectItem key={room} value={room}>
|
<SelectItem value="stopped">已停止</SelectItem>
|
||||||
{room}
|
<SelectItem value="maintenance">维护中</SelectItem>
|
||||||
</SelectItem>
|
<SelectItem value="offline">离线</SelectItem>
|
||||||
))}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 bg-slate-800 rounded-lg p-1">
|
<div className="flex items-center gap-1 bg-slate-800 rounded-lg p-1">
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === 'room' ? 'secondary' : 'ghost'}
|
variant={viewMode === 'grid' ? 'secondary' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7"
|
className="h-7"
|
||||||
onClick={() => setViewMode('room')}
|
onClick={() => setViewMode('grid')}
|
||||||
>
|
|
||||||
<MapPin className="h-4 w-4 mr-1" />
|
|
||||||
机房视图
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={viewMode === 'cabinet' ? 'secondary' : 'ghost'}
|
|
||||||
size="sm"
|
|
||||||
className="h-7"
|
|
||||||
onClick={() => setViewMode('cabinet')}
|
|
||||||
>
|
>
|
||||||
<Grid3X3 className="h-4 w-4 mr-1" />
|
<Grid3X3 className="h-4 w-4 mr-1" />
|
||||||
机柜视图
|
网格视图
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
|
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
|
||||||
@@ -289,176 +214,143 @@ export function CabinetView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* View Content */}
|
{/* View Content */}
|
||||||
{viewMode === 'room' && (
|
{loading ? (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
<div className="flex items-center justify-center py-16">
|
||||||
{filteredCabinets.map((cabinet) => {
|
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||||
const usagePercent = (cabinet.usedU / cabinet.totalU) * 100;
|
</div>
|
||||||
const powerPercent = (cabinet.powerUsed / cabinet.powerCapacity) * 100;
|
) : filteredServers.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-20 text-slate-500">
|
||||||
return (
|
<MapPin className="h-10 w-10 mb-3 opacity-40" />
|
||||||
<Card
|
<p className="text-sm">暂无服务器数据</p>
|
||||||
key={cabinet.id}
|
</div>
|
||||||
className={`bg-slate-900 border-2 cursor-pointer hover:shadow-lg transition-all ${getStatusColor(cabinet.status)}`}
|
) : viewMode === 'grid' ? (
|
||||||
onClick={() => setSelectedCabinet(cabinet)}
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
>
|
{filteredServers.map((srv) => (
|
||||||
<CardContent className="p-4">
|
<Card
|
||||||
<div className="flex items-center justify-between mb-2">
|
key={srv.id}
|
||||||
<span className="text-sm font-medium text-slate-200">{cabinet.name}</span>
|
className="bg-slate-900 border-slate-800 cursor-pointer hover:border-slate-700 transition-all"
|
||||||
{cabinet.status === 'active' && (
|
onClick={() => setSelectedServer(srv)}
|
||||||
<div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
>
|
||||||
)}
|
<CardContent className="p-4">
|
||||||
</div>
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="space-y-2">
|
<div className="flex items-center gap-2">
|
||||||
<div>
|
<div className="p-2 rounded-lg bg-blue-500/10">
|
||||||
<div className="flex justify-between text-xs text-slate-500 mb-1">
|
<Server className="h-4 w-4 text-blue-400" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between text-xs text-slate-500 mb-1">
|
<p className="text-sm font-medium text-slate-200">{srv.hostname || '--'}</p>
|
||||||
<span>功率</span>
|
<p className="text-xs text-slate-500">{srv.ip || '--'}</p>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
|
||||||
{getCabinetDevices(cabinet.id).length} 设备
|
{srv.region}
|
||||||
</Badge>
|
</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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
|
|
||||||
{viewMode === 'list' && (
|
|
||||||
<Card className="bg-slate-900 border-slate-800">
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="border-t border-slate-800">
|
<div className="border-t border-slate-800">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-slate-800">
|
<thead className="bg-slate-800">
|
||||||
<tr>
|
<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">机柜</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">IP地址</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-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>
|
<th className="px-4 py-3 text-right text-xs font-medium text-slate-400">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800">
|
<tbody className="divide-y divide-slate-800">
|
||||||
{devices.map((device) => (
|
{filteredServers.map((srv) => (
|
||||||
<tr key={device.id} className="hover:bg-slate-800/50">
|
<tr key={srv.id} className="hover:bg-slate-800/50">
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{getDeviceIcon(device.type)}
|
<Server className="h-4 w-4 text-blue-400" />
|
||||||
<span className="text-sm font-medium text-slate-200">{device.name}</span>
|
<span className="text-sm font-medium text-slate-200">{srv.hostname || '--'}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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">
|
<td className="px-4 py-3">
|
||||||
<Badge variant="outline" className="border-slate-700 text-slate-400 text-xs">
|
<Badge variant="outline" className={getStatusColor(srv.status)}>
|
||||||
{device.type}
|
{getStatusText(srv.status)}
|
||||||
</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>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-right">
|
<td className="px-4 py-3 text-right">
|
||||||
<div className="flex items-center justify-end gap-1">
|
<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" />
|
<Eye className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<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">
|
||||||
@@ -475,43 +367,39 @@ export function CabinetView() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Device Detail Dialog */}
|
{/* Server Detail Dialog */}
|
||||||
<Dialog open={!!selectedDevice} onOpenChange={() => setSelectedDevice(null)}>
|
<Dialog open={!!selectedServer} onOpenChange={() => setSelectedServer(null)}>
|
||||||
<DialogContent className="bg-slate-900 border-slate-800">
|
<DialogContent className="bg-slate-900 border-slate-800">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||||
{selectedDevice && getDeviceIcon(selectedDevice.type)}
|
<Server className="h-5 w-5 text-blue-400" />
|
||||||
{selectedDevice?.name}
|
{selectedServer?.hostname}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-500">
|
<DialogDescription className="text-slate-500">
|
||||||
{selectedDevice?.type} · {selectedDevice?.assetNo}
|
{selectedServer?.ip} · {selectedServer?.os}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-slate-800/50">
|
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-slate-800/50">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500 mb-1">序列号</p>
|
<p className="text-xs text-slate-500 mb-1">IP 地址</p>
|
||||||
<p className="text-sm text-slate-200">{selectedDevice?.sn || '--'}</p>
|
<p className="text-sm text-slate-200">{selectedServer?.ip || '--'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500 mb-1">资产编号</p>
|
<p className="text-xs text-slate-500 mb-1">操作系统</p>
|
||||||
<p className="text-sm text-slate-200">{selectedDevice?.assetNo || '--'}</p>
|
<p className="text-sm text-slate-200">{selectedServer?.os || '--'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500 mb-1">IP地址</p>
|
<p className="text-xs text-slate-500 mb-1">配置</p>
|
||||||
<p className="text-sm text-slate-200">{selectedDevice?.ip || '--'}</p>
|
<p className="text-sm text-slate-200">
|
||||||
|
{selectedServer?.cpu ?? 0} 核 / {selectedServer?.memory ?? 0} GB
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-500 mb-1">状态</p>
|
<p className="text-xs text-slate-500 mb-1">状态</p>
|
||||||
<Badge variant="outline" className={
|
<Badge variant="outline" className={selectedServer ? getStatusColor(selectedServer.status) : ''}>
|
||||||
selectedDevice?.status === 'online'
|
{selectedServer ? getStatusText(selectedServer.status) : ''}
|
||||||
? '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>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,21 +35,13 @@ const cloudProviders = [
|
|||||||
{ id: 'azure', name: 'Azure', color: 'bg-blue-600', icon: 'AZ' },
|
{ 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) {
|
export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||||
const [cloudAccounts, setCloudAccounts] = useState<any[]>([]);
|
const [cloudAccounts, setCloudAccounts] = useState<any[]>([]);
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string>('');
|
const [selectedProvider, setSelectedProvider] = useState<string>('');
|
||||||
const [selectedAccount, setSelectedAccount] = useState<string>('');
|
const [selectedAccount, setSelectedAccount] = useState<string>('');
|
||||||
const [selectedServers, setSelectedServers] = useState<string[]>([]);
|
const [selectedServers, setSelectedServers] = useState<string[]>([]);
|
||||||
|
const [cloudServers, setCloudServers] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const loadAccounts = async () => {
|
const loadAccounts = async () => {
|
||||||
@@ -73,7 +65,11 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
|||||||
if (step < 3) {
|
if (step < 3) {
|
||||||
if (step === 2) {
|
if (step === 2) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setTimeout(() => setIsLoading(false), 1000);
|
// 云主机发现功能尚未实现,暂以空列表展示
|
||||||
|
setTimeout(() => {
|
||||||
|
setCloudServers([]);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 600);
|
||||||
}
|
}
|
||||||
setStep(step + 1);
|
setStep(step + 1);
|
||||||
}
|
}
|
||||||
@@ -94,10 +90,10 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
if (selectedServers.length === mockCloudServers.length) {
|
if (selectedServers.length === cloudServers.length && cloudServers.length > 0) {
|
||||||
setSelectedServers([]);
|
setSelectedServers([]);
|
||||||
} else {
|
} 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">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-slate-400 text-sm">请选择要导入的云主机</p>
|
<p className="text-slate-400 text-sm">请选择要导入的云主机</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={handleSelectAll}>
|
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={handleSelectAll} disabled={cloudServers.length === 0}>
|
||||||
{selectedServers.length === mockCloudServers.length ? '取消全选' : '全选'}
|
{selectedServers.length === cloudServers.length && cloudServers.length > 0 ? '取消全选' : '全选'}
|
||||||
</Button>
|
</Button>
|
||||||
<Badge className="bg-blue-500/20 text-blue-400 border-0">
|
<Badge className="bg-blue-500/20 text-blue-400 border-0">
|
||||||
已选 {selectedServers.length} 台
|
已选 {selectedServers.length} 台
|
||||||
@@ -262,7 +258,7 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
|||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400 w-10">
|
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400 w-10">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedServers.length === mockCloudServers.length && mockCloudServers.length > 0}
|
checked={selectedServers.length === cloudServers.length && cloudServers.length > 0}
|
||||||
onCheckedChange={handleSelectAll}
|
onCheckedChange={handleSelectAll}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
@@ -275,35 +271,45 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800">
|
<tbody className="divide-y divide-slate-800">
|
||||||
{mockCloudServers.map((server) => (
|
{cloudServers.length === 0 ? (
|
||||||
<tr
|
<tr>
|
||||||
key={server.id}
|
<td colSpan={7} className="px-3 py-12 text-center">
|
||||||
className={`hover:bg-slate-800/50 ${selectedServers.includes(server.id) ? 'bg-blue-500/10' : ''}`}
|
<Server className="h-10 w-10 mx-auto mb-3 text-slate-600" />
|
||||||
>
|
<p className="text-slate-500">暂无云主机</p>
|
||||||
<td className="px-3 py-2">
|
<p className="text-xs text-slate-600 mt-1">云主机发现功能尚未实现,请手动添加服务器</p>
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -20,7 +20,13 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { regions } from '@/mocks/data';
|
import { regionApi } from '@/services/api';
|
||||||
|
|
||||||
|
interface RegionOption {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ExcelServerImportProps {
|
interface ExcelServerImportProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -32,13 +38,10 @@ const connectionMethods = [
|
|||||||
{ id: 'key', name: '密钥认证', description: '使用SSH密钥连接' },
|
{ id: 'key', name: '密钥认证', description: '使用SSH密钥连接' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockPreviewData = [
|
type PreviewRow = {
|
||||||
{ row: 2, hostname: 'web-server-01', ip: '192.168.1.101', port: 22, username: 'root', status: 'valid', message: '验证通过' },
|
row: number; hostname: string; ip: string; port: number;
|
||||||
{ row: 3, hostname: 'web-server-02', ip: '192.168.1.102', port: 22, username: 'root', status: 'valid', message: '验证通过' },
|
username: string; status: string; message: string;
|
||||||
{ 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: '验证通过' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
@@ -46,14 +49,40 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
|||||||
const [connectionMethod, setConnectionMethod] = useState<string>('password');
|
const [connectionMethod, setConnectionMethod] = useState<string>('password');
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
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 = () => {
|
const handleNext = () => {
|
||||||
if (step === 2 && selectedFile) {
|
if (step === 2 && selectedFile) {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
setPreviewData(mockPreviewData);
|
setPreviewData([]); // 真实解析由后端处理
|
||||||
setStep(step + 1);
|
setStep(step + 1);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { operationLogs } from '@/mocks/data';
|
import { auditApi } from '@/services/api';
|
||||||
import type { OperationLog } from '@/types';
|
import type { OperationLog } from '@/types';
|
||||||
|
|
||||||
interface SystemLogsTabProps {
|
interface SystemLogsTabProps {
|
||||||
@@ -19,8 +20,45 @@ interface SystemLogsTabProps {
|
|||||||
setSearchQuery: (v: string) => void;
|
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) {
|
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) =>
|
||||||
log.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
log.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
log.module.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"
|
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
@@ -62,48 +104,59 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp
|
|||||||
<Card className="bg-slate-900 border-slate-800">
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="border-t border-slate-800">
|
<div className="border-t border-slate-800">
|
||||||
<Table>
|
{loading ? (
|
||||||
<TableHeader>
|
<div className="flex items-center justify-center py-16">
|
||||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||||
<TableHead className="text-slate-400">时间</TableHead>
|
</div>
|
||||||
<TableHead className="text-slate-400">用户</TableHead>
|
) : filteredLogs.length === 0 ? (
|
||||||
<TableHead className="text-slate-400">模块</TableHead>
|
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||||
<TableHead className="text-slate-400">操作</TableHead>
|
<Search className="h-8 w-8 mb-3 opacity-40" />
|
||||||
<TableHead className="text-slate-400">资源</TableHead>
|
<p className="text-sm">暂无操作日志</p>
|
||||||
<TableHead className="text-slate-400">IP地址</TableHead>
|
</div>
|
||||||
<TableHead className="text-slate-400">结果</TableHead>
|
) : (
|
||||||
</TableRow>
|
<Table>
|
||||||
</TableHeader>
|
<TableHeader>
|
||||||
<TableBody>
|
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||||
{filteredLogs.map((log) => (
|
<TableHead className="text-slate-400">时间</TableHead>
|
||||||
<TableRow key={log.id} className="border-slate-800 hover:bg-slate-800/50">
|
<TableHead className="text-slate-400">用户</TableHead>
|
||||||
<TableCell>
|
<TableHead className="text-slate-400">模块</TableHead>
|
||||||
<span className="text-sm text-slate-400">
|
<TableHead className="text-slate-400">操作</TableHead>
|
||||||
{new Date(log.createdAt).toLocaleString()}
|
<TableHead className="text-slate-400">资源</TableHead>
|
||||||
</span>
|
<TableHead className="text-slate-400">IP地址</TableHead>
|
||||||
</TableCell>
|
<TableHead className="text-slate-400">结果</TableHead>
|
||||||
<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>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in new issue
Block a user