feat: 服务器管理前后端字段对齐 — 移除CMDB幽灵字段 + SSH导向表单 + GB→MB内存转换
This commit is contained in:
1 parent
56e0cb15ac
commit
a1237587f6
4 files changed
+146
-187
No files matched your search
@@ -148,6 +148,10 @@ func (h *ServerHandler) CreateServer(c *gin.Context) {
|
|||||||
Status: "stopped",
|
Status: "stopped",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Status != "" {
|
||||||
|
server.Status = req.Status
|
||||||
|
}
|
||||||
|
|
||||||
if server.Port == 0 {
|
if server.Port == 0 {
|
||||||
server.Port = 22
|
server.Port = 22
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ type CreateServerRequest struct {
|
|||||||
CPU int `json:"cpu"`
|
CPU int `json:"cpu"`
|
||||||
Memory int64 `json:"memory"`
|
Memory int64 `json:"memory"`
|
||||||
Disk int64 `json:"disk"`
|
Disk int64 `json:"disk"`
|
||||||
|
Status string `json:"status" binding:"omitempty,oneof=running stopped maintenance error"`
|
||||||
RegionID *uint `json:"region_id"`
|
RegionID *uint `json:"region_id"`
|
||||||
CloudID *uint `json:"cloud_id"`
|
CloudID *uint `json:"cloud_id"`
|
||||||
Tags string `json:"tags"`
|
Tags string `json:"tags"`
|
||||||
|
|||||||
@@ -129,3 +129,27 @@ cd ~/devops/aiops
|
|||||||
- 成本统计从硬编码改为基于实际云账号数据计算
|
- 成本统计从硬编码改为基于实际云账号数据计算
|
||||||
- K8s 集群拓扑图(TopologyView)前后端数据联调
|
- K8s 集群拓扑图(TopologyView)前后端数据联调
|
||||||
- 告警"通知配置"按钮功能实现(通知渠道管理弹窗)
|
- 告警"通知配置"按钮功能实现(通知渠道管理弹窗)
|
||||||
|
|
||||||
|
## 2026-06-14 05:15 自动推进
|
||||||
|
|
||||||
|
- **任务**: 服务器管理模块前后端字段对齐 — 修复前端表单使用后端不支持的CMDB幽灵字段导致数据丢失 + 内存单位 GB→MB 转换 + 表格列重构
|
||||||
|
- **修改**:
|
||||||
|
- `backend/internal/models/server.go` — CreateServerRequest 添加 Status 字段(原硬编码 "stopped")
|
||||||
|
- `backend/internal/api/handlers/server.go` — CreateServer handler 改用 req.Status 替代硬编码值
|
||||||
|
- `frontend/src/components/server/ServerManager.tsx` — 完全重写表单/表格/过滤器:
|
||||||
|
- 移除后端不支持的幽灵字段:asset_type/connect_type/floatingip/instance_id/expire_time/remark/region/region_id
|
||||||
|
- 表单改为SSH连接导向:主机名/IP/公网IP/SSH端口/登录用户/认证方式(密码/密钥)/密码 + 可折叠详情区(OS/状态/CPU/内存/磁盘/标签/描述)
|
||||||
|
- 内存字段前端输入GB,提交时 ×1024 转换为MB(后端存储MB)
|
||||||
|
- 表格列从11列精简到8列,新增公网IP列,移除资产类型/连接方式/区域列
|
||||||
|
- 移除不存在的区域筛选器(servers 无 region_id 数据)
|
||||||
|
- 移除未实现的"查看详情"下拉项(无详情页)
|
||||||
|
- **结果**:
|
||||||
|
- `go build -o aiops-server cmd/main.go` 编译通过 ✅
|
||||||
|
- `npx tsc --noEmit` 类型检查通过 ✅
|
||||||
|
- `npm run build` 构建通过 ✅
|
||||||
|
- API端到端验证: POST /servers → Code 200,hostname/ip/public_ip/port/username/auth_type/memory(16384MB=16GB)/status/tags 全部正确保存 ✅
|
||||||
|
- API端到端验证: PUT /servers/:id → Code 200,更新成功 ✅
|
||||||
|
- API端到端验证: DELETE /servers/:id → Code 200 "删除成功" ✅
|
||||||
|
- 浏览器UI验证: 服务器列表页正常渲染,内存显示为GB(128GB而非131072MB),公网IP列正确显示 ✅
|
||||||
|
- 表单UI验证: 添加服务器弹窗字段正确,验证逻辑正常(hostname+ip必填时按钮启用)✅
|
||||||
|
- 服务运行中: 后端:8080 + 前端:5173 ✅
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible';
|
} from '@/components/ui/collapsible';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -53,7 +52,6 @@ import {
|
|||||||
RotateCw,
|
RotateCw,
|
||||||
Trash2,
|
Trash2,
|
||||||
Edit,
|
Edit,
|
||||||
Eye,
|
|
||||||
Download,
|
Download,
|
||||||
Upload,
|
Upload,
|
||||||
Server,
|
Server,
|
||||||
@@ -70,59 +68,39 @@ import type { Server as ServerType } from '@/types';
|
|||||||
import { CloudServerImport } from './CloudServerImport';
|
import { CloudServerImport } from './CloudServerImport';
|
||||||
import { ExcelServerImport } from './ExcelServerImport';
|
import { ExcelServerImport } from './ExcelServerImport';
|
||||||
|
|
||||||
// 资产类型字典(参考 CMDB 运维平台)
|
// 表单数据结构 — 与后端 CreateServerRequest/UpdateServerRequest 字段对齐
|
||||||
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 {
|
interface ServerFormData {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
|
public_ip: string;
|
||||||
|
port: string;
|
||||||
|
username: string;
|
||||||
|
auth_type: string; // password | key
|
||||||
|
password: string;
|
||||||
os: string;
|
os: string;
|
||||||
cpu: string;
|
cpu: string;
|
||||||
memory: string;
|
memory: string; // GB (表单) → MB (后端)
|
||||||
disk: string;
|
disk: string;
|
||||||
status: string;
|
status: string;
|
||||||
region: string;
|
tags: string;
|
||||||
asset_type: string;
|
description: string;
|
||||||
connect_type: string;
|
|
||||||
floatingip: string;
|
|
||||||
instance_id: string;
|
|
||||||
expire_time: string;
|
|
||||||
remark: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_FORM: ServerFormData = {
|
const EMPTY_FORM: ServerFormData = {
|
||||||
hostname: '',
|
hostname: '',
|
||||||
ip: '',
|
ip: '',
|
||||||
|
public_ip: '',
|
||||||
|
port: '22',
|
||||||
|
username: 'root',
|
||||||
|
auth_type: 'password',
|
||||||
|
password: '',
|
||||||
os: '',
|
os: '',
|
||||||
cpu: '',
|
cpu: '',
|
||||||
memory: '',
|
memory: '',
|
||||||
disk: '',
|
disk: '',
|
||||||
status: 'running',
|
status: 'running',
|
||||||
region: '',
|
tags: '',
|
||||||
asset_type: 'physical',
|
description: '',
|
||||||
connect_type: 'private',
|
|
||||||
floatingip: '',
|
|
||||||
instance_id: '',
|
|
||||||
expire_time: '',
|
|
||||||
remark: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ServerManager() {
|
export function ServerManager() {
|
||||||
@@ -131,7 +109,6 @@ export function ServerManager() {
|
|||||||
const [selectedServers, setSelectedServers] = useState<string[]>([]);
|
const [selectedServers, setSelectedServers] = useState<string[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
const [regionFilter, setRegionFilter] = useState<string>('all');
|
|
||||||
const [cloudImportOpen, setCloudImportOpen] = useState(false);
|
const [cloudImportOpen, setCloudImportOpen] = useState(false);
|
||||||
const [excelImportOpen, setExcelImportOpen] = useState(false);
|
const [excelImportOpen, setExcelImportOpen] = useState(false);
|
||||||
|
|
||||||
@@ -175,21 +152,13 @@ export function ServerManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取区域名称:后端 region 可能是对象 {name} 或字符串
|
|
||||||
const getRegionName = (server: any): string => {
|
|
||||||
if (!server.region) return '';
|
|
||||||
if (typeof server.region === 'string') return server.region;
|
|
||||||
return server.region.name || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredServers = servers.filter((server) => {
|
const filteredServers = servers.filter((server) => {
|
||||||
const region = getRegionName(server);
|
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
(server.hostname || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(server.hostname || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
(server.ip || '').includes(searchQuery);
|
(server.ip || '').includes(searchQuery) ||
|
||||||
|
(server.public_ip || '').includes(searchQuery);
|
||||||
const matchesStatus = statusFilter === 'all' || server.status === statusFilter;
|
const matchesStatus = statusFilter === 'all' || server.status === statusFilter;
|
||||||
const matchesRegion = regionFilter === 'all' || region === regionFilter;
|
return matchesSearch && matchesStatus;
|
||||||
return matchesSearch && matchesStatus && matchesRegion;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleSelectAll = () => {
|
const toggleSelectAll = () => {
|
||||||
@@ -243,18 +212,18 @@ export function ServerManager() {
|
|||||||
setFormData({
|
setFormData({
|
||||||
hostname: server.hostname || '',
|
hostname: server.hostname || '',
|
||||||
ip: server.ip || '',
|
ip: server.ip || '',
|
||||||
|
public_ip: server.public_ip || '',
|
||||||
|
port: String(server.port || 22),
|
||||||
|
username: server.username || 'root',
|
||||||
|
auth_type: server.auth_type || 'password',
|
||||||
|
password: '',
|
||||||
os: server.os || '',
|
os: server.os || '',
|
||||||
cpu: String(server.cpu || ''),
|
cpu: String(server.cpu || ''),
|
||||||
memory: String(server.memory || ''),
|
memory: server.memory ? String(Math.round(server.memory / 1024)) : '', // MB → GB
|
||||||
disk: String(server.disk || ''),
|
disk: String(server.disk || ''),
|
||||||
status: server.status || 'running',
|
status: server.status || 'running',
|
||||||
region: getRegionName(server),
|
tags: server.tags || '',
|
||||||
asset_type: server.asset_type || 'physical',
|
description: server.description || '',
|
||||||
connect_type: server.connect_type || 'private',
|
|
||||||
floatingip: server.floatingip || '',
|
|
||||||
instance_id: server.instance_id || '',
|
|
||||||
expire_time: server.expire_time || '',
|
|
||||||
remark: server.remark || '',
|
|
||||||
});
|
});
|
||||||
setDetailsOpen(false);
|
setDetailsOpen(false);
|
||||||
setFormOpen(true);
|
setFormOpen(true);
|
||||||
@@ -265,12 +234,25 @@ export function ServerManager() {
|
|||||||
if (!formData.hostname || !formData.ip) return;
|
if (!formData.hostname || !formData.ip) return;
|
||||||
try {
|
try {
|
||||||
setFormSaving(true);
|
setFormSaving(true);
|
||||||
const payload = {
|
const payload: Record<string, any> = {
|
||||||
...formData,
|
hostname: formData.hostname,
|
||||||
|
ip: formData.ip,
|
||||||
|
public_ip: formData.public_ip || undefined,
|
||||||
|
port: Number(formData.port) || 22,
|
||||||
|
username: formData.username || 'root',
|
||||||
|
auth_type: formData.auth_type || 'password',
|
||||||
|
os: formData.os,
|
||||||
cpu: Number(formData.cpu) || 0,
|
cpu: Number(formData.cpu) || 0,
|
||||||
memory: Number(formData.memory) || 0,
|
memory: (Number(formData.memory) || 0) * 1024, // GB → MB
|
||||||
disk: Number(formData.disk) || 0,
|
disk: Number(formData.disk) || 0,
|
||||||
|
status: formData.status,
|
||||||
|
tags: formData.tags || undefined,
|
||||||
|
description: formData.description || undefined,
|
||||||
};
|
};
|
||||||
|
// 密码仅在填写时提交
|
||||||
|
if (formData.password) {
|
||||||
|
payload.password = formData.password;
|
||||||
|
}
|
||||||
if (editingId) {
|
if (editingId) {
|
||||||
await serverApi.updateServer(editingId, payload);
|
await serverApi.updateServer(editingId, payload);
|
||||||
} else {
|
} else {
|
||||||
@@ -318,9 +300,7 @@ export function ServerManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const regions = Array.from(new Set(servers.map((s) => getRegionName(s)).filter(Boolean)));
|
const colSpanCount = 8;
|
||||||
|
|
||||||
const colSpanCount = 11;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -470,19 +450,6 @@ export function ServerManager() {
|
|||||||
<SelectItem value="offline">离线</SelectItem>
|
<SelectItem value="offline">离线</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Select value={regionFilter} onValueChange={setRegionFilter}>
|
|
||||||
<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-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
|
||||||
<SelectItem value="all">全部区域</SelectItem>
|
|
||||||
{regions.map((region) => (
|
|
||||||
<SelectItem key={region} value={region}>
|
|
||||||
{region}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
{selectedServers.length > 0 && (
|
{selectedServers.length > 0 && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -523,12 +490,10 @@ export function ServerManager() {
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-[var(--dg-text-secondary)]">主机名</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)]">IP地址</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)]">区域</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>
|
<TableHead className="text-[var(--dg-text-secondary)] text-right">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -552,7 +517,7 @@ export function ServerManager() {
|
|||||||
const cpuUsage = Number(server.cpu_usage || 0);
|
const cpuUsage = Number(server.cpu_usage || 0);
|
||||||
const memoryUsage = Number(server.memory_usage || 0);
|
const memoryUsage = Number(server.memory_usage || 0);
|
||||||
const tags = getTags(server);
|
const tags = getTags(server);
|
||||||
const region = getRegionName(server);
|
const memGB = server.memory ? Math.round(server.memory / 1024) : 0;
|
||||||
return (
|
return (
|
||||||
<TableRow key={server.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
<TableRow key={server.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -576,35 +541,21 @@ export function ServerManager() {
|
|||||||
{server.ip}
|
{server.ip}
|
||||||
</code>
|
</code>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{/* 资产类型 */}
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
{server.public_ip ? (
|
||||||
variant="outline"
|
<code className="text-sm text-[var(--dg-cyan)] bg-[var(--dg-cyan)]/10 px-2 py-0.5 rounded">
|
||||||
className="text-xs border-[var(--dg-border)]"
|
{server.public_ip}
|
||||||
style={{ color: getAssetTypeColor(server.asset_type) }}
|
</code>
|
||||||
>
|
) : (
|
||||||
{getAssetTypeLabel(server.asset_type)}
|
<span className="text-sm text-[var(--dg-text-muted)]">-</span>
|
||||||
</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>
|
||||||
<TableCell>{getStatusBadge(server.status)}</TableCell>
|
<TableCell>{getStatusBadge(server.status)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-sm text-[var(--dg-text-secondary)]">
|
<div className="text-sm text-[var(--dg-text-secondary)]">
|
||||||
<span className="text-[var(--dg-text-muted)]">{server.cpu}核</span>
|
<span className="text-[var(--dg-text-muted)]">{server.cpu}核</span>
|
||||||
<span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</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="text-[var(--dg-text-muted)]">{memGB}GB</span>
|
||||||
<span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
<span className="mx-1 text-[var(--dg-text-muted)] opacity-50">/</span>
|
||||||
<span className="text-[var(--dg-text-muted)]">{server.disk}GB</span>
|
<span className="text-[var(--dg-text-muted)]">{server.disk}GB</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -639,9 +590,6 @@ export function ServerManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
<span className="text-sm text-[var(--dg-text-secondary)]">{region || '-'}</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{tags.slice(0, 3).map((tag) => (
|
{tags.slice(0, 3).map((tag) => (
|
||||||
@@ -674,10 +622,6 @@ export function ServerManager() {
|
|||||||
<DropdownMenuContent align="end" className="w-48 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
<DropdownMenuContent align="end" className="w-48 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
|
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">操作</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
<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
|
<DropdownMenuItem
|
||||||
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||||
onClick={() => openEditForm(server)}
|
onClick={() => openEditForm(server)}
|
||||||
@@ -755,91 +699,68 @@ export function ServerManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CMDB 资产字段 */}
|
{/* SSH 连接配置 */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-[var(--dg-text-secondary)]">资产类型</Label>
|
<Label className="text-[var(--dg-text-secondary)]">公网IP</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="如 203.0.113.10"
|
||||||
|
value={formData.public_ip}
|
||||||
|
onChange={(e) => setFormData({ ...formData, public_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 className="space-y-1.5">
|
||||||
|
<Label className="text-[var(--dg-text-secondary)]">SSH端口</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="22"
|
||||||
|
value={formData.port}
|
||||||
|
onChange={(e) => setFormData({ ...formData, port: 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
|
||||||
|
placeholder="root"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={(e) => setFormData({ ...formData, username: 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
|
<Select
|
||||||
value={formData.asset_type}
|
value={formData.auth_type}
|
||||||
onValueChange={(v) => setFormData({ ...formData, asset_type: v })}
|
onValueChange={(v) => setFormData({ ...formData, auth_type: v })}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||||
<SelectValue placeholder="选择资产类型" />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||||
{ASSET_TYPE_OPTIONS.map((opt) => (
|
<SelectItem value="password">密码认证</SelectItem>
|
||||||
<SelectItem key={opt.value} value={opt.value}>
|
<SelectItem value="key">密钥认证</SelectItem>
|
||||||
<span style={{ color: opt.color }} className="mr-1">●</span>
|
|
||||||
{opt.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
{/* 云主机扩展字段 */}
|
<div className="space-y-1.5">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<Label className="text-[var(--dg-text-secondary)]">
|
||||||
<div className="space-y-1.5">
|
{formData.auth_type === 'key' ? 'SSH 私钥' : '登录密码'}
|
||||||
<Label className="text-[var(--dg-text-secondary)]">弹性IP地址</Label>
|
{!editingId && <span className="text-red-400 ml-1">*</span>}
|
||||||
<Input
|
{editingId && <span className="text-[var(--dg-text-muted)] ml-1 text-xs">(留空则不修改)</span>}
|
||||||
placeholder="如 203.0.113.10"
|
</Label>
|
||||||
value={formData.floatingip}
|
<Textarea
|
||||||
onChange={(e) => setFormData({ ...formData, floatingip: e.target.value })}
|
placeholder={formData.auth_type === 'key' ? '粘贴 SSH 私钥内容...' : '输入登录密码...'}
|
||||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
value={formData.password}
|
||||||
/>
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||||
</div>
|
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] min-h-[50px] font-mono text-sm"
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* 更多详情 - 可折叠区域 */}
|
{/* 更多详情 - 可折叠区域 */}
|
||||||
@@ -852,7 +773,7 @@ export function ServerManager() {
|
|||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Server className="h-4 w-4" />
|
<Server className="h-4 w-4" />
|
||||||
更多详情(OS版本 / CPU / 内存 / 磁盘 / 状态)
|
更多详情(OS版本 / 运行状态 / CPU / 内存 / 磁盘 / 标签 / 描述)
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className={cn('h-4 w-4 transition-transform', detailsOpen && 'rotate-180')} />
|
<ChevronDown className={cn('h-4 w-4 transition-transform', detailsOpen && 'rotate-180')} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -919,11 +840,20 @@ export function ServerManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-[var(--dg-text-secondary)]">备注</Label>
|
<Label className="text-[var(--dg-text-secondary)]">标签</Label>
|
||||||
|
<Input
|
||||||
|
placeholder='JSON 格式,如 {"project":"web","team":"ops"}'
|
||||||
|
value={formData.tags}
|
||||||
|
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
|
||||||
|
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] font-mono text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="补充说明..."
|
placeholder="补充说明..."
|
||||||
value={formData.remark}
|
value={formData.description}
|
||||||
onChange={(e) => setFormData({ ...formData, remark: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, description: 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]"
|
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>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user