connections page

This commit is contained in:
pompurin404 2024-08-06 15:30:24 +08:00
parent 15dd29c0f8
commit e4d4b54874
No known key found for this signature in database
8 changed files with 267 additions and 75 deletions

View File

@ -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<AxiosInstance> => {
if (axiosIns && !force) return axiosIns
@ -46,13 +47,6 @@ export const patchMihomoConfig = async (patch: Partial<IMihomoConfig>): Promise<
})) as Promise<void>
}
export const mihomoConnections = async (): Promise<IMihomoConnectionsInfo> => {
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<void> => {
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
}
}
}

View File

@ -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)

View File

@ -8,7 +8,11 @@
*::-webkit-scrollbar {
width: 8px;
height: 6px;
height: 8px;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
/* Light mode */

View File

@ -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> = (props) => {
const { connection, onClose } = props
return (
<Modal size="xl" hideCloseButton isOpen={true} scrollBehavior="inside">
<ModalContent>
<ModalHeader className="flex"></ModalHeader>
<ModalBody>
<pre>
<code>{JSON.stringify(connection, null, 2)}</code>
</pre>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onClose}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default ConnectionDetailModal

View File

@ -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> = (props) => {
const { id, metadata, mutate } = props
return (
<Card className="m-2">
<CardBody className="">
<div className="flex justify-between">
<div className="select-none h-[32px] leading-[32px]">
{metadata.type}({metadata.network})
</div>
<div className="select-none h-[32px] leading-[32px]">{metadata.inboundIP}</div>
<div className="select-none h-[32px] leading-[32px]">{'-->'}</div>
<div className="select-none h-[32px] leading-[32px]">{metadata.remoteDestination}</div>
<Button
isIconOnly
size="sm"
color="danger"
variant="light"
onPress={() => {
mihomoCloseConnection(id).then(() => {
mutate()
})
}}
>
<IoClose className="text-[24px]" />
</Button>
</div>
</CardBody>
</Card>
)
}
export default ConnectionItem

View File

@ -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<IMihomoConnectionsInfo>('mihomoConnections', mihomoConnections, {
refreshInterval: 1000
})
const [filter, setFilter] = useState('')
const [connectionsInfo, setConnectionsInfo] = useState<IMihomoConnectionsInfo>()
const [connections, setConnections] = useState<IMihomoConnectionDetail[]>([])
const [selectedConnection, setSelectedConnection] = useState<IMihomoConnectionDetail>()
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 (
<BasePage
title="连接"
@ -27,28 +82,30 @@ const Connections: React.FC = () => {
<div className="flex">
<div className="flex items-center">
<span className="mx-1 text-gray-400">
: {calcTraffic(connections.downloadTotal)}{' '}
: {calcTraffic(connectionsInfo?.downloadTotal ?? 0)}{' '}
</span>
<span className="mx-1 text-gray-400">
: {calcTraffic(connections.uploadTotal)}{' '}
: {calcTraffic(connectionsInfo?.uploadTotal ?? 0)}{' '}
</span>
</div>
<Button
className="ml-1"
size="sm"
color="primary"
onPress={() =>
mihomoCloseAllConnections().then(() => {
mutate()
})
}
onPress={() => mihomoCloseAllConnections()}
>
</Button>
</div>
}
>
<div className="sticky top-[48px] z-40 backdrop-blur bg-background/40 flex p-2">
{isDetailModalOpen && selectedConnection && (
<ConnectionDetailModal
onClose={() => setIsDetailModalOpen(false)}
connection={selectedConnection}
/>
)}
<div className="overflow-x-auto sticky top-[49px] z-40 backdrop-blur bg-background/40 flex p-2">
<Input
variant="bordered"
size="sm"
@ -57,7 +114,106 @@ const Connections: React.FC = () => {
onValueChange={setFilter}
/>
</div>
{filteredConnections?.map((connection) => {
<Table
onRowAction={(id: Key) => {
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"
>
<TableHeader>
<TableColumn key="type" allowsSorting>
</TableColumn>
<TableColumn key="process" allowsSorting>
</TableColumn>
<TableColumn key="host" allowsSorting>
</TableColumn>
<TableColumn key="sniffer" allowsSorting>
</TableColumn>
<TableColumn key="rule" allowsSorting>
</TableColumn>
<TableColumn key="chains" width={500}>
</TableColumn>
<TableColumn key="download" allowsSorting>
</TableColumn>
<TableColumn key="upload" allowsSorting>
</TableColumn>
<TableColumn key="downloadSpeed" allowsSorting>
</TableColumn>
<TableColumn key="uploadSpeed" allowsSorting>
</TableColumn>
<TableColumn key="start" allowsSorting>
</TableColumn>
<TableColumn key="sourceIp"></TableColumn>
<TableColumn key="sourcePort"></TableColumn>
<TableColumn key="inboundUser"></TableColumn>
<TableColumn key="close"></TableColumn>
</TableHeader>
<TableBody items={sortedConnections ?? []}>
{(item) => (
<TableRow key={item.id}>
<TableCell className="max-w-[10px]">
{item.metadata.type}({item.metadata.network})
</TableCell>
<TableCell>{item.metadata.process}</TableCell>
<TableCell className="max-w-[200px] text-ellipsis whitespace-nowrap overflow-hidden">
{item.metadata.host}
</TableCell>
<TableCell className="max-w-[200px] text-ellipsis whitespace-nowrap overflow-hidden">
{item.metadata.sniffHost ?? '-'}
</TableCell>
<TableCell className="max-w-[200px] text-ellipsis whitespace-nowrap overflow-hidden">
{item.rule}:{item.rulePayload}
</TableCell>
<TableCell className="max-w-[200px] text-ellipsis whitespace-nowrap overflow-hidden">
{item.chains.reverse().join('::')}
</TableCell>
<TableCell className="whitespace-nowrap">{calcTraffic(item.download)}</TableCell>
<TableCell className="whitespace-nowrap">{calcTraffic(item.upload)}</TableCell>
<TableCell className="whitespace-nowrap">
{calcTraffic(item.downloadSpeed ?? 0)}/s
</TableCell>
<TableCell className="whitespace-nowrap">
{calcTraffic(item.uploadSpeed ?? 0)}/s
</TableCell>
<TableCell className="whitespace-nowrap">{dayjs(item.start).fromNow()}</TableCell>
<TableCell className="whitespace-nowrap">{item.metadata.sourceIP}</TableCell>
<TableCell className="whitespace-nowrap">{item.metadata.sourcePort}</TableCell>
<TableCell className="whitespace-nowrap">{item.metadata.inboundUser}</TableCell>
<TableCell>
<Button
isIconOnly
size="sm"
color="danger"
variant="light"
onPress={() => mihomoCloseConnection(item.id)}
>
<IoCloseCircle className="text-lg" />
</Button>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* {filteredConnections?.map((connection) => {
return (
<ConnectionItem
mutate={mutate}
@ -72,7 +228,7 @@ const Connections: React.FC = () => {
start={connection.start}
/>
)
})}
})} */}
</BasePage>
)
}

View File

@ -6,10 +6,6 @@ export async function mihomoConfig(): Promise<IMihomoConfig> {
return await window.electron.ipcRenderer.invoke('mihomoConfig')
}
export async function mihomoConnections(): Promise<IMihomoConnectionsInfo> {
return await window.electron.ipcRenderer.invoke('mihomoConnections')
}
export async function mihomoCloseConnection(id: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('mihomoCloseConnection', id)
}
@ -43,6 +39,14 @@ export async function stopMihomoLogs(): Promise<void> {
return await window.electron.ipcRenderer.invoke('stopMihomoLogs')
}
export async function startMihomoConnections(): Promise<void> {
return await window.electron.ipcRenderer.invoke('startMihomoConnections')
}
export async function stopMihomoConnections(): Promise<void> {
return await window.electron.ipcRenderer.invoke('stopMihomoConnections')
}
export async function patchMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
return await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch)
}

View File

@ -71,6 +71,8 @@ interface IMihomoConnectionDetail {
dscp: number
sniffHost: string
}
uploadSpeed?: number
downloadSpeed?: number
upload: number
download: number
start: string