feat: 完善告警规则配置页面 — 增删改查弹窗 + 修复白屏bug + UpdateRule notify_channels序列化修复
- AlertManager: 新建/编辑告警规则弹窗(规则名称/描述/类型/数据源/表达式/级别/持续时间/通知渠道Switch)
- AlertManager: 修复白屏bug — getRules返回{list,total}未解包导致rules.map崩溃(Frontend Pitfall #1)
- AlertManager: 修复规则列表字段显示 — 原引用不存在的rule.metric/operator/threshold字段,改为rule.source/expression
- AlertManager: 新增规则删除按钮(带确认)
- alert.go: 修复UpdateRule notify_channels序列化bug — 原直接赋值[]string到text列导致500,改用toJSON()
- 同步提交前序未提交改动(WebShell/K8s/Server/Login等组件改进)
This commit is contained in:
1 parent
170818c141
commit
00d5510366
13 files changed
+2170
-496
No files matched your search
@@ -1,10 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -28,7 +36,6 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
@@ -49,22 +56,75 @@ import {
|
||||
Eye,
|
||||
Download,
|
||||
Upload,
|
||||
Filter,
|
||||
Server,
|
||||
Cpu,
|
||||
HardDrive,
|
||||
Activity,
|
||||
CheckSquare,
|
||||
Square,
|
||||
Cloud,
|
||||
FileSpreadsheet,
|
||||
RefreshCw,
|
||||
ChevronDown,
|
||||
Loader2,
|
||||
Save,
|
||||
} from 'lucide-react';
|
||||
import { serverApi } from '@/services/api';
|
||||
import type { Server as ServerType } from '@/types';
|
||||
import { CloudServerImport } from './CloudServerImport';
|
||||
import { ExcelServerImport } from './ExcelServerImport';
|
||||
|
||||
// 资产类型字典(参考 CMDB 运维平台)
|
||||
const ASSET_TYPE_OPTIONS = [
|
||||
{ value: 'physical', label: '物理机', color: 'var(--dg-orange)' },
|
||||
{ value: 'virtual', label: '虚拟机', color: 'var(--dg-blue)' },
|
||||
{ value: 'cloud', label: '云主机', color: 'var(--dg-cyan)' },
|
||||
{ value: 'container', label: '容器', color: 'var(--dg-purple)' },
|
||||
];
|
||||
|
||||
const getAssetTypeLabel = (value: string): string =>
|
||||
ASSET_TYPE_OPTIONS.find((o) => o.value === value)?.label || value || '-';
|
||||
|
||||
const getAssetTypeColor = (value: string): string =>
|
||||
ASSET_TYPE_OPTIONS.find((o) => o.value === value)?.color || 'var(--dg-text-secondary)';
|
||||
|
||||
const getConnectTypeLabel = (value: string): string => {
|
||||
if (value === 'public') return '公网';
|
||||
if (value === 'private') return '内网';
|
||||
return value || '-';
|
||||
};
|
||||
|
||||
// 表单数据结构
|
||||
interface ServerFormData {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
os: string;
|
||||
cpu: string;
|
||||
memory: string;
|
||||
disk: string;
|
||||
status: string;
|
||||
region: string;
|
||||
asset_type: string;
|
||||
connect_type: string;
|
||||
floatingip: string;
|
||||
instance_id: string;
|
||||
expire_time: string;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
const EMPTY_FORM: ServerFormData = {
|
||||
hostname: '',
|
||||
ip: '',
|
||||
os: '',
|
||||
cpu: '',
|
||||
memory: '',
|
||||
disk: '',
|
||||
status: 'running',
|
||||
region: '',
|
||||
asset_type: 'physical',
|
||||
connect_type: 'private',
|
||||
floatingip: '',
|
||||
instance_id: '',
|
||||
expire_time: '',
|
||||
remark: '',
|
||||
};
|
||||
|
||||
export function ServerManager() {
|
||||
const [servers, setServers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -75,6 +135,13 @@ export function ServerManager() {
|
||||
const [cloudImportOpen, setCloudImportOpen] = useState(false);
|
||||
const [excelImportOpen, setExcelImportOpen] = useState(false);
|
||||
|
||||
// 创建/编辑表单状态
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState<ServerFormData>(EMPTY_FORM);
|
||||
const [formSaving, setFormSaving] = useState(false);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -150,6 +217,74 @@ export function ServerManager() {
|
||||
}
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedServers.length === 0) return;
|
||||
try {
|
||||
await serverApi.batchDelete(selectedServers.map((id) => Number(id)));
|
||||
setSelectedServers([]);
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error('批量删除失败:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 打开创建表单
|
||||
const openCreateForm = () => {
|
||||
setEditingId(null);
|
||||
setFormData(EMPTY_FORM);
|
||||
setDetailsOpen(false);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
// 打开编辑表单
|
||||
const openEditForm = (server: any) => {
|
||||
setEditingId(server.id);
|
||||
setFormData({
|
||||
hostname: server.hostname || '',
|
||||
ip: server.ip || '',
|
||||
os: server.os || '',
|
||||
cpu: String(server.cpu || ''),
|
||||
memory: String(server.memory || ''),
|
||||
disk: String(server.disk || ''),
|
||||
status: server.status || 'running',
|
||||
region: getRegionName(server),
|
||||
asset_type: server.asset_type || 'physical',
|
||||
connect_type: server.connect_type || 'private',
|
||||
floatingip: server.floatingip || '',
|
||||
instance_id: server.instance_id || '',
|
||||
expire_time: server.expire_time || '',
|
||||
remark: server.remark || '',
|
||||
});
|
||||
setDetailsOpen(false);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
// 保存表单
|
||||
const handleSave = async () => {
|
||||
if (!formData.hostname || !formData.ip) return;
|
||||
try {
|
||||
setFormSaving(true);
|
||||
const payload = {
|
||||
...formData,
|
||||
cpu: Number(formData.cpu) || 0,
|
||||
memory: Number(formData.memory) || 0,
|
||||
disk: Number(formData.disk) || 0,
|
||||
};
|
||||
if (editingId) {
|
||||
await serverApi.updateServer(editingId, payload);
|
||||
} else {
|
||||
await serverApi.createServer(payload);
|
||||
}
|
||||
setFormOpen(false);
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error('保存服务器失败:', e);
|
||||
} finally {
|
||||
setFormSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: ServerType['status']) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
@@ -161,7 +296,7 @@ export function ServerManager() {
|
||||
);
|
||||
case 'stopped':
|
||||
return (
|
||||
<Badge variant="outline" className="border-slate-500/30 text-slate-400 bg-slate-500/10">
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-muted)] bg-[var(--dg-bg-tertiary)]">
|
||||
已停止
|
||||
</Badge>
|
||||
);
|
||||
@@ -185,56 +320,61 @@ export function ServerManager() {
|
||||
|
||||
const regions = Array.from(new Set(servers.map((s) => getRegionName(s)).filter(Boolean)));
|
||||
|
||||
const colSpanCount = 11;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">服务器管理</h1>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] 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"
|
||||
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
|
||||
onClick={loadData}
|
||||
>
|
||||
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
|
||||
刷新
|
||||
</Button>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800">
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800">
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
批量导入
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-slate-800 border-slate-700 text-slate-100">
|
||||
<DropdownMenuContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<DropdownMenuLabel>选择导入方式</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-slate-700" />
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
<DropdownMenuItem
|
||||
className="hover:bg-slate-700 cursor-pointer"
|
||||
className="hover:bg-[var(--dg-bg-hover)] cursor-pointer focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]"
|
||||
onClick={() => setCloudImportOpen(true)}
|
||||
>
|
||||
<Cloud className="h-4 w-4 mr-2 text-blue-400" />
|
||||
<Cloud className="h-4 w-4 mr-2 text-[var(--dg-blue)]" />
|
||||
云主机导入
|
||||
<span className="ml-2 text-xs text-slate-500">从云平台同步</span>
|
||||
<span className="ml-2 text-xs text-[var(--dg-text-muted)]">从云平台同步</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="hover:bg-slate-700 cursor-pointer"
|
||||
className="hover:bg-[var(--dg-bg-hover)] cursor-pointer focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]"
|
||||
onClick={() => setExcelImportOpen(true)}
|
||||
>
|
||||
<FileSpreadsheet className="h-4 w-4 mr-2 text-emerald-400" />
|
||||
Excel导入
|
||||
<span className="ml-2 text-xs text-slate-500">批量导入服务器</span>
|
||||
<span className="ml-2 text-xs text-[var(--dg-text-muted)]">批量导入服务器</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
|
||||
<Button
|
||||
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
||||
onClick={openCreateForm}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
添加服务器
|
||||
</Button>
|
||||
@@ -243,24 +383,24 @@ export function ServerManager() {
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<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">{servers.length}</p>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">服务器总数</p>
|
||||
<p className="text-2xl font-bold text-[var(--dg-text-primary)]">{servers.length}</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
|
||||
<div className="p-3 rounded-lg bg-[var(--dg-blue)]/10 text-[var(--dg-blue)]">
|
||||
<Server className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<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-[var(--dg-text-muted)]">运行中</p>
|
||||
<p className="text-2xl font-bold text-emerald-400">
|
||||
{servers.filter((s) => s.status === 'running').length}
|
||||
</p>
|
||||
@@ -271,11 +411,11 @@ export function ServerManager() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<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-[var(--dg-text-muted)]">维护中</p>
|
||||
<p className="text-2xl font-bold text-amber-400">
|
||||
{servers.filter((s) => s.status === 'maintenance').length}
|
||||
</p>
|
||||
@@ -286,16 +426,16 @@ export function ServerManager() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<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">
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">已停止</p>
|
||||
<p className="text-2xl font-bold text-[var(--dg-text-muted)]">
|
||||
{servers.filter((s) => s.status === 'stopped').length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-slate-500/10 text-slate-400">
|
||||
<div className="p-3 rounded-lg bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)]">
|
||||
<Power className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,24 +444,25 @@ export function ServerManager() {
|
||||
</div>
|
||||
|
||||
{/* Filters & Table */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-0">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between flex-wrap gap-4 p-4">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<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-[var(--dg-text-muted)]" />
|
||||
<Input
|
||||
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"
|
||||
className="pl-9 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-32 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectTrigger className="w-32 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder="状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="running">运行中</SelectItem>
|
||||
<SelectItem value="stopped">已停止</SelectItem>
|
||||
@@ -330,10 +471,10 @@ export function ServerManager() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={regionFilter} onValueChange={setRegionFilter}>
|
||||
<SelectTrigger className="w-32 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectTrigger className="w-32 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder="区域" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<SelectItem value="all">全部区域</SelectItem>
|
||||
{regions.map((region) => (
|
||||
<SelectItem key={region} value={region}>
|
||||
@@ -345,54 +486,64 @@ export function ServerManager() {
|
||||
</div>
|
||||
{selectedServers.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-slate-400">已选择 {selectedServers.length} 项</span>
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300">
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">已选择 {selectedServers.length} 项</span>
|
||||
<Button variant="outline" size="sm" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<Terminal className="h-4 w-4 mr-2" />
|
||||
批量命令
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300">
|
||||
<Button variant="outline" size="sm" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<RotateCw className="h-4 w-4 mr-2" />
|
||||
重启
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
|
||||
onClick={handleBatchDelete}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
批量删除
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="border-t border-slate-800">
|
||||
|
||||
<div className="border-t border-[var(--dg-border)]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="w-12">
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedServers.length === filteredServers.length && filteredServers.length > 0
|
||||
}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
className="border-slate-600"
|
||||
className="border-[var(--dg-border)]"
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-slate-400">主机名</TableHead>
|
||||
<TableHead className="text-slate-400">IP地址</TableHead>
|
||||
<TableHead className="text-slate-400">状态</TableHead>
|
||||
<TableHead className="text-slate-400">配置</TableHead>
|
||||
<TableHead className="text-slate-400">使用率</TableHead>
|
||||
<TableHead className="text-slate-400">区域</TableHead>
|
||||
<TableHead className="text-slate-400">标签</TableHead>
|
||||
<TableHead className="text-slate-400 text-right">操作</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">主机名</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">IP地址</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">资产类型</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">连接方式</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">状态</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">配置</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">使用率</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">区域</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)]">标签</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-secondary)] text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={9} className="text-center text-slate-500 py-12">
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableCell colSpan={colSpanCount} className="text-center text-[var(--dg-text-muted)] py-12">
|
||||
<RefreshCw className="h-5 w-5 animate-spin mx-auto mb-2" />
|
||||
加载中...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredServers.length === 0 ? (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={9} className="text-center text-slate-500 py-12">
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableCell colSpan={colSpanCount} className="text-center text-[var(--dg-text-muted)] py-12">
|
||||
暂无服务器数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -403,43 +554,66 @@ export function ServerManager() {
|
||||
const tags = getTags(server);
|
||||
const region = getRegionName(server);
|
||||
return (
|
||||
<TableRow key={server.id} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableRow key={server.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedServers.includes(server.id)}
|
||||
onCheckedChange={() => toggleSelect(server.id)}
|
||||
className="border-slate-600"
|
||||
className="border-[var(--dg-border)]"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-slate-500" />
|
||||
<Server className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<div>
|
||||
<p className="font-medium text-slate-200">{server.hostname}</p>
|
||||
<p className="text-xs text-slate-500">{server.os}</p>
|
||||
<p className="font-medium text-[var(--dg-text-primary)]">{server.hostname}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{server.os}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-sm text-slate-300 bg-slate-800 px-2 py-0.5 rounded">
|
||||
<code className="text-sm text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] px-2 py-0.5 rounded">
|
||||
{server.ip}
|
||||
</code>
|
||||
</TableCell>
|
||||
{/* 资产类型 */}
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs border-[var(--dg-border)]"
|
||||
style={{ color: getAssetTypeColor(server.asset_type) }}
|
||||
>
|
||||
{getAssetTypeLabel(server.asset_type)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
{/* 连接方式 */}
|
||||
<TableCell>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs px-2 py-0.5 rounded',
|
||||
server.connect_type === 'public'
|
||||
? 'text-[var(--dg-cyan)] bg-[var(--dg-cyan)]/10'
|
||||
: 'text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)]'
|
||||
)}
|
||||
>
|
||||
{getConnectTypeLabel(server.connect_type)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(server.status)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-slate-300">
|
||||
<span className="text-slate-500">{server.cpu}核</span>
|
||||
<span className="mx-1 text-slate-600">/</span>
|
||||
<span className="text-slate-500">{server.memory}GB</span>
|
||||
<span className="mx-1 text-slate-600">/</span>
|
||||
<span className="text-slate-500">{server.disk}GB</span>
|
||||
<div className="text-sm text-[var(--dg-text-secondary)]">
|
||||
<span className="text-[var(--dg-text-muted)]">{server.cpu}核</span>
|
||||
<span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
||||
<span className="text-[var(--dg-text-muted)]">{server.memory}GB</span>
|
||||
<span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
||||
<span className="text-[var(--dg-text-muted)]">{server.disk}GB</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 w-8">CPU</span>
|
||||
<div className="w-16 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<span className="text-xs text-[var(--dg-text-muted)] w-8">CPU</span>
|
||||
<div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full',
|
||||
@@ -448,11 +622,11 @@ export function ServerManager() {
|
||||
style={{ width: `${cpuUsage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400">{cpuUsage.toFixed(1)}%</span>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)]">{cpuUsage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 w-8">内存</span>
|
||||
<div className="w-16 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<span className="text-xs text-[var(--dg-text-muted)] w-8">内存</span>
|
||||
<div className="w-16 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full',
|
||||
@@ -461,12 +635,12 @@ export function ServerManager() {
|
||||
style={{ width: `${memoryUsage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400">{memoryUsage.toFixed(1)}%</span>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)]">{memoryUsage.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{region || '-'}</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{region || '-'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
@@ -474,13 +648,13 @@ export function ServerManager() {
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="outline"
|
||||
className="text-xs border-slate-700 text-slate-400 bg-slate-800/50"
|
||||
className="text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)]/50"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{tags.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
|
||||
<Badge variant="outline" className="text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
+{tags.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -488,37 +662,40 @@ export function ServerManager() {
|
||||
</TableCell>
|
||||
<TableCell className="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-blue-400">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-blue)]">
|
||||
<Terminal className="h-4 w-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<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-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 bg-slate-800 border-slate-700">
|
||||
<DropdownMenuLabel className="text-slate-300">操作</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-slate-700" />
|
||||
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
|
||||
<DropdownMenuContent align="end" className="w-48 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
<DropdownMenuItem className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer">
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
查看详情
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
|
||||
<DropdownMenuItem
|
||||
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||
onClick={() => openEditForm(server)}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
|
||||
<DropdownMenuItem className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer">
|
||||
<Terminal className="h-4 w-4 mr-2" />
|
||||
WebShell
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-slate-700" />
|
||||
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
<DropdownMenuItem className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer">
|
||||
<RotateCw className="h-4 w-4 mr-2" />
|
||||
重启
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-red-400 focus:bg-slate-700 focus:text-red-400"
|
||||
className="text-red-400 focus:bg-[var(--dg-bg-hover)] focus:text-red-400 cursor-pointer"
|
||||
onClick={() => handleDelete(server.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
@@ -538,6 +715,246 @@ export function ServerManager() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 创建/编辑服务器 Dialog */}
|
||||
<Dialog open={formOpen} onOpenChange={setFormOpen}>
|
||||
<DialogContent className="max-w-2xl bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Server className="h-5 w-5 text-[var(--dg-blue)]" />
|
||||
{editingId ? '编辑服务器' : '添加服务器'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)]">
|
||||
{editingId ? '修改服务器资产信息' : '录入新的服务器资产到 CMDB'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
{/* 基础信息 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">
|
||||
主机名 <span className="text-red-400">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="web-server-01"
|
||||
value={formData.hostname}
|
||||
onChange={(e) => setFormData({ ...formData, hostname: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">
|
||||
IP地址 <span className="text-red-400">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="10.0.0.1"
|
||||
value={formData.ip}
|
||||
onChange={(e) => setFormData({ ...formData, ip: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CMDB 资产字段 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">资产类型</Label>
|
||||
<Select
|
||||
value={formData.asset_type}
|
||||
onValueChange={(v) => setFormData({ ...formData, asset_type: v })}
|
||||
>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder="选择资产类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
{ASSET_TYPE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
<span style={{ color: opt.color }} className="mr-1">●</span>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">连接方式</Label>
|
||||
<RadioGroup
|
||||
value={formData.connect_type}
|
||||
onValueChange={(v) => setFormData({ ...formData, connect_type: v })}
|
||||
className="flex items-center gap-6 h-9"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="public" id="connect-public" />
|
||||
<Label htmlFor="connect-public" className="text-[var(--dg-text-secondary)] cursor-pointer font-normal">
|
||||
公网
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="private" id="connect-private" />
|
||||
<Label htmlFor="connect-private" className="text-[var(--dg-text-secondary)] cursor-pointer font-normal">
|
||||
内网
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 云主机扩展字段 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">弹性IP地址</Label>
|
||||
<Input
|
||||
placeholder="如 203.0.113.10"
|
||||
value={formData.floatingip}
|
||||
onChange={(e) => setFormData({ ...formData, floatingip: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">实例ID</Label>
|
||||
<Input
|
||||
placeholder="如 i-bp1abc123def"
|
||||
value={formData.instance_id}
|
||||
onChange={(e) => setFormData({ ...formData, instance_id: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">到期时间</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={formData.expire_time}
|
||||
onChange={(e) => setFormData({ ...formData, expire_time: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">所在区域</Label>
|
||||
<Input
|
||||
placeholder="如 华东1"
|
||||
value={formData.region}
|
||||
onChange={(e) => setFormData({ ...formData, region: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 更多详情 - 可折叠区域 */}
|
||||
<Collapsible open={detailsOpen} onOpenChange={setDetailsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-between text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)]"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4" />
|
||||
更多详情(OS版本 / CPU / 内存 / 磁盘 / 状态)
|
||||
</span>
|
||||
<ChevronDown className={cn('h-4 w-4 transition-transform', detailsOpen && 'rotate-180')} />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-4 pt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">操作系统</Label>
|
||||
<Input
|
||||
placeholder="如 CentOS 7.9"
|
||||
value={formData.os}
|
||||
onChange={(e) => setFormData({ ...formData, os: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">运行状态</Label>
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={(v) => setFormData({ ...formData, status: v })}
|
||||
>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<SelectItem value="running">运行中</SelectItem>
|
||||
<SelectItem value="stopped">已停止</SelectItem>
|
||||
<SelectItem value="maintenance">维护中</SelectItem>
|
||||
<SelectItem value="offline">离线</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">CPU核数</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="4"
|
||||
value={formData.cpu}
|
||||
onChange={(e) => setFormData({ ...formData, cpu: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">内存 (GB)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="8"
|
||||
value={formData.memory}
|
||||
onChange={(e) => setFormData({ ...formData, memory: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">磁盘 (GB)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="100"
|
||||
value={formData.disk}
|
||||
onChange={(e) => setFormData({ ...formData, disk: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">备注</Label>
|
||||
<Textarea
|
||||
placeholder="补充说明..."
|
||||
value={formData.remark}
|
||||
onChange={(e) => setFormData({ ...formData, remark: e.target.value })}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] min-h-[60px]"
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setFormOpen(false)}
|
||||
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)]"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!formData.hostname || !formData.ip || formSaving}
|
||||
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
||||
>
|
||||
{formSaving ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{editingId ? '保存修改' : '创建'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Import Dialogs */}
|
||||
<CloudServerImport
|
||||
open={cloudImportOpen}
|
||||
|
||||
Reference in new issue
Block a user