重写ssh前端组件,通用化
This commit is contained in:
parent
bb6985341f
commit
c9b8157754
@ -42,12 +42,12 @@ const Toast = React.forwardRef<
|
|||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
VariantProps<typeof toastVariants>
|
VariantProps<typeof toastVariants>
|
||||||
>(({ className, variant, ...props }, ref) => {
|
>(({ className, variant, duration = 3000, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<ToastPrimitives.Root
|
<ToastPrimitives.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(toastVariants({ variant }), className)}
|
className={cn(toastVariants({ variant }), className)}
|
||||||
duration={3000}
|
duration={duration}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,8 +13,11 @@ export function Toaster() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
{toasts.map(({ id, title, description, action, ...props }) => (
|
{toasts.map(({ id, title, description, action, ...props }) => {
|
||||||
<Toast key={id} {...props}>
|
// 确保duration有默认值,不被undefined覆盖
|
||||||
|
const duration = props.duration ?? 3000
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props} duration={duration}>
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
{description && (
|
{description && (
|
||||||
@ -24,7 +27,8 @@ export function Toaster() {
|
|||||||
{action}
|
{action}
|
||||||
<ToastClose />
|
<ToastClose />
|
||||||
</Toast>
|
</Toast>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
<ToastViewport />
|
<ToastViewport />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -11,6 +11,10 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
MonitorDot,
|
||||||
|
Tags,
|
||||||
|
Tag,
|
||||||
|
FileText,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@ -69,17 +73,21 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
|
|||||||
<CardContent className="flex flex-1 flex-col gap-3 p-3">
|
<CardContent className="flex flex-1 flex-col gap-3 p-3">
|
||||||
{/* 基础信息 */}
|
{/* 基础信息 */}
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-muted/50">
|
{/* 左侧:操作系统大图标 */}
|
||||||
|
<div className="flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-lg bg-muted/50">
|
||||||
{server.osType ? (
|
{server.osType ? (
|
||||||
<div className="h-5 w-5 text-primary">{getOsIcon(server.osType)}</div>
|
<div className="h-8 w-8 text-primary">{getOsIcon(server.osType)}</div>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="h-5 w-5 rounded" />
|
<Skeleton className="h-8 w-8 rounded" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||||
|
{/* 第一行:状态徽章 + 分类 */}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 w-2 rounded-full ${
|
className={`h-2 w-2 rounded-full flex-shrink-0 ${
|
||||||
server.status === 'ONLINE'
|
server.status === 'ONLINE'
|
||||||
? 'bg-emerald-500 animate-pulse'
|
? 'bg-emerald-500 animate-pulse'
|
||||||
: server.status === 'OFFLINE'
|
: server.status === 'OFFLINE'
|
||||||
@ -108,31 +116,96 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 服务器名图标:NAS(QC-NAS) */}
|
||||||
{renderValue(
|
{renderValue(
|
||||||
<h3 className="truncate text-sm font-semibold text-foreground">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
|
<Tag className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||||
|
<span className="font-semibold text-foreground truncate">
|
||||||
{server.serverName}
|
{server.serverName}
|
||||||
</h3>,
|
{server.hostname && (
|
||||||
|
<span className="text-muted-foreground font-normal">({server.hostname})</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>,
|
||||||
'w-32',
|
'w-32',
|
||||||
{ skeleton: true }
|
{ skeleton: true }
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
||||||
<Network className="h-3 w-3 flex-shrink-0" />
|
{/* IP图标:172.22.222.111 */}
|
||||||
|
<div className="flex items-center gap-1.5 text-xs">
|
||||||
|
<Network className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||||
{renderValue(
|
{renderValue(
|
||||||
<span className="font-mono text-xs text-foreground">{server.hostIp}</span>,
|
<span className="font-mono text-foreground">{server.hostIp}</span>,
|
||||||
'w-24',
|
'w-24',
|
||||||
{ skeleton: true }
|
{ skeleton: true }
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{server.hostname && (
|
|
||||||
<span className="truncate text-[11px] text-muted-foreground">{server.hostname}</span>
|
{/* 操作系统版本图标:Linux Debian... */}
|
||||||
)}
|
<div className="flex items-center gap-1.5 text-xs">
|
||||||
{server.osType && (
|
<MonitorDot className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||||
<span className="text-[11px] text-muted-foreground">
|
{renderValue(
|
||||||
{(server.osType && OsTypeLabels[server.osType]?.label) || server.osType}
|
server.osType ? (
|
||||||
|
<span className="text-muted-foreground truncate">
|
||||||
|
{OsTypeLabels[server.osType]?.label || server.osType}
|
||||||
{server.osVersion && ` ${server.osVersion}`}
|
{server.osVersion && ` ${server.osVersion}`}
|
||||||
</span>
|
</span>
|
||||||
|
) : null,
|
||||||
|
'w-40',
|
||||||
|
{ skeleton: true }
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 标签图标:华为云 阿里云 */}
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Tags className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||||
|
{renderValue(
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const tags = server.tags ? JSON.parse(server.tags) : [];
|
||||||
|
if (Array.isArray(tags) && tags.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{tags.slice(0, 3).map((tag: string, index: number) => (
|
||||||
|
<Badge key={index} variant="outline" className="text-[10px] py-0 px-1.5">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{tags.length > 3 && (
|
||||||
|
<Badge variant="outline" className="text-[10px] py-0 px-1.5">
|
||||||
|
+{tags.length - 3}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
'w-32',
|
||||||
|
{ skeleton: true }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 描述图标:测试 */}
|
||||||
|
<div className="flex items-start gap-1.5">
|
||||||
|
<FileText className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground mt-0.5" />
|
||||||
|
{renderValue(
|
||||||
|
server.description ? (
|
||||||
|
<p className="text-[11px] text-muted-foreground line-clamp-2 flex-1">
|
||||||
|
{server.description}
|
||||||
|
</p>
|
||||||
|
) : null,
|
||||||
|
'w-full',
|
||||||
|
{ skeleton: true }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧:展开按钮 */}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -222,9 +295,10 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
{/* 展开内容 */}
|
{/* 展开内容 - 仅显示SSH信息和大图标配置 */}
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="absolute left-[-1px] right-[-1px] top-full z-20 space-y-3 rounded-b-xl border-x border-b border-border bg-card p-3 text-xs shadow-lg group-hover:border-primary/40">
|
<div className="absolute left-[-1px] right-[-1px] top-full z-20 space-y-3 rounded-b-xl border-x border-b border-border bg-card p-3 text-xs shadow-lg group-hover:border-primary/40">
|
||||||
|
{/* SSH连接信息 */}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{renderValue(
|
{renderValue(
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
@ -241,10 +315,12 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 详细配置信息 - 大图标展示 */}
|
||||||
<div className="grid grid-cols-3 gap-2 text-center">
|
<div className="grid grid-cols-3 gap-2 text-center">
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary">
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||||
<Cpu className="h-4 w-4" />
|
<Cpu className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
{renderValue(
|
{renderValue(
|
||||||
server.cpuCores ? (
|
server.cpuCores ? (
|
||||||
@ -255,8 +331,8 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary">
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||||
<MemoryStick className="h-4 w-4" />
|
<MemoryStick className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
{renderValue(
|
{renderValue(
|
||||||
server.memorySize ? (
|
server.memorySize ? (
|
||||||
@ -267,8 +343,8 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary">
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||||
<HardDrive className="h-4 w-4" />
|
<HardDrive className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
{renderValue(
|
{renderValue(
|
||||||
server.diskSize ? (
|
server.diskSize ? (
|
||||||
@ -279,33 +355,6 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{server.tags && (() => {
|
|
||||||
try {
|
|
||||||
const tags = JSON.parse(server.tags);
|
|
||||||
if (Array.isArray(tags) && tags.length > 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{tags.slice(0, 3).map((tag: string, index: number) => (
|
|
||||||
<Badge key={index} variant="outline" className="text-[11px]">
|
|
||||||
{tag}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{tags.length > 3 && (
|
|
||||||
<Badge variant="outline" className="text-[11px]">+{tags.length - 3}</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{server.description ? (
|
|
||||||
<p className="text-xs text-muted-foreground line-clamp-3">{server.description}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -29,11 +29,17 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/collapsible';
|
||||||
import type { ServerResponse, ServerCategoryResponse } from '../types';
|
import type { ServerResponse, ServerCategoryResponse } from '../types';
|
||||||
import { OsType, OsTypeLabels, AuthType, AuthTypeLabels, ServerStatus } from '../types';
|
import { OsType, OsTypeLabels, AuthType, AuthTypeLabels, ServerStatus } from '../types';
|
||||||
import { createServer, updateServer, getServerCategories } from '../service';
|
import { createServer, updateServer, getServerCategories } from '../service';
|
||||||
import { serverFormSchema, type ServerFormValues } from '../schema';
|
import { serverFormSchema, type ServerFormValues } from '../schema';
|
||||||
import { Eye, EyeOff, X, Plus } from 'lucide-react';
|
import { Eye, EyeOff, X, Plus, ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
interface ServerEditDialogProps {
|
interface ServerEditDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -54,6 +60,7 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
|
|||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [showPassphrase, setShowPassphrase] = useState(false);
|
const [showPassphrase, setShowPassphrase] = useState(false);
|
||||||
const [tagInput, setTagInput] = useState('');
|
const [tagInput, setTagInput] = useState('');
|
||||||
|
const [systemInfoOpen, setSystemInfoOpen] = useState(true);
|
||||||
const isEdit = !!server?.id;
|
const isEdit = !!server?.id;
|
||||||
|
|
||||||
const form = useForm<ServerFormValues>({
|
const form = useForm<ServerFormValues>({
|
||||||
@ -162,11 +169,7 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
|
|||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
// 错误已在request拦截器中处理,这里不再重复显示toast
|
||||||
variant: 'destructive',
|
|
||||||
title: isEdit ? '更新失败' : '创建失败',
|
|
||||||
description: error.response?.data?.message || '操作失败',
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -181,9 +184,13 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
|
|||||||
|
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
{/* 基础信息区 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
|
基础信息
|
||||||
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{/* 基本信息 */}
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="serverName"
|
name="serverName"
|
||||||
@ -200,36 +207,6 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="hostIp"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
IP地址 <span className="text-destructive">*</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="例如:192.168.1.100" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="hostname"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>主机名</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="例如:prod-server-01" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="categoryId"
|
name="categoryId"
|
||||||
@ -259,188 +236,24 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* SSH 配置 */}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="sshPort"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>SSH端口</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
{...field}
|
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="sshUser"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>SSH用户名</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="例如:root" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 认证方式 */}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="authType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>认证方式</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={field.value || 'none'}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const authType = value === 'none' ? undefined : (value as AuthType);
|
|
||||||
field.onChange(authType);
|
|
||||||
// 切换认证方式时清空相关字段
|
|
||||||
if (authType === AuthType.PASSWORD) {
|
|
||||||
form.setValue('sshPrivateKey', '');
|
|
||||||
form.setValue('sshPassphrase', '');
|
|
||||||
} else if (authType === AuthType.KEY) {
|
|
||||||
form.setValue('sshPassword', '');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="选择认证方式" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">不选择</SelectItem>
|
|
||||||
{Object.entries(AuthTypeLabels).map(([key, { label }]) => (
|
|
||||||
<SelectItem key={key} value={key}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 根据认证方式显示不同的输入框 */}
|
|
||||||
{form.watch('authType') === AuthType.PASSWORD && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="sshPassword"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>SSH密码</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
placeholder="输入SSH密码"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeOff className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{form.watch('authType') === AuthType.KEY && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="sshPrivateKey"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="col-span-2">
|
|
||||||
<FormLabel>SSH私钥</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder="请粘贴SSH私钥内容"
|
|
||||||
rows={6}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="sshPassphrase"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>私钥密码(可选)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
type={showPassphrase ? 'text' : 'password'}
|
|
||||||
placeholder="如果私钥加密,请输入密码"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
|
||||||
onClick={() => setShowPassphrase(!showPassphrase)}
|
|
||||||
>
|
|
||||||
{showPassphrase ? (
|
|
||||||
<EyeOff className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 操作系统信息 */}
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="osType"
|
name="osType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>操作系统类型</FormLabel>
|
<FormLabel>
|
||||||
|
操作系统类型 <span className="text-destructive">*</span>
|
||||||
|
</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
value={field.value || 'none'}
|
value={field.value}
|
||||||
onValueChange={(value) => field.onChange(value === 'none' ? undefined : (value as OsType))}
|
onValueChange={(value) => field.onChange(value as OsType)}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="选择系统类型" />
|
<SelectValue placeholder="请选择系统类型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">不选择</SelectItem>
|
|
||||||
{Object.entries(OsTypeLabels).map(([key, value]) => (
|
{Object.entries(OsTypeLabels).map(([key, value]) => (
|
||||||
<SelectItem key={key} value={key}>
|
<SelectItem key={key} value={key}>
|
||||||
{value.label}
|
{value.label}
|
||||||
@ -453,81 +266,6 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="osVersion"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>操作系统版本</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="例如:CentOS 7.9" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 硬件配置 */}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuCores"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>CPU核心数</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="例如:8"
|
|
||||||
{...field}
|
|
||||||
value={field.value || ''}
|
|
||||||
onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="memorySize"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>内存大小(GB)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="例如:16"
|
|
||||||
{...field}
|
|
||||||
value={field.value || ''}
|
|
||||||
onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="diskSize"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>磁盘大小(GB)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="例如:500"
|
|
||||||
{...field}
|
|
||||||
value={field.value || ''}
|
|
||||||
onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 标签 */}
|
{/* 标签 */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@ -622,6 +360,325 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* SSH连接信息区 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
|
SSH连接信息
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hostIp"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
IP地址 <span className="text-destructive">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="例如:192.168.1.100" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sshPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
SSH端口 <span className="text-destructive">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sshUser"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
SSH用户名 <span className="text-destructive">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="例如:root" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 认证方式 */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="authType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
认证方式 <span className="text-destructive">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const authType = value as AuthType;
|
||||||
|
field.onChange(authType);
|
||||||
|
// 切换认证方式时清空相关字段
|
||||||
|
if (authType === AuthType.PASSWORD) {
|
||||||
|
form.setValue('sshPrivateKey', '');
|
||||||
|
form.setValue('sshPassphrase', '');
|
||||||
|
} else if (authType === AuthType.KEY) {
|
||||||
|
form.setValue('sshPassword', '');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="请选择认证方式" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.entries(AuthTypeLabels).map(([key, { label }]) => (
|
||||||
|
<SelectItem key={key} value={key}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 根据认证方式显示不同的输入框 */}
|
||||||
|
{form.watch('authType') === AuthType.PASSWORD && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sshPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
SSH密码 <span className="text-destructive">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
placeholder="输入SSH密码"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.watch('authType') === AuthType.KEY && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sshPrivateKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>
|
||||||
|
SSH私钥 <span className="text-destructive">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="请粘贴SSH私钥内容"
|
||||||
|
rows={6}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sshPassphrase"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>私钥密码(可选)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type={showPassphrase ? 'text' : 'password'}
|
||||||
|
placeholder="如果私钥加密,请输入密码"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||||
|
onClick={() => setShowPassphrase(!showPassphrase)}
|
||||||
|
>
|
||||||
|
{showPassphrase ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* 系统信息区(自动采集 - 可折叠) */}
|
||||||
|
<Collapsible open={systemInfoOpen} onOpenChange={setSystemInfoOpen}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<CollapsibleTrigger className="flex w-full items-center justify-between hover:opacity-70 transition-opacity">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
|
系统信息
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs text-muted-foreground">测试连接后自动采集</span>
|
||||||
|
</div>
|
||||||
|
<ChevronDown className={`h-4 w-4 transition-transform ${systemInfoOpen ? 'rotate-180' : ''}`} />
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hostname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
主机名 <span className="text-xs text-muted-foreground">(自动采集)</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="测试连接后自动填充" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="osVersion"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
系统版本 <span className="text-xs text-muted-foreground">(自动采集)</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="测试连接后自动填充" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 硬件配置 */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="cpuCores"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
CPU核心数 <span className="text-xs text-muted-foreground">(自动采集)</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="测试连接后自动填充"
|
||||||
|
{...field}
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="memorySize"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
内存大小(GB) <span className="text-xs text-muted-foreground">(自动采集)</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="测试连接后自动填充"
|
||||||
|
{...field}
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="diskSize"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
磁盘大小(GB) <span className="text-xs text-muted-foreground">(自动采集)</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="测试连接后自动填充"
|
||||||
|
{...field}
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
|
|||||||
@ -173,6 +173,8 @@ const ServerList: React.FC = () => {
|
|||||||
title: '连接成功',
|
title: '连接成功',
|
||||||
description: `${result.hostname} - ${result.osVersion} (响应时间: ${result.responseTime}ms)`,
|
description: `${result.hostname} - ${result.osVersion} (响应时间: ${result.responseTime}ms)`,
|
||||||
});
|
});
|
||||||
|
// 测试连接成功后刷新数据
|
||||||
|
await loadServers();
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
|
|||||||
@ -32,38 +32,43 @@ export const serverFormSchema = z.object({
|
|||||||
.min(1, 'SSH端口不能小于1')
|
.min(1, 'SSH端口不能小于1')
|
||||||
.max(65535, 'SSH端口不能大于65535')
|
.max(65535, 'SSH端口不能大于65535')
|
||||||
.default(22),
|
.default(22),
|
||||||
sshUser: z.string().max(50, 'SSH用户名不能超过50个字符').optional(),
|
sshUser: z.string().min(1, 'SSH用户名不能为空').max(50, 'SSH用户名不能超过50个字符'),
|
||||||
authType: z.nativeEnum(AuthType).optional(),
|
authType: z.nativeEnum(AuthType, {
|
||||||
|
errorMap: () => ({ message: '请选择认证方式' })
|
||||||
|
}),
|
||||||
sshPassword: z.string().max(200, 'SSH密码不能超过200个字符').optional(),
|
sshPassword: z.string().max(200, 'SSH密码不能超过200个字符').optional(),
|
||||||
sshPrivateKey: z.string().optional(),
|
sshPrivateKey: z.string().optional(),
|
||||||
sshPassphrase: z.string().max(200, '私钥密码不能超过200个字符').optional(),
|
sshPassphrase: z.string().max(200, '私钥密码不能超过200个字符').optional(),
|
||||||
categoryId: z.number().optional(),
|
categoryId: z.number().optional(),
|
||||||
osType: z.nativeEnum(OsType).optional(),
|
osType: z.nativeEnum(OsType, {
|
||||||
|
errorMap: () => ({ message: '请选择操作系统类型' })
|
||||||
|
}),
|
||||||
osVersion: z.string().max(100, '操作系统版本不能超过100个字符').optional(),
|
osVersion: z.string().max(100, '操作系统版本不能超过100个字符').optional(),
|
||||||
hostname: z.string().max(100, '主机名不能超过100个字符').optional(),
|
hostname: z.string().max(100, '主机名不能超过100个字符').optional(),
|
||||||
status: z.nativeEnum(ServerStatus).optional(),
|
status: z.nativeEnum(ServerStatus).optional(),
|
||||||
description: z.string().max(500, '描述不能超过500个字符').optional(),
|
description: z.string().max(500, '描述不能超过500个字符').optional(),
|
||||||
cpuCores: z.number().min(1, 'CPU核心数必须大于0').optional(),
|
cpuCores: z.union([z.number().min(1, 'CPU核心数必须大于0'), z.null(), z.undefined()]).optional(),
|
||||||
memorySize: z.number().min(1, '内存大小必须大于0').optional(),
|
memorySize: z.union([z.number().min(1, '内存大小必须大于0'), z.null(), z.undefined()]).optional(),
|
||||||
diskSize: z.number().min(1, '磁盘大小必须大于0').optional(),
|
diskSize: z.union([z.number().min(1, '磁盘大小必须大于0'), z.null(), z.undefined()]).optional(),
|
||||||
tags: z.string().optional(),
|
tags: z.string().optional(),
|
||||||
}).refine(
|
}).superRefine((data, ctx) => {
|
||||||
(data) => {
|
|
||||||
// 如果选择密码认证,必须填写密码
|
// 如果选择密码认证,必须填写密码
|
||||||
if (data.authType === AuthType.PASSWORD && !data.sshPassword) {
|
if (data.authType === AuthType.PASSWORD && !data.sshPassword) {
|
||||||
return false;
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
message: 'SSH密码不能为空',
|
||||||
|
path: ['sshPassword'],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// 如果选择密钥认证,必须填写私钥
|
// 如果选择密钥认证,必须填写私钥
|
||||||
if (data.authType === AuthType.KEY && !data.sshPrivateKey) {
|
if (data.authType === AuthType.KEY && !data.sshPrivateKey) {
|
||||||
return false;
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
message: 'SSH私钥不能为空',
|
||||||
|
path: ['sshPrivateKey'],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return true;
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
message: '密码认证必须填写密码,密钥认证必须填写私钥',
|
|
||||||
path: ['authType'],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ServerCategoryFormValues = z.infer<typeof serverCategoryFormSchema>;
|
export type ServerCategoryFormValues = z.infer<typeof serverCategoryFormSchema>;
|
||||||
export type ServerFormValues = z.infer<typeof serverFormSchema>;
|
export type ServerFormValues = z.infer<typeof serverFormSchema>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user