// 远程执行页 — 关联主机后批量执行命令 / 上传文件(Go-native SSH) // Logan.Li // 对接后端 /remote-exec/*(controller.RemoteExecHandler + pkg/remoteexec),替代已移除的 Ansible。 import { useState, useEffect } from 'react'; import { Terminal, Upload, RefreshCw, Loader2, Server as ServerIcon, CheckCircle2, XCircle, FileUp, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { serverApi, remoteExecApi, type RemoteExecResult, type RemoteUploadResult } from '@/api'; import { PageShell } from '@/components/layout/PageShell'; import { PageEmpty } from '@/components/layout/PageEmpty'; import type { JsonData } from '@/types/common'; import i18n from '@/i18n'; interface RemoteServer { id: number; hostname: string; ip: string; username: string; port: number; auth_type: string; status: string; has_auth?: boolean; } export function RemoteExecPage() { const _confirm = useConfirm(); const [servers, setServers] = useState([]); const [selected, setSelected] = useState>(new Set()); const [keyword, setKeyword] = useState(''); const [loading, setLoading] = useState(true); const [tab, setTab] = useState<'exec' | 'upload'>('exec'); const [command, setCommand] = useState(''); const [timeoutSec, setTimeoutSec] = useState(60); const [file, setFile] = useState(null); const [remotePath, setRemotePath] = useState('/tmp/'); const [execResults, setExecResults] = useState([]); const [uploadResults, setUploadResults] = useState([]); const [running, setRunning] = useState(false); const load = async () => { setLoading(true); try { const res = await serverApi.getServers({ page: 1, page_size: 500, keyword }); setServers(((res as JsonData)?.list as RemoteServer[]) || []); } catch { toast.error(i18n.t('task.remoteexecpaget.s68755a4d')); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const selIds = [...selected]; const selCount = selIds.length; const toggle = (id: number) => { const next = new Set(selected); if (next.has(id)) { next.delete(id); } else { next.add(id); } setSelected(next); }; const toggleAll = () => { setSelected(selected.size === servers.length ? new Set() : new Set(servers.map(s => s.id))); }; const doExec = async () => { if (!command.trim()) { toast.error(i18n.t('agent.agentmonitortsx.s9358aada')); return; } if (selCount === 0) { toast.error(i18n.t('task.remoteexecpaget.se62f41c4')); return; } const ok = await _confirm({ title: i18n.t('task.remoteexecpaget.s110b7142'), description: `将在 ${selCount} 台主机上执行该命令,请确认命令安全:\n\n${command}`, confirmText: i18n.t('deploy.label.execute'), variant: 'destructive', }); if (!ok) return; setRunning(true); setExecResults([]); try { const res = await remoteExecApi.execute({ server_ids: selIds, command, timeout: timeoutSec }); setExecResults(res || []); const succ = (res || []).filter(r => r.success).length; toast.success(`执行完成:${succ}/${selCount} 成功`); } catch { toast.error(i18n.t('automation.systemTaskFailed')); } finally { setRunning(false); } }; const doUpload = async () => { if (!file) { toast.error(i18n.t('task.remoteexecpaget.s68ea13ee')); return; } if (!remotePath.trim()) { toast.error(i18n.t('task.remoteexecpaget.sb68231b8')); return; } if (selCount === 0) { toast.error(i18n.t('task.remoteexecpaget.se62f41c4')); return; } const dst = remotePath.trim().endsWith('/') ? remotePath.trim() + file.name : remotePath.trim(); setRunning(true); setUploadResults([]); try { const res = await remoteExecApi.uploadFile({ server_ids: selIds, remote_path: dst, file }); setUploadResults(res || []); const succ = (res || []).filter(r => r.success).length; toast.success(`上传完成:${succ}/${selCount} 成功`); } catch { toast.error(i18n.t('common.uploadFailed')); } finally { setRunning(false); } }; return ( } contentClassName="flex-1 min-h-0" loading={loading} actions={( )} >
{/* 左侧:主机选择 */} {i18n.t('automation.remoteSelectHost')} setKeyword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && load()} className="h-7 w-40 text-xs dg-input" /> {servers.length === 0 ? ( ) : ( <>
0} onCheckedChange={toggleAll} /> 全选({servers.length} 台) {selCount > 0 && 已选 {selCount}}
{servers.map(s => ( ))} )}
{/* 右侧:操作 + 结果(内部子 Tab 保留按钮切换) // Logan.Li */}
{tab === 'exec' ? ( {i18n.t('task.remoteexecpaget.seb6e67f1')}