import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined, MinusCircleOutlined, } from '@ant-design/icons' import { Drawer, Progress, Space, Tag, Typography } from 'antd' import { fetchEventSource } from '@microsoft/fetch-event-source' import { useEffect, useRef, useState } from 'react' import { useAuthStore } from '../store/auth' interface RackStatus { rack_name: string rack_host: string state: 'pending' | 'running' | 'done' | 'error' found: number ssh_ok: boolean } interface SlaveItem { rack_name: string ip: string found: number } interface InfraItem { type: 'mikrotik' | 'proxmox' ip: string } interface DevicePingItem { ip: string status: 'online' | 'offline' ping_ms?: number | null phase?: string } type PhaseState = 'idle' | 'running' | 'done' interface Props { open: boolean onClose: () => void objectName: string taskId: string | null onComplete?: () => void } export function DiscoveryDrawer({ open, onClose, objectName, taskId, onComplete }: Props) { const accessToken = useAuthStore((s) => s.accessToken) const [phase, setPhase] = useState<'connecting' | 'racks' | 'slaves' | 'infra' | 'done'>('connecting') const [currentAction, setCurrentAction] = useState(null) const [racks, setRacks] = useState([]) const [current, setCurrent] = useState(0) const [total, setTotal] = useState(0) const [slaves, setSlaves] = useState([]) const [slavesPhase, setSlavesPhase] = useState('idle') const [mikrotikPhase, setMikrotikPhase] = useState('idle') const [proxmoxPhase, setProxmoxPhase] = useState('idle') const [infraFound, setInfraFound] = useState([]) const [devicePings, setDevicePings] = useState([]) const [result, setResult] = useState<{ created: number; updated: number; skipped: number; errors: string[] } | null>(null) const ctrlRef = useRef(null) useEffect(() => { if (!open || !taskId || !accessToken) return setPhase('connecting') setCurrentAction(null) setRacks([]) setCurrent(0) setTotal(0) setSlaves([]) setSlavesPhase('idle') setMikrotikPhase('idle') setProxmoxPhase('idle') setInfraFound([]) setDevicePings([]) setResult(null) const ctrl = new AbortController() ctrlRef.current = ctrl fetchEventSource(`/api/v1/events?task_id=${taskId}`, { headers: { Authorization: `Bearer ${accessToken}` }, signal: ctrl.signal, onmessage(ev) { try { const data = JSON.parse(ev.data) if (ev.event === 'disc_action') { setCurrentAction(data.message) } else if (ev.event === 'disc_server') { setTotal(data.rack_count) setPhase('racks') } else if (ev.event === 'disc_rack_start') { setTotal(data.total) setCurrent(data.index) setRacks((prev) => { if (prev.find((r) => r.rack_name === data.rack_name)) { return prev.map((r) => r.rack_name === data.rack_name ? { ...r, state: 'running' } : r ) } return [ ...prev, { rack_name: data.rack_name, rack_host: data.rack_host, state: 'running', found: 0, ssh_ok: false }, ] }) } else if (ev.event === 'disc_rack_done') { setCurrent((c) => c + 1) setRacks((prev) => prev.map((r) => r.rack_name === data.rack_name ? { ...r, state: data.ssh_ok ? 'done' : 'error', found: data.found ?? 0, ssh_ok: data.ssh_ok } : r ) ) } else if (ev.event === 'disc_phase') { if (data.phase === 'slaves') { setPhase('slaves') setSlavesPhase('running') } else if (data.phase === 'mikrotik') { setPhase('infra') setSlavesPhase((s) => s === 'running' ? 'done' : s) setMikrotikPhase('running') } else if (data.phase === 'proxmox') { setMikrotikPhase((s) => s === 'running' ? 'done' : s) setProxmoxPhase('running') } } else if (ev.event === 'disc_slave_found') { setSlaves((prev) => [...prev, { rack_name: data.rack_name, ip: data.ip, found: data.found }]) } else if (ev.event === 'disc_infra_found') { setInfraFound((prev) => [...prev, { type: data.type, ip: data.ip }]) } else if (ev.event === 'disc_device_ping') { setCurrentAction((prev) => { if (prev) return prev const status = data.status === 'online' ? 'online' : 'offline' return `Проверка доступности ${data.ip}: ${status}` }) setDevicePings((prev) => { const item = { ip: data.ip, status: data.status === 'online' ? 'online' : 'offline', ping_ms: data.ping_ms, phase: data.phase, } satisfies DevicePingItem const withoutCurrent = prev.filter((p) => p.ip !== item.ip) return [item, ...withoutCurrent].slice(0, 12) }) } else if (ev.event === 'disc_done') { setResult({ created: data.created ?? 0, updated: data.updated ?? 0, skipped: data.skipped ?? 0, errors: data.errors ?? [], }) setSlavesPhase((s) => s === 'running' ? 'done' : s) setMikrotikPhase((s) => s === 'running' ? 'done' : s) setProxmoxPhase((s) => s === 'running' ? 'done' : s) setPhase('done') setCurrentAction(null) ctrl.abort() onComplete?.() } } catch { // ignore parse errors } }, onerror(err) { ctrl.abort() throw err }, }) return () => ctrl.abort() }, [open, taskId, accessToken]) const percent = total > 0 ? Math.round((current / total) * 100) : 0 const rackIcon = (state: RackStatus['state']) => { if (state === 'running') return if (state === 'done') return if (state === 'error') return return } const phaseIcon = (state: PhaseState) => { if (state === 'running') return if (state === 'done') return return } return ( {/* Current action */} {(phase === 'connecting' || currentAction) && (
{phase === 'connecting' && !currentAction ? ( Запуск discovery... ) : currentAction ? ( {currentAction} ) : null}
)} {/* Rack progress bar */} {(phase === 'racks' || phase === 'slaves' || phase === 'infra' || phase === 'done') && (
Стойки: {Math.min(current, total)} / {total} = total ? 100 : percent} status={phase === 'done' ? 'success' : 'active'} style={{ marginBottom: 0 }} />
)} {/* Rack list */} {racks.length > 0 && (
{racks.map((r) => (
{rackIcon(r.state)} {r.rack_name} {r.rack_host} {r.state === 'done' && ( {r.found} уст. )} {r.state === 'error' && SSH ошибка}
))}
)} {/* Ping status */} {devicePings.length > 0 && (
Ping {devicePings.map((p) => (
{p.status === 'online' ? ( ) : ( )} {p.ip} {p.status} {p.status === 'online' && p.ping_ms != null && ( {p.ping_ms} ms )}
))}
)} {/* Slave phase */} {slavesPhase !== 'idle' && (
0 ? 6 : 0 }}> {phaseIcon(slavesPhase)} Слейв-стойки {slaves.length > 0 && {slaves.length} найдено}
{slaves.map((s) => (
{s.rack_name} {s.ip} {s.found} уст.
))}
)} {/* MikroTik phase */} {mikrotikPhase !== 'idle' && (
{phaseIcon(mikrotikPhase)} MikroTik (SSH_CLIENT) {infraFound.filter((i) => i.type === 'mikrotik').map((i) => ( {i.ip} ))} {mikrotikPhase === 'done' && infraFound.filter((i) => i.type === 'mikrotik').length === 0 && ( не найден )}
)} {/* Proxmox phase */} {proxmoxPhase !== 'idle' && (
{phaseIcon(proxmoxPhase)} Proxmox (порт 8006) {infraFound.filter((i) => i.type === 'proxmox').map((i) => ( {i.ip} ))} {proxmoxPhase === 'done' && infraFound.filter((i) => i.type === 'proxmox').length === 0 && ( не найдено )}
)} {/* Final result */} {phase === 'done' && result && (
+{result.created} добавлено {result.updated} обновлено {result.skipped} без изменений {result.errors.length > 0 && (
{result.errors.map((e, i) => ( ⚠ {e} ))}
)}
)}
) }