mirror of
https://github.com/pompurin404/mihomo-party.git
synced 2024-11-16 03:32:17 +08:00
connections page
This commit is contained in:
parent
15dd29c0f8
commit
e4d4b54874
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 6px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Light mode */
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
2
src/shared/types.d.ts
vendored
2
src/shared/types.d.ts
vendored
|
@ -71,6 +71,8 @@ interface IMihomoConnectionDetail {
|
|||
dscp: number
|
||||
sniffHost: string
|
||||
}
|
||||
uploadSpeed?: number
|
||||
downloadSpeed?: number
|
||||
upload: number
|
||||
download: number
|
||||
start: string
|
||||
|
|
Loading…
Reference in New Issue
Block a user