feat: ping through agent — send ICMP ping from workstation via agent

Adds a ping tool in the Equipment page workstation card (visible when
agent is online). User enters an IP/hostname, the request goes through
the PMS backend → WebSocket → agent → executes ping -n 4, returns
raw output displayed in a monospace block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 10:08:38 +03:00
parent 7f546d809e
commit 11300bccbf
3 changed files with 74 additions and 0 deletions

View File

@@ -599,6 +599,9 @@ function WorkstationCard({
const [updateMsg, setUpdateMsg] = useState<string | null>(null)
const [testResults, setTestResults] = useState<Record<string, { ok: boolean; msg: string } | null>>({})
const [testingDeviceId, setTestingDeviceId] = useState<string | null>(null)
const [pingHost, setPingHost] = useState('')
const [pinging, setPinging] = useState(false)
const [pingResult, setPingResult] = useState<{ ok: boolean; output?: string; error?: string } | null>(null)
const hasUpdate = !!(
ws.isOnline &&
@@ -676,6 +679,20 @@ function WorkstationCard({
}
}
const pingNow = async () => {
if (!pingHost.trim() || pinging) return
setPinging(true)
setPingResult(null)
try {
const r = await api.workstations.ping(slug, ws.id, pingHost.trim())
setPingResult(r)
} catch (err) {
setPingResult({ ok: false, error: err instanceof Error ? err.message : 'Ошибка' })
} finally {
setPinging(false)
}
}
const devices = ws.devices ?? []
return (
@@ -905,6 +922,44 @@ function WorkstationCard({
Добавить устройство
</button>
)}
{/* Ping tool — only when agent is online */}
{ws.isOnline && (
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
<p className="text-xs text-slate-400 dark:text-slate-500 mb-2">Пинг через агента</p>
<div className="flex gap-2">
<input
value={pingHost}
onChange={e => setPingHost(e.target.value)}
onKeyDown={e => e.key === 'Enter' && pingNow()}
placeholder="192.168.1.1 или hostname"
className="input text-sm font-mono flex-1 py-1.5"
/>
<button
onClick={pingNow}
disabled={pinging || !pingHost.trim()}
className="btn-secondary py-1.5 px-3 text-sm flex items-center gap-1.5 shrink-0 h-9 whitespace-nowrap"
>
{pinging ? <RefreshCw size={13} className="animate-spin" /> : <Network size={13} />}
{pinging ? 'Пинг...' : 'Пинг'}
</button>
</div>
{pingResult && (
<div className={cn(
'mt-2 rounded-lg text-xs',
pingResult.ok
? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400'
: 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400',
)}>
{pingResult.ok && pingResult.output ? (
<pre className="p-3 whitespace-pre-wrap font-mono text-xs overflow-x-auto">{pingResult.output.trim()}</pre>
) : (
<p className="p-3 flex items-center gap-1.5"><X size={11} />{pingResult.error ?? 'Ошибка'}</p>
)}
</div>
)}
</div>
)}
</div>
)}
</div>