Please provide the specific file changes or a description of the modifications you have made so I can generate the commit message for you.
This commit is contained in:
@@ -1,16 +1,33 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, 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'
|
||||
import type { BatchCommandResult, Connection, ConnectionReachabilityStatus, ConnectionStatusItem } from '../types'
|
||||
|
||||
const reachabilityCopy: Record<ConnectionReachabilityStatus, { dot: string; label: string; text: string }> = {
|
||||
unknown: { dot: 'bg-slate-500', label: '未检测', text: 'text-slate-500' },
|
||||
checking: { dot: 'bg-amber-400', label: '检测中', text: 'text-amber-300' },
|
||||
online: { dot: 'bg-emerald-500 shadow-[0_0_4px_rgba(16,185,129,0.5)]', label: '在线', text: 'text-emerald-500/80' },
|
||||
offline: { dot: 'bg-slate-500', label: '离线', text: 'text-slate-500' },
|
||||
}
|
||||
|
||||
export default function BatchCommandModal({
|
||||
open,
|
||||
connections,
|
||||
connectionStatuses,
|
||||
connectionStatusDetails,
|
||||
onRefreshStatuses,
|
||||
statusError,
|
||||
statusLoading,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean
|
||||
connections: Connection[]
|
||||
connectionStatuses: Record<number, ConnectionReachabilityStatus>
|
||||
connectionStatusDetails: Record<number, ConnectionStatusItem>
|
||||
onRefreshStatuses: () => Promise<void>
|
||||
statusError: string | null
|
||||
statusLoading: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>(() => connections.slice(0, 2).map((item) => item.id))
|
||||
@@ -28,14 +45,35 @@ export default function BatchCommandModal({
|
||||
[results, selectedIds.length],
|
||||
)
|
||||
|
||||
const onlineIds = useMemo(
|
||||
() => connections.filter((item) => connectionStatuses[item.id] === 'online').map((item) => item.id),
|
||||
[connectionStatuses, connections],
|
||||
)
|
||||
|
||||
const offlineCount = useMemo(
|
||||
() => connections.filter((item) => connectionStatuses[item.id] === 'offline').length,
|
||||
[connectionStatuses, connections],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIds((prev) =>
|
||||
prev.filter(
|
||||
(id) =>
|
||||
connections.some((item) => item.id === id) &&
|
||||
connectionStatuses[id] === 'online',
|
||||
),
|
||||
)
|
||||
}, [connectionStatuses, connections])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
async function handleRun() {
|
||||
if (!selectedIds.length || !command.trim()) return
|
||||
const runnableIds = selectedIds.filter((id) => connectionStatuses[id] === 'online')
|
||||
if (!runnableIds.length || !command.trim()) return
|
||||
setRunning(true)
|
||||
setError(null)
|
||||
try {
|
||||
const response = await executeBatchCommand(selectedIds, command)
|
||||
const response = await executeBatchCommand(runnableIds, command)
|
||||
setResults(response.data.results)
|
||||
} catch (err) {
|
||||
const message =
|
||||
@@ -53,21 +91,45 @@ export default function BatchCommandModal({
|
||||
<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 className="text-blue-400 disabled:text-slate-600" disabled={statusLoading} onClick={() => setSelectedIds(onlineIds)}>
|
||||
全选
|
||||
</button>
|
||||
<button className="text-slate-400" onClick={() => setSelectedIds([])}>
|
||||
清空
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-1 text-slate-400 transition hover:text-slate-200 disabled:text-slate-600"
|
||||
disabled={statusLoading || connections.length === 0}
|
||||
onClick={() => {
|
||||
void onRefreshStatuses().catch(() => undefined)
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={12} className={statusLoading ? 'animate-spin' : ''} />
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-slate-800 px-4 py-2 text-xs text-slate-500">
|
||||
在线 {onlineIds.length} / 离线 {offlineCount} / 总数 {connections.length}
|
||||
</div>
|
||||
<div className="flex-1 space-y-1 overflow-auto p-2">
|
||||
{connections.map((connection) => {
|
||||
const checked = selectedIds.includes(connection.id)
|
||||
const reachability = connectionStatuses[connection.id] ?? 'unknown'
|
||||
const reachabilityMeta = reachabilityCopy[reachability]
|
||||
const statusDetail = connectionStatusDetails[connection.id]
|
||||
const disabled = reachability !== 'online'
|
||||
return (
|
||||
<label key={connection.id} className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2 transition hover:bg-slate-800">
|
||||
<label
|
||||
key={connection.id}
|
||||
title={statusDetail?.message || reachabilityMeta.label}
|
||||
className={`flex items-center gap-2 rounded-xl px-3 py-2 transition ${
|
||||
disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer hover:bg-slate-800'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
checked={checked}
|
||||
onChange={() =>
|
||||
setSelectedIds((prev) =>
|
||||
@@ -75,8 +137,14 @@ export default function BatchCommandModal({
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Server size={14} className={checked ? 'text-blue-400' : 'text-slate-500'} />
|
||||
<span className="truncate text-sm text-slate-300">{connection.name}</span>
|
||||
<div className="relative shrink-0">
|
||||
<Server size={14} className={checked && !disabled ? 'text-blue-400' : 'text-slate-500'} />
|
||||
<span className={`absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full border border-slate-900 ${reachabilityMeta.dot}`} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm text-slate-300">{connection.name}</div>
|
||||
<div className={`text-[10px] ${reachabilityMeta.text}`}>{reachabilityMeta.label}</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
@@ -100,7 +168,7 @@ export default function BatchCommandModal({
|
||||
</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}
|
||||
disabled={running || statusLoading || selectedIds.filter((id) => connectionStatuses[id] === 'online').length === 0}
|
||||
onClick={() => void handleRun()}
|
||||
>
|
||||
{running ? <RefreshCw size={16} className="animate-spin" /> : <Play size={16} fill="currentColor" />}
|
||||
@@ -116,11 +184,12 @@ export default function BatchCommandModal({
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-4 overflow-auto p-4">
|
||||
{statusError ? <div className="rounded-xl border border-amber-500/20 bg-amber-500/10 px-4 py-3 text-sm text-amber-200">{statusError}</div> : null}
|
||||
{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>
|
||||
<p>{statusLoading ? '正在检测主机状态...' : '请在上方输入命令并点击执行'}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{results.map((result) => (
|
||||
|
||||
Reference in New Issue
Block a user