From e4d4b54874d52a8f8fd9fb9d9050f36016ae4013 Mon Sep 17 00:00:00 2001 From: pompurin404 Date: Tue, 6 Aug 2024 15:30:24 +0800 Subject: [PATCH] connections page --- src/main/core/mihomoApi.ts | 49 ++++- src/main/utils/ipc.ts | 6 +- src/renderer/src/assets/main.css | 6 +- .../connections/connection-detail-modal.tsx | 29 +++ .../connections/connection-item.tsx | 40 ---- src/renderer/src/pages/connections.tsx | 198 ++++++++++++++++-- src/renderer/src/utils/ipc.ts | 12 +- src/shared/types.d.ts | 2 + 8 files changed, 267 insertions(+), 75 deletions(-) create mode 100644 src/renderer/src/components/connections/connection-detail-modal.tsx delete mode 100644 src/renderer/src/components/connections/connection-item.tsx diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index 920beb4..23c18aa 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -7,6 +7,7 @@ let axiosIns: AxiosInstance = null! let mihomoTrafficWs: WebSocket | null = null let mihomoMemoryWs: WebSocket | null = null let mihomoLogsWs: WebSocket | null = null +let mihomoConnectionsWs: WebSocket | null = null export const getAxios = async (force: boolean = false): Promise => { if (axiosIns && !force) return axiosIns @@ -46,13 +47,6 @@ export const patchMihomoConfig = async (patch: Partial): Promise< })) as Promise } -export const mihomoConnections = async (): Promise => { - const instance = await getAxios() - return (await instance.get('/connections').catch(() => { - return { downloadTotal: 0, uploadTotal: 0, connections: [], memory: 0 } - })) as IMihomoConnectionsInfo -} - export const mihomoCloseConnection = async (id: string): Promise => { const instance = await getAxios() return (await instance.delete(`/connections/${encodeURIComponent(id)}`).catch((e) => { @@ -230,3 +224,44 @@ const mihomoLogs = (): void => { } } } + +export const startMihomoConnections = (): void => { + mihomoConnections() +} + +export const stopMihomoConnections = (): void => { + if (mihomoConnectionsWs) { + mihomoConnectionsWs.removeAllListeners() + if (mihomoConnectionsWs.readyState === WebSocket.OPEN) { + mihomoConnectionsWs.close() + } + mihomoConnectionsWs = null + } +} + +const mihomoConnections = (): void => { + let server = getControledMihomoConfig()['external-controller'] + const secret = getControledMihomoConfig().secret ?? '' + if (server?.startsWith(':')) server = `127.0.0.1${server}` + stopMihomoConnections() + + mihomoConnectionsWs = new WebSocket( + `ws://${server}/connections?token=${encodeURIComponent(secret)}` + ) + + mihomoConnectionsWs.onmessage = (e): void => { + const data = e.data as string + window?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo) + } + + mihomoConnectionsWs.onclose = (): void => { + mihomoConnections() + } + + mihomoConnectionsWs.onerror = (): void => { + if (mihomoConnectionsWs) { + mihomoConnectionsWs.close() + mihomoConnectionsWs = null + } + } +} diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 914cb67..1af933c 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -4,13 +4,14 @@ import { mihomoCloseAllConnections, mihomoCloseConnection, mihomoConfig, - mihomoConnections, mihomoProxies, mihomoProxyDelay, mihomoRules, mihomoVersion, patchMihomoConfig, + startMihomoConnections, startMihomoLogs, + stopMihomoConnections, stopMihomoLogs } from '../core/mihomoApi' import { checkAutoRun, disableAutoRun, enableAutoRun } from '../resolve/autoRun' @@ -36,7 +37,6 @@ import { checkUpdate } from '../resolve/autoUpdater' export function registerIpcMainHandlers(): void { ipcMain.handle('mihomoVersion', mihomoVersion) ipcMain.handle('mihomoConfig', mihomoConfig) - ipcMain.handle('mihomoConnections', mihomoConnections) ipcMain.handle('mihomoCloseConnection', (_e, id) => mihomoCloseConnection(id)) ipcMain.handle('mihomoCloseAllConnections', mihomoCloseAllConnections) ipcMain.handle('mihomoRules', mihomoRules) @@ -45,6 +45,8 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('mihomoProxyDelay', (_e, proxy, url) => mihomoProxyDelay(proxy, url)) ipcMain.handle('startMihomoLogs', startMihomoLogs) ipcMain.handle('stopMihomoLogs', stopMihomoLogs) + ipcMain.handle('startMihomoConnections', () => startMihomoConnections()) + ipcMain.handle('stopMihomoConnections', () => stopMihomoConnections()) ipcMain.handle('patchMihomoConfig', (_e, patch) => patchMihomoConfig(patch)) ipcMain.handle('checkAutoRun', checkAutoRun) ipcMain.handle('enableAutoRun', enableAutoRun) diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 59d4ad9..f985ad0 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -8,7 +8,11 @@ *::-webkit-scrollbar { width: 8px; - height: 6px; + height: 8px; +} + +*::-webkit-scrollbar-corner { + background-color: transparent; } /* Light mode */ diff --git a/src/renderer/src/components/connections/connection-detail-modal.tsx b/src/renderer/src/components/connections/connection-detail-modal.tsx new file mode 100644 index 0000000..cef7202 --- /dev/null +++ b/src/renderer/src/components/connections/connection-detail-modal.tsx @@ -0,0 +1,29 @@ +import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react' +import React from 'react' +interface Props { + connection: IMihomoConnectionDetail + onClose: () => void +} +const ConnectionDetailModal: React.FC = (props) => { + const { connection, onClose } = props + + return ( + + + 连接详情 + +
+            {JSON.stringify(connection, null, 2)}
+          
+
+ + + +
+
+ ) +} + +export default ConnectionDetailModal diff --git a/src/renderer/src/components/connections/connection-item.tsx b/src/renderer/src/components/connections/connection-item.tsx deleted file mode 100644 index 651cac3..0000000 --- a/src/renderer/src/components/connections/connection-item.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Button, Card, CardBody } from '@nextui-org/react' -import { mihomoCloseConnection } from '@renderer/utils/ipc' -import { IoClose } from 'react-icons/io5' -import React from 'react' - -interface Props extends IMihomoConnectionDetail { - mutate: () => void -} -const ConnectionItem: React.FC = (props) => { - const { id, metadata, mutate } = props - return ( - - -
-
- {metadata.type}({metadata.network}) -
-
{metadata.inboundIP}
-
{'-->'}
-
{metadata.remoteDestination}
- -
-
-
- ) -} - -export default ConnectionItem diff --git a/src/renderer/src/pages/connections.tsx b/src/renderer/src/pages/connections.tsx index 1237155..e745f6c 100644 --- a/src/renderer/src/pages/connections.tsx +++ b/src/renderer/src/pages/connections.tsx @@ -1,25 +1,80 @@ -import ConnectionItem from '@renderer/components/connections/connection-item' import BasePage from '@renderer/components/base/base-page' -import { mihomoCloseAllConnections, mihomoConnections } from '@renderer/utils/ipc' -import { useMemo, useState } from 'react' +import { + mihomoCloseAllConnections, + mihomoCloseConnection, + startMihomoConnections, + stopMihomoConnections +} from '@renderer/utils/ipc' +import { Key, useEffect, useMemo, useState } from 'react' import { Button, Input } from '@nextui-org/react' -import useSWR from 'swr' +import { IoCloseCircle } from 'react-icons/io5' import { calcTraffic } from '@renderer/utils/calc' +import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@nextui-org/react' +import dayjs from 'dayjs' +import ConnectionDetailModal from '@renderer/components/connections/connection-detail-modal' + +let preData: IMihomoConnectionDetail[] = [] const Connections: React.FC = () => { - const { data: connections = { downloadTotal: 0, uploadTotal: 0, connections: [] }, mutate } = - useSWR('mihomoConnections', mihomoConnections, { - refreshInterval: 1000 - }) const [filter, setFilter] = useState('') + const [connectionsInfo, setConnectionsInfo] = useState() + const [connections, setConnections] = useState([]) + const [selectedConnection, setSelectedConnection] = useState() + const [isDetailModalOpen, setIsDetailModalOpen] = useState(false) + const [sortKey, setSortKey] = useState('') + const [descend, setDescend] = useState(false) const filteredConnections = useMemo(() => { - if (filter === '') return connections.connections - return connections.connections?.filter((connection) => { - return connection.metadata.remoteDestination.includes(filter) + if (filter === '') return connections + return connections?.filter((connection) => { + const raw = JSON.stringify(connection) + return raw.includes(filter) }) }, [connections, filter]) + const sortedConnections = useMemo(() => { + if (sortKey === '') return filteredConnections + return filteredConnections.sort((a, b) => { + const localA = a[sortKey] ? a[sortKey] : a.metadata[sortKey] + const localB = b[sortKey] ? b[sortKey] : b.metadata[sortKey] + if (descend) { + if (typeof localA === 'string') { + return localB.localeCompare(localA) + } + return localB - localA + } else { + if (typeof localA === 'string') { + return localA.localeCompare(localB) + } + return localA - localB + } + }) + }, [filteredConnections, sortKey, descend]) + + useEffect(() => { + startMihomoConnections() + window.electron.ipcRenderer.on('mihomoConnections', (_e, info: IMihomoConnectionsInfo) => { + setConnectionsInfo(info) + const newConns: IMihomoConnectionDetail[] = [] + for (const conn of info.connections ?? []) { + const preConn = preData?.find((c) => c.id === conn.id) + + if (preConn) { + conn.downloadSpeed = conn.download - preConn.download + conn.uploadSpeed = conn.upload - preConn.upload + } + newConns.push(conn) + } + setConnections(newConns) + preData = newConns + }) + + return (): void => { + stopMihomoConnections() + window.electron.ipcRenderer.removeAllListeners('mihomoConnections') + } + }, []) + return ( {
- 下载: {calcTraffic(connections.downloadTotal)}{' '} + 下载: {calcTraffic(connectionsInfo?.downloadTotal ?? 0)}{' '} - 上传: {calcTraffic(connections.uploadTotal)}{' '} + 上传: {calcTraffic(connectionsInfo?.uploadTotal ?? 0)}{' '}
} > -
+ {isDetailModalOpen && selectedConnection && ( + setIsDetailModalOpen(false)} + connection={selectedConnection} + /> + )} +
{ onValueChange={setFilter} />
- {filteredConnections?.map((connection) => { + { + setSelectedConnection(connections.find((c) => c.id === (id as string))) + setIsDetailModalOpen(true) + }} + sortDescriptor={{ column: sortKey, direction: descend ? 'descending' : 'ascending' }} + onSortChange={(desc) => { + setSortKey(desc.column as string) + setDescend(desc.direction !== 'ascending') + }} + isHeaderSticky + isStriped + className="h-[calc(100vh-100px)] p-2" + > + + + 类型 + + + 进程 + + + 主机 + + + 嗅探域名 + + + 规则 + + + 链路 + + + 下载量 + + + 上传量 + + + 下载速度 + + + 上传速度 + + + 连接时间 + + 源地址 + 源端口 + 入站用户 + 关闭连接 + + + {(item) => ( + + + {item.metadata.type}({item.metadata.network}) + + {item.metadata.process} + + {item.metadata.host} + + + {item.metadata.sniffHost ?? '-'} + + + {item.rule}:{item.rulePayload} + + + {item.chains.reverse().join('::')} + + {calcTraffic(item.download)} + {calcTraffic(item.upload)} + + {calcTraffic(item.downloadSpeed ?? 0)}/s + + + {calcTraffic(item.uploadSpeed ?? 0)}/s + + {dayjs(item.start).fromNow()} + {item.metadata.sourceIP} + {item.metadata.sourcePort} + {item.metadata.inboundUser} + + + + + )} + +
+ {/* {filteredConnections?.map((connection) => { return ( { start={connection.start} /> ) - })} + })} */} ) } diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index b037cf0..f509b7f 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -6,10 +6,6 @@ export async function mihomoConfig(): Promise { return await window.electron.ipcRenderer.invoke('mihomoConfig') } -export async function mihomoConnections(): Promise { - return await window.electron.ipcRenderer.invoke('mihomoConnections') -} - export async function mihomoCloseConnection(id: string): Promise { return await window.electron.ipcRenderer.invoke('mihomoCloseConnection', id) } @@ -43,6 +39,14 @@ export async function stopMihomoLogs(): Promise { return await window.electron.ipcRenderer.invoke('stopMihomoLogs') } +export async function startMihomoConnections(): Promise { + return await window.electron.ipcRenderer.invoke('startMihomoConnections') +} + +export async function stopMihomoConnections(): Promise { + return await window.electron.ipcRenderer.invoke('stopMihomoConnections') +} + export async function patchMihomoConfig(patch: Partial): Promise { return await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch) } diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index 97ad049..a0db8e2 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -71,6 +71,8 @@ interface IMihomoConnectionDetail { dscp: number sniffHost: string } + uploadSpeed?: number + downloadSpeed?: number upload: number download: number start: string