feat: 清除全部mock数据,Ansible/日志/告警中心/角色权限接真实API;移除SQL审计(不属于项目定位)
This commit is contained in:
1 parent
73b5a48df9
commit
df697e0b25
12 files changed
+593
-903
No files matched your search
@@ -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)));
|
||||
|
||||
const filteredCabinets = cabinets.filter((cabinet) => {
|
||||
const matchesRoom = selectedRoom === 'all' || cabinet.room === selectedRoom;
|
||||
const matchesSearch = cabinet.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchesRoom && matchesSearch;
|
||||
// 拉取服务器列表 // Logan.Li
|
||||
const fetchServers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await serverApi.getServers({ page: 1, page_size: 200 });
|
||||
setServers((resp?.list ?? []) as ServerType[]);
|
||||
} catch {
|
||||
setServers([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServers();
|
||||
}, []);
|
||||
|
||||
const filteredServers = servers.filter((srv) => {
|
||||
const matchesStatus = statusFilter === 'all' || srv.status === statusFilter;
|
||||
const matchesSearch =
|
||||
srv.hostname?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
srv.ip?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchesStatus && matchesSearch;
|
||||
});
|
||||
|
||||
const getCabinetDevices = (cabinetId: string) => {
|
||||
return devices.filter((d) => d.cabinetId === cabinetId).sort((a, b) => b.startU - a.startU);
|
||||
};
|
||||
|
||||
const getDeviceIcon = (type: Device['type']) => {
|
||||
const icons = {
|
||||
server: <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>
|
||||
|
||||
@@ -3,14 +3,14 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
import {
|
||||
Cloud,
|
||||
Key,
|
||||
CheckCircle,
|
||||
@@ -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} 台
|
||||
@@ -261,8 +257,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
||||
<thead className="bg-slate-800 sticky top-0">
|
||||
<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}
|
||||
<Checkbox
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user