[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
242
components/systemManager/PortsManagerTab.tsx
Normal file
242
components/systemManager/PortsManagerTab.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
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<typeof useSystemManagerBackend>;
|
||||
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<PortFilter>('all');
|
||||
const [pendingKillPid, setPendingKillPid] = useState<number | null>(null);
|
||||
const [killBusy, setKillBusy] = useState(false);
|
||||
const [actionError, setActionError] = useState<string | null>(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<ListeningPortInfo[] | null> => {
|
||||
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 (
|
||||
<SystemPanelShell section="system-manager-ports">
|
||||
<SystemPanelToolbar
|
||||
trailing={(
|
||||
<SystemPanelRefreshButton
|
||||
title={t('history.action.refresh')}
|
||||
loading={isRefreshActive}
|
||||
onClick={() => void refresh()}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<SystemPanelSearch
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
placeholder={t('systemManager.ports.search')}
|
||||
/>
|
||||
</SystemPanelToolbar>
|
||||
|
||||
<SystemPanelSegmented
|
||||
value={filter}
|
||||
onChange={setFilter}
|
||||
options={[
|
||||
{ id: 'all', label: t('systemManager.ports.filter.all') },
|
||||
{ id: 'tcp', label: 'TCP' },
|
||||
{ id: 'udp', label: 'UDP' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<SystemPanelMetaBar>
|
||||
{t('systemManager.ports.meta', { count: ports.length })}
|
||||
</SystemPanelMetaBar>
|
||||
|
||||
{actionError ? <SystemPanelInlineError message={actionError} /> : null}
|
||||
|
||||
{error && !(data?.length) ? (
|
||||
<SystemPanelError
|
||||
message={error}
|
||||
onRetry={() => void refresh()}
|
||||
retryLabel={t('history.action.retry')}
|
||||
loading={loading}
|
||||
/>
|
||||
) : !(data?.length) && (loading || listPending) ? (
|
||||
<SystemPanelLoading message={t('systemManager.ports.loading')} />
|
||||
) : !ports.length ? (
|
||||
<SystemPanelEmpty icon={Network} message={t('systemManager.ports.empty')} />
|
||||
) : (
|
||||
<SystemPanelList>
|
||||
{ports.map((port) => (
|
||||
<SystemPanelRow
|
||||
key={port.id}
|
||||
title={`${port.address}:${port.port}`}
|
||||
subtitle={
|
||||
port.processName
|
||||
? `${port.processName}${port.pid != null ? ` · PID ${port.pid}` : ''}`
|
||||
: (port.pid != null ? `PID ${port.pid}` : t('systemManager.ports.unknownProcess'))
|
||||
}
|
||||
trailing={(
|
||||
<SystemPanelStatusBadge tone="muted">
|
||||
{port.protocol.toUpperCase()}
|
||||
</SystemPanelStatusBadge>
|
||||
)}
|
||||
actions={allowMutations && port.pid != null ? (
|
||||
<SystemPanelRoundButton
|
||||
title={t('systemManager.ports.terminate')}
|
||||
destructive
|
||||
onClick={() => setPendingKillPid(port.pid)}
|
||||
>
|
||||
<Skull size={12} />
|
||||
</SystemPanelRoundButton>
|
||||
) : null}
|
||||
/>
|
||||
))}
|
||||
</SystemPanelList>
|
||||
)}
|
||||
|
||||
<SystemPanelConfirmDialog
|
||||
open={allowMutations && pendingKillPid != null}
|
||||
title={t('systemManager.ports.terminate')}
|
||||
message={t('systemManager.ports.confirmTerminate', { pid: String(pendingKillPid ?? 0) })}
|
||||
confirmLabel={t('systemManager.ports.terminate')}
|
||||
destructive
|
||||
busy={killBusy}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && !killBusy) setPendingKillPid(null);
|
||||
}}
|
||||
onConfirm={() => {
|
||||
const pid = pendingKillPid;
|
||||
if (pid == null) return;
|
||||
setPendingKillPid(null);
|
||||
void executeTerminate(pid);
|
||||
}}
|
||||
/>
|
||||
</SystemPanelShell>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user