import { Network, Skull } from 'lucide-react'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useI18n } from '../../application/i18n/I18nProvider'; import type { useSystemManagerBackend } from '../../application/state/useSystemManagerBackend'; import { usePolling, useStableTranslate } from '../../application/state/useSystemManager'; import { listeningPortInfoEqual } from '../../domain/systemManager/pollEquals'; import type { ListeningPortInfo } from '../../domain/systemManager/types'; import { SystemPanelConfirmDialog } from './SystemPanelConfirmDialog'; import { mergePollListByKey, useStableListOrder } from './listStable'; import { SystemPanelEmpty, SystemPanelError, SystemPanelInlineError, SystemPanelList, SystemPanelLoading, SystemPanelMetaBar, SystemPanelRefreshButton, SystemPanelRoundButton, SystemPanelRow, SystemPanelSearch, SystemPanelSegmented, SystemPanelShell, SystemPanelStatusBadge, SystemPanelToolbar, } from './SystemPanelUi'; type Backend = ReturnType; type PortFilter = 'all' | 'tcp' | 'udp'; const mergePorts = ( prev: ListeningPortInfo[] | null, next: ListeningPortInfo[], ) => mergePollListByKey(prev, next, (p) => p.id, listeningPortInfoEqual); interface PortsManagerTabProps { sessionId: string; isVisible: boolean; backend: Backend; refreshIntervalSec: number; /** Network appliances: list only, no process terminate. */ allowMutations?: boolean; } export const PortsManagerTab = memo(function PortsManagerTab({ sessionId, isVisible, backend, refreshIntervalSec, allowMutations = true, }: PortsManagerTabProps) { const { t } = useI18n(); const stableT = useStableTranslate(); const intervalMs = Math.max(2, refreshIntervalSec) * 1000; const [query, setQuery] = useState(''); const [filter, setFilter] = useState('all'); const [pendingKillPid, setPendingKillPid] = useState(null); const [killBusy, setKillBusy] = useState(false); const [actionError, setActionError] = useState(null); const [listPending, setListPending] = useState(false); const sessionIdRef = useRef(sessionId); sessionIdRef.current = sessionId; useEffect(() => { setListPending(false); setPendingKillPid(null); setKillBusy(false); setActionError(null); }, [sessionId]); const fetcher = useCallback(async (): Promise => { const requestedSessionId = sessionId; try { const result = await backend.listListeningPorts(requestedSessionId); if (sessionIdRef.current !== requestedSessionId) return null; if (result.pending) { setListPending(true); return null; } setListPending(false); if (!result.success) { throw new Error(result.error || stableT('systemManager.errors.loadPorts')); } return result.ports || []; } catch (error) { if (sessionIdRef.current === requestedSessionId) setListPending(false); throw error; } }, [backend, sessionId, stableT]); const { data, error, loading, refresh } = usePolling( fetcher, intervalMs, isVisible, mergePorts, { resetKey: sessionId }, ); const isRefreshActive = loading || listPending; const filtered = useMemo(() => { const q = query.trim().toLowerCase(); return (data || []).filter((port) => { if (filter === 'tcp' && !port.protocol.startsWith('tcp')) return false; if (filter === 'udp' && !port.protocol.startsWith('udp')) return false; if (!q) return true; return ( String(port.port).includes(q) || port.address.toLowerCase().includes(q) || port.processName.toLowerCase().includes(q) || (port.pid != null && String(port.pid).includes(q)) || port.protocol.toLowerCase().includes(q) ); }); }, [data, filter, query]); const ports = useStableListOrder( filtered, (p) => p.id, `${filter}|${query}`, (a, b) => a.port - b.port || a.protocol.localeCompare(b.protocol), ); const executeTerminate = useCallback(async (pid: number) => { const requestedSessionId = sessionId; setKillBusy(true); setActionError(null); try { const result = await backend.signalSystemProcess({ sessionId: requestedSessionId, pid, signal: 'TERM', }); if (sessionIdRef.current !== requestedSessionId) return; if (result.pending) { setActionError(t('systemManager.errors.sshChannelUnavailable')); return; } if (!result.success) { setActionError(result.error || t('systemManager.errors.actionFailed')); return; } void refresh(); } finally { if (sessionIdRef.current === requestedSessionId) setKillBusy(false); } }, [backend, refresh, sessionId, t]); return ( void refresh()} /> )} > {t('systemManager.ports.meta', { count: ports.length })} {actionError ? : null} {error && !(data?.length) ? ( void refresh()} retryLabel={t('history.action.retry')} loading={loading} /> ) : !(data?.length) && (loading || listPending) ? ( ) : !ports.length ? ( ) : ( {ports.map((port) => ( {port.protocol.toUpperCase()} )} actions={allowMutations && port.pid != null ? ( setPendingKillPid(port.pid)} > ) : null} /> ))} )} { if (!open && !killBusy) setPendingKillPid(null); }} onConfirm={() => { const pid = pendingKillPid; if (pid == null) return; setPendingKillPid(null); void executeTerminate(pid); }} /> ); });