feat: rebuild frontend with react
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { CheckCircle2, Clock, Command, Copy, Play, RefreshCw, Server, Terminal, XCircle } from 'lucide-react'
|
||||
import Modal from './Modal'
|
||||
import { executeBatchCommand } from '../services/connections'
|
||||
import type { BatchCommandResult, Connection } from '../types'
|
||||
|
||||
export default function BatchCommandModal({
|
||||
open,
|
||||
connections,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean
|
||||
connections: Connection[]
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>(() => connections.slice(0, 2).map((item) => item.id))
|
||||
const [command, setCommand] = useState('df -h\nfree -m')
|
||||
const [running, setRunning] = useState(false)
|
||||
const [results, setResults] = useState<BatchCommandResult[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const summary = useMemo(
|
||||
() => ({
|
||||
total: selectedIds.length,
|
||||
success: results.filter((item) => item.success).length,
|
||||
failure: results.filter((item) => !item.success).length,
|
||||
}),
|
||||
[results, selectedIds.length],
|
||||
)
|
||||
|
||||
if (!open) return null
|
||||
|
||||
async function handleRun() {
|
||||
if (!selectedIds.length || !command.trim()) return
|
||||
setRunning(true)
|
||||
setError(null)
|
||||
try {
|
||||
const response = await executeBatchCommand(selectedIds, command)
|
||||
setResults(response.data.results)
|
||||
} catch (err) {
|
||||
const message =
|
||||
(err as { response?: { data?: { message?: string; error?: string } } }).response?.data?.message || '批量执行失败'
|
||||
setError(message)
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title="批量执行命令" onClose={onClose} maxWidth="max-w-6xl">
|
||||
<div className="flex h-[70vh] overflow-hidden rounded-2xl border border-slate-800 bg-[#0d1117]">
|
||||
<div className="flex w-72 shrink-0 flex-col border-r border-slate-800 bg-slate-900/70">
|
||||
<div className="flex items-center justify-between border-b border-slate-800 px-4 py-3 text-sm text-slate-300">
|
||||
<span>目标主机 ({selectedIds.length})</span>
|
||||
<div className="flex gap-3 text-xs">
|
||||
<button className="text-blue-400" onClick={() => setSelectedIds(connections.map((item) => item.id))}>
|
||||
全选
|
||||
</button>
|
||||
<button className="text-slate-400" onClick={() => setSelectedIds([])}>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1 overflow-auto p-2">
|
||||
{connections.map((connection) => {
|
||||
const checked = selectedIds.includes(connection.id)
|
||||
return (
|
||||
<label key={connection.id} className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2 transition hover:bg-slate-800">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() =>
|
||||
setSelectedIds((prev) =>
|
||||
checked ? prev.filter((item) => item !== connection.id) : [...prev, connection.id],
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Server size={14} className={checked ? 'text-blue-400' : 'text-slate-500'} />
|
||||
<span className="truncate text-sm text-slate-300">{connection.name}</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="border-b border-slate-800 bg-slate-900 px-4 py-4">
|
||||
<div className="flex items-end gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm text-slate-300">
|
||||
<Terminal size={14} className="text-emerald-400" />
|
||||
批量执行脚本
|
||||
</label>
|
||||
<textarea
|
||||
rows={5}
|
||||
className="w-full resize-none rounded-2xl border border-slate-700 bg-black px-4 py-3 font-mono text-sm text-emerald-300 outline-none focus:border-blue-500"
|
||||
value={command}
|
||||
onChange={(event) => setCommand(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="flex h-[46px] items-center gap-2 rounded-xl bg-blue-600 px-6 text-white transition hover:bg-blue-500 disabled:opacity-60"
|
||||
disabled={running || selectedIds.length === 0}
|
||||
onClick={() => void handleRun()}
|
||||
>
|
||||
{running ? <RefreshCw size={16} className="animate-spin" /> : <Play size={16} fill="currentColor" />}
|
||||
{running ? '执行中...' : '开始执行'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 border-b border-slate-800 bg-slate-800/60 px-4 py-2 text-sm">
|
||||
<span className="text-slate-300">总数: {summary.total}</span>
|
||||
<span className="font-medium text-emerald-400">成功: {summary.success}</span>
|
||||
<span className="font-medium text-red-400">失败: {summary.failure}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-4 overflow-auto p-4">
|
||||
{error ? <div className="rounded-xl border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-300">{error}</div> : null}
|
||||
{!results.length && !error ? (
|
||||
<div className="flex h-full flex-col items-center justify-center text-slate-500">
|
||||
<Command size={48} className="mb-3 text-slate-700" />
|
||||
<p>请在上方输入命令并点击执行</p>
|
||||
</div>
|
||||
) : null}
|
||||
{results.map((result) => (
|
||||
<div key={result.connectionId} className="overflow-hidden rounded-2xl border border-slate-700 bg-slate-900">
|
||||
<div className="flex items-center justify-between border-b border-slate-800 bg-slate-800/80 px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{result.success ? <CheckCircle2 size={16} className="text-emerald-500" /> : <XCircle size={16} className="text-red-500" />}
|
||||
<span className="text-sm font-medium text-slate-100">{result.connectionName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-slate-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{result.durationMs}ms
|
||||
</span>
|
||||
<button
|
||||
className="flex items-center gap-1 transition hover:text-slate-300"
|
||||
onClick={() => navigator.clipboard.writeText(result.output || result.error || '')}
|
||||
>
|
||||
<Copy size={12} />
|
||||
复制输出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre className={`overflow-auto bg-black p-4 text-xs leading-relaxed ${result.success ? 'text-slate-300' : 'text-red-400'}`}>
|
||||
{result.output || result.error || ''}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user