feat: Agent批量升级 + 版本号注入 + 服务器凭证下拉关联
- Dockerfile: ARG VERSION/BUILD_TIME 通过 ldflags 注入 agent+backend 二进制 - run.sh: 自动从 git tag 获取版本号 - docker-compose: build args 传递 VERSION/BUILD_TIME - backend: BatchUpgradeAgent SSH下载新二进制+重启, /agent/bin 静态文件服务 - frontend: 监控大屏添加批量升级按钮 (闪电图标) - frontend: 服务器编辑/添加表单改为凭证下拉选择器 - frontend: ServerForm 加载 /credentials 列表, 选择后自动填充用户名/认证方式
This commit is contained in:
1 parent
228b6b52f9
commit
ed08ea255e
9 files changed
+253
-52
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -23,20 +24,23 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Server, ChevronDown, Loader2, Save } from 'lucide-react';
|
||||
import { Server, ChevronDown, Loader2, Save, Key, Shield } from 'lucide-react';
|
||||
import http from '@/services/http';
|
||||
|
||||
// 表单数据结构 — 与后端 CreateServerRequest/UpdateServerRequest 字段对齐
|
||||
// 表单数据结构
|
||||
export interface ServerFormData {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
public_ip: string;
|
||||
port: string;
|
||||
username: string;
|
||||
auth_type: string; // password | key
|
||||
auth_type: string;
|
||||
password: string;
|
||||
ssh_key: string;
|
||||
credential_id: number | null;
|
||||
os: string;
|
||||
cpu: string;
|
||||
memory: string; // GB (表单) → MB (后端)
|
||||
memory: string;
|
||||
disk: string;
|
||||
status: string;
|
||||
tags: string;
|
||||
@@ -51,6 +55,8 @@ export const EMPTY_FORM: ServerFormData = {
|
||||
username: 'root',
|
||||
auth_type: 'password',
|
||||
password: '',
|
||||
ssh_key: '',
|
||||
credential_id: null,
|
||||
os: '',
|
||||
cpu: '',
|
||||
memory: '',
|
||||
@@ -72,6 +78,13 @@ interface ServerFormProps {
|
||||
formSaving: boolean;
|
||||
}
|
||||
|
||||
interface Credential {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
auth_type: string;
|
||||
}
|
||||
|
||||
export function ServerForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -83,6 +96,41 @@ export function ServerForm({
|
||||
onSave,
|
||||
formSaving,
|
||||
}: ServerFormProps) {
|
||||
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||||
|
||||
// 加载凭证列表
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
http.get('/credentials')
|
||||
.then((res: any) => {
|
||||
const list = Array.isArray(res) ? res : (res?.data || []);
|
||||
setCredentials(list);
|
||||
})
|
||||
.catch(() => setCredentials([]));
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// 选择凭证时自动填充
|
||||
const handleCredentialSelect = (credId: string) => {
|
||||
const id = Number(credId);
|
||||
if (!id) {
|
||||
// 手动输入模式
|
||||
setFormData({ ...formData, credential_id: null });
|
||||
return;
|
||||
}
|
||||
const cred = credentials.find(c => c.id === id);
|
||||
if (cred) {
|
||||
setFormData({
|
||||
...formData,
|
||||
credential_id: id,
|
||||
username: cred.username || 'root',
|
||||
auth_type: cred.auth_type || 'password',
|
||||
password: '', // 已关联凭证,不需要重复输入
|
||||
ssh_key: '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-h-[90vh] overflow-y-auto">
|
||||
@@ -146,48 +194,83 @@ export function ServerForm({
|
||||
</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
|
||||
value={formData.auth_type}
|
||||
onValueChange={(v) => setFormData({ ...formData, auth_type: 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="password">密码认证</SelectItem>
|
||||
<SelectItem value="key">密钥认证</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 凭证选择 — 下拉关联已有凭证 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">
|
||||
{formData.auth_type === 'key' ? 'SSH 私钥' : '登录密码'}
|
||||
{!editingId && <span className="text-red-400 ml-1">*</span>}
|
||||
{editingId && <span className="text-[var(--dg-text-muted)] ml-1 text-xs">(留空则不修改)</span>}
|
||||
<Label className="text-[var(--dg-text-secondary)] flex items-center gap-1.5">
|
||||
<Key className="h-3.5 w-3.5" /> SSH 凭证
|
||||
{!editingId && !formData.credential_id && <span className="text-red-400 ml-1">*</span>}
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder={formData.auth_type === 'key' ? '粘贴 SSH 私钥内容...' : '输入登录密码...'}
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: 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-[50px] font-mono text-sm"
|
||||
/>
|
||||
<Select
|
||||
value={formData.credential_id ? String(formData.credential_id) : ''}
|
||||
onValueChange={handleCredentialSelect}
|
||||
>
|
||||
<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)]">
|
||||
<SelectItem value="">手动输入(不关联凭证)</SelectItem>
|
||||
{credentials.map((cred) => (
|
||||
<SelectItem key={cred.id} value={String(cred.id)}>
|
||||
<span className="flex items-center gap-2">
|
||||
<Shield className="h-3 w-3 text-blue-400" />
|
||||
{cred.name} ({cred.username}@{cred.auth_type === 'key' ? '密钥' : '密码'})
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{formData.credential_id && (
|
||||
<p className="text-[10px] text-green-400/70">已关联凭证,登录信息从凭证自动获取</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 更多详情 - 可折叠区域 */}
|
||||
{/* 手动输入模式 — 仅当未选凭证时显示 */}
|
||||
{!formData.credential_id && (
|
||||
<>
|
||||
<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
|
||||
value={formData.auth_type}
|
||||
onValueChange={(v) => setFormData({ ...formData, auth_type: 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="password">密码认证</SelectItem>
|
||||
<SelectItem value="key">密钥认证</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[var(--dg-text-secondary)]">
|
||||
{formData.auth_type === 'key' ? 'SSH 私钥' : '登录密码'}
|
||||
{!editingId && <span className="text-red-400 ml-1">*</span>}
|
||||
{editingId && <span className="text-[var(--dg-text-muted)] ml-1 text-xs">(留空则不修改)</span>}
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder={formData.auth_type === 'key' ? '粘贴 SSH 私钥内容...' : '输入登录密码...'}
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: 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-[50px] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 更多详情 */}
|
||||
<Collapsible open={detailsOpen} onOpenChange={setDetailsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -300,9 +300,17 @@ export function ServerManager() {
|
||||
tags: formData.tags || undefined,
|
||||
description: formData.description || undefined,
|
||||
};
|
||||
// 密码仅在填写时提交
|
||||
if (formData.password) {
|
||||
payload.password = formData.password;
|
||||
// 凭证关联 — 下拉选择后自动关联,无需手输密码
|
||||
if (formData.credential_id) {
|
||||
payload.credential_id = formData.credential_id;
|
||||
} else {
|
||||
// 手动输入模式:仅填了密码才提交
|
||||
if (formData.password) {
|
||||
payload.password = formData.password;
|
||||
}
|
||||
if (formData.ssh_key) {
|
||||
payload.ssh_key = formData.ssh_key;
|
||||
}
|
||||
}
|
||||
if (editingId) {
|
||||
await serverApi.updateServer(editingId, payload);
|
||||
|
||||
Reference in new issue
Block a user