refactor connections

This commit is contained in:
pompurin404 2024-08-30 23:08:07 +08:00
parent a5b7ed2378
commit c5c90caa1b
No known key found for this signature in database
8 changed files with 272 additions and 96 deletions

View File

@ -16,8 +16,8 @@ const SettingItem: React.FC<Props> = (props) => {
<> <>
<div className="h-[32px] w-full flex justify-between"> <div className="h-[32px] w-full flex justify-between">
<div className="h-full flex items-center"> <div className="h-full flex items-center">
<h4 className="h-full text-md leading-[32px]">{title}</h4> <h4 className="h-full text-md leading-[32px] whitespace-nowrap mr-2">{title}</h4>
<div>{actions}</div> <div className="mr-2">{actions}</div>
</div> </div>
{children} {children}
</div> </div>

View File

@ -1,12 +1,31 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react' import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
import React from 'react' import React from 'react'
import SettingItem from '../base/base-setting-item'
import { calcTraffic } from '@renderer/utils/calc'
import dayjs from 'dayjs'
interface Props { interface Props {
connection: IMihomoConnectionDetail connection: IMihomoConnectionDetail
onClose: () => void onClose: () => void
} }
// sourceIP: string
// destinationIP: string
// destinationGeoIP: string
// destinationIPASN: string
// sourcePort: string
// destinationPort: string
// inboundIP: string
// inboundPort: string
// inboundName: string
// inboundUser: string
// host: string
// dnsMode: string
// specialProxy: string
// specialRules: string
// remoteDestination: string
// dscp: number
// sniffHost: string
const ConnectionDetailModal: React.FC<Props> = (props) => { const ConnectionDetailModal: React.FC<Props> = (props) => {
const { connection, onClose } = props const { connection, onClose } = props
return ( return (
<Modal <Modal
backdrop="blur" backdrop="blur"
@ -16,12 +35,80 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
onOpenChange={onClose} onOpenChange={onClose}
scrollBehavior="inside" scrollBehavior="inside"
> >
<ModalContent> <ModalContent className="flag-emoji break-all">
<ModalHeader className="flex"></ModalHeader> <ModalHeader className="flex"></ModalHeader>
<ModalBody> <ModalBody>
<pre> <SettingItem title="连接类型">
<code className="select-text">{JSON.stringify(connection, null, 2)}</code> {connection.metadata.type}({connection.metadata.network})
</pre> </SettingItem>
<SettingItem title="连接建立时间">{dayjs(connection.start).fromNow()}</SettingItem>
<SettingItem title="规则">
{connection.rule}
{connection.rulePayload ? `(${connection.rulePayload})` : ''}
</SettingItem>
<SettingItem title="代理链">{[...connection.chains].reverse().join('>>')}</SettingItem>
<SettingItem title="上传速度">{calcTraffic(connection.uploadSpeed || 0)}/s</SettingItem>
<SettingItem title="下载速度">{calcTraffic(connection.downloadSpeed || 0)}/s</SettingItem>
<SettingItem title="上传量">{calcTraffic(connection.upload)}</SettingItem>
<SettingItem title="下载量">{calcTraffic(connection.download)}</SettingItem>
{connection.metadata.process && (
<SettingItem title="进程名">
{connection.metadata.process}
{connection.metadata.uid ? `(${connection.metadata.uid})` : ''}
</SettingItem>
)}
{connection.metadata.processPath && (
<SettingItem title="进程路径">{connection.metadata.processPath}</SettingItem>
)}
{connection.metadata.sourceIP && (
<SettingItem title="源IP">{connection.metadata.sourceIP}</SettingItem>
)}
{connection.metadata.destinationIP && (
<SettingItem title="目标IP">{connection.metadata.destinationIP}</SettingItem>
)}
{connection.metadata.destinationGeoIP && (
<SettingItem title="目标GeoIP">{connection.metadata.destinationGeoIP}</SettingItem>
)}
{connection.metadata.destinationIPASN && (
<SettingItem title="目标ASN">{connection.metadata.destinationIPASN}</SettingItem>
)}
{connection.metadata.sourcePort && (
<SettingItem title="源端口">{connection.metadata.sourcePort}</SettingItem>
)}
{connection.metadata.destinationPort && (
<SettingItem title="目标端口">{connection.metadata.destinationPort}</SettingItem>
)}
{connection.metadata.inboundIP && (
<SettingItem title="入站IP">{connection.metadata.inboundIP}</SettingItem>
)}
{connection.metadata.inboundPort && (
<SettingItem title="入站端口">{connection.metadata.inboundPort}</SettingItem>
)}
{connection.metadata.inboundName && (
<SettingItem title="入站名称">{connection.metadata.inboundName}</SettingItem>
)}
{connection.metadata.inboundUser && (
<SettingItem title="入站用户">{connection.metadata.inboundUser}</SettingItem>
)}
{connection.metadata.host && (
<SettingItem title="主机">{connection.metadata.host}</SettingItem>
)}
{connection.metadata.dnsMode && (
<SettingItem title="DNS模式">{connection.metadata.dnsMode}</SettingItem>
)}
{connection.metadata.specialProxy && (
<SettingItem title="特殊代理">{connection.metadata.specialProxy}</SettingItem>
)}
{connection.metadata.specialRules && (
<SettingItem title="特殊规则">{connection.metadata.specialRules}</SettingItem>
)}
{connection.metadata.remoteDestination && (
<SettingItem title="远程目标">{connection.metadata.remoteDestination}</SettingItem>
)}
<SettingItem title="DSCP">{connection.metadata.dscp}</SettingItem>
{connection.metadata.sniffHost && (
<SettingItem title="嗅探主机">{connection.metadata.sniffHost}</SettingItem>
)}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant="light" onPress={onClose}> <Button variant="light" onPress={onClose}>

View File

@ -0,0 +1,89 @@
import { Button, Card, CardFooter, CardHeader, Chip } from '@nextui-org/react'
import { calcTraffic } from '@renderer/utils/calc'
import dayjs from 'dayjs'
import React, { useState } from 'react'
import { CgClose } from 'react-icons/cg'
import ConnectionDetailModal from './connection-detail-modal'
interface Props {
index: number
info: IMihomoConnectionDetail
close: (id: string) => Promise<void>
}
const ConnectionItem: React.FC<Props> = (props) => {
const { index, info } = props
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false)
return (
<div className={`px-2 pb-2 ${index === 0 ? 'pt-2' : ''}`}>
{isDetailModalOpen && (
<ConnectionDetailModal onClose={() => setIsDetailModalOpen(false)} connection={info} />
)}
<Card
isPressable
className="w-full"
onPress={() => {
setIsDetailModalOpen(true)
}}
>
<div className="w-full flex justify-between">
<div className="w-[calc(100%-48px)]">
<CardHeader className="pb-0 gap-1">
<Chip color="primary" size="sm" radius="sm" variant="light">
{info.metadata.type}({info.metadata.network.toUpperCase()})
</Chip>
<div className="text-ellipsis whitespace-nowrap overflow-hidden">
{info.metadata.process || info.metadata.sourceIP}
{' -> '}
{info.metadata.host ||
info.metadata.sniffHost ||
info.metadata.remoteDestination ||
info.metadata.destinationIP}
</div>
<small className="whitespace-nowrap text-default-500">
{dayjs(info.start).fromNow()}
</small>
</CardHeader>
<CardFooter
onWheel={(e) => {
e.currentTarget.scrollLeft += e.deltaY
}}
className="overscroll-contain pt-1 flex justify-start gap-1 overflow-x-auto no-scrollbar"
>
<Chip
className="flag-emoji text-ellipsis whitespace-nowrap overflow-hidden"
size="sm"
radius="sm"
variant="bordered"
>
{info.chains[0]}
</Chip>
<Chip size="sm" radius="sm" variant="bordered">
{calcTraffic(info.upload)} {calcTraffic(info.download)}
</Chip>
{info.uploadSpeed !== 0 || info.downloadSpeed !== 0 ? (
<Chip color="primary" size="sm" radius="sm" variant="bordered">
{calcTraffic(info.uploadSpeed || 0)}/s {calcTraffic(info.downloadSpeed || 0)}
/s
</Chip>
) : null}
</CardFooter>
</div>
<Button
color="warning"
variant="light"
isIconOnly
className="mr-2 my-auto"
onPress={() => {
props.close(info.id)
}}
>
<CgClose className="text-lg" />
</Button>
</div>
</Card>
</div>
)
}
export default ConnectionItem

View File

@ -5,12 +5,12 @@ import {
startMihomoConnections, startMihomoConnections,
stopMihomoConnections stopMihomoConnections
} from '@renderer/utils/ipc' } from '@renderer/utils/ipc'
import { Key, useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { Button, Divider, Input } from '@nextui-org/react' import { Button, Divider, Input, Select, SelectItem } from '@nextui-org/react'
import { IoCloseCircle } from 'react-icons/io5'
import { calcTraffic } from '@renderer/utils/calc' import { calcTraffic } from '@renderer/utils/calc'
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@nextui-org/react' import ConnectionItem from '@renderer/components/connections/connection-item'
import ConnectionDetailModal from '@renderer/components/connections/connection-detail-modal' import { Virtuoso } from 'react-virtuoso'
import dayjs from 'dayjs'
let preData: IMihomoConnectionDetail[] = [] let preData: IMihomoConnectionDetail[] = []
@ -18,10 +18,42 @@ const Connections: React.FC = () => {
const [filter, setFilter] = useState('') const [filter, setFilter] = useState('')
const [connectionsInfo, setConnectionsInfo] = useState<IMihomoConnectionsInfo>() const [connectionsInfo, setConnectionsInfo] = useState<IMihomoConnectionsInfo>()
const [connections, setConnections] = useState<IMihomoConnectionDetail[]>([]) const [connections, setConnections] = useState<IMihomoConnectionDetail[]>([])
const [selectedConnection, setSelectedConnection] = useState<IMihomoConnectionDetail>() const [direction, setDirection] = useState(true)
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false) const [sortBy, setSortBy] = useState('time')
const filteredConnections = useMemo(() => { const filteredConnections = useMemo(() => {
if (sortBy) {
connections.sort((a, b) => {
if (direction) {
switch (sortBy) {
case 'time':
return dayjs(b.start).unix() - dayjs(a.start).unix()
case 'upload':
return a.upload - b.upload
case 'download':
return a.download - b.download
case 'uploadSpeed':
return (a.uploadSpeed || 0) - (b.uploadSpeed || 0)
case 'downloadSpeed':
return (a.downloadSpeed || 0) - (b.downloadSpeed || 0)
}
return 0
} else {
switch (sortBy) {
case 'time':
return dayjs(a.start).unix() - dayjs(b.start).unix()
case 'upload':
return b.upload - a.upload
case 'download':
return b.download - a.download
case 'uploadSpeed':
return (b.uploadSpeed || 0) - (a.uploadSpeed || 0)
case 'downloadSpeed':
return (b.downloadSpeed || 0) - (a.downloadSpeed || 0)
}
return 0
}
})
}
if (filter === '') return connections if (filter === '') return connections
return connections?.filter((connection) => { return connections?.filter((connection) => {
const raw = JSON.stringify(connection) const raw = JSON.stringify(connection)
@ -70,99 +102,70 @@ const Connections: React.FC = () => {
className="ml-1" className="ml-1"
size="sm" size="sm"
color="primary" color="primary"
onPress={() => mihomoCloseAllConnections()} onPress={() => {
if (filter === '') {
mihomoCloseAllConnections()
} else {
filteredConnections.forEach((conn) => {
mihomoCloseConnection(conn.id)
})
}
}}
> >
({filteredConnections.length})
</Button> </Button>
</div> </div>
} }
> >
{isDetailModalOpen && selectedConnection && (
<ConnectionDetailModal
onClose={() => setIsDetailModalOpen(false)}
connection={selectedConnection}
/>
)}
<div className="overflow-x-auto sticky top-[49px] z-40"> <div className="overflow-x-auto sticky top-[49px] z-40">
<div className="flex p-2"> <div className="flex p-2 gap-2">
<Input <Input
variant="bordered" variant="flat"
size="sm" size="sm"
value={filter} value={filter}
placeholder="筛选过滤" placeholder="筛选过滤"
isClearable isClearable
onValueChange={setFilter} onValueChange={setFilter}
/> />
<Select
size="sm"
className="w-[180px]"
selectedKeys={new Set([sortBy])}
onSelectionChange={async (v) => {
setSortBy(v.currentKey as string)
}}
>
<SelectItem key="upload"></SelectItem>
<SelectItem key="download"></SelectItem>
<SelectItem key="uploadSpeed"></SelectItem>
<SelectItem key="downloadSpeed"></SelectItem>
<SelectItem key="time"></SelectItem>
</Select>
<Button
size="sm"
onPress={() => {
setDirection((pre) => !pre)
}}
>
{direction ? '升序' : '降序'}
</Button>
</div> </div>
<Divider /> <Divider />
</div> </div>
<Table <div className="h-[calc(100vh-100px)] mt-[1px]">
onRowAction={(id: Key) => { <Virtuoso
setSelectedConnection(connections.find((c) => c.id === (id as string))) data={filteredConnections}
setIsDetailModalOpen(true) itemContent={(i, connection) => (
}} <ConnectionItem
isHeaderSticky close={mihomoCloseConnection}
isStriped index={i}
className="h-[calc(100vh-100px)] p-2" key={connection.id}
> info={connection}
<TableHeader> />
<TableColumn key="type"></TableColumn>
<TableColumn key="origin"></TableColumn>
<TableColumn key="target"></TableColumn>
<TableColumn key="rule"></TableColumn>
<TableColumn key="chains"></TableColumn>
<TableColumn key="close"></TableColumn>
</TableHeader>
<TableBody items={filteredConnections ?? []}>
{(item) => (
<TableRow key={item.id}>
<TableCell>
{item.metadata.type}({item.metadata.network})
</TableCell>
<TableCell>{item.metadata.process || item.metadata.sourceIP}</TableCell>
<TableCell className="max-w-[100px] text-ellipsis whitespace-nowrap overflow-hidden">
{item.metadata.host ||
item.metadata.sniffHost ||
item.metadata.remoteDestination ||
item.metadata.destinationIP}
</TableCell>
<TableCell className="max-w-[100px] text-ellipsis whitespace-nowrap overflow-hidden">
{item.rule} {item.rulePayload}
</TableCell>
<TableCell className="flag-emoji max-w-[200px] text-ellipsis whitespace-nowrap overflow-hidden">
{item.chains.reverse().join('::')}
</TableCell>
<TableCell>
<Button
isIconOnly
size="sm"
color="danger"
variant="light"
onPress={() => mihomoCloseConnection(item.id)}
>
<IoCloseCircle className="text-lg" />
</Button>
</TableCell>
</TableRow>
)} )}
</TableBody> />
</Table> </div>
{/* {filteredConnections?.map((connection) => {
return (
<ConnectionItem
mutate={mutate}
key={connection.id}
id={connection.id}
chains={connection.chains}
download={connection.download}
upload={connection.upload}
metadata={connection.metadata}
rule={connection.rule}
rulePayload={connection.rulePayload}
start={connection.start}
/>
)
})} */}
</BasePage> </BasePage>
) )
} }

View File

@ -48,7 +48,6 @@ const Logs: React.FC = () => {
<div className="sticky top-[49px] z-40"> <div className="sticky top-[49px] z-40">
<div className="w-full flex p-2"> <div className="w-full flex p-2">
<Input <Input
variant="bordered"
size="sm" size="sm"
value={filter} value={filter}
placeholder="筛选过滤" placeholder="筛选过滤"

View File

@ -136,7 +136,6 @@ const Override: React.FC = () => {
<div className="sticky top-[49px] z-40 backdrop-blur bg-background/40"> <div className="sticky top-[49px] z-40 backdrop-blur bg-background/40">
<div className="flex p-2"> <div className="flex p-2">
<Input <Input
variant="bordered"
size="sm" size="sm"
value={url} value={url}
onValueChange={setUrl} onValueChange={setUrl}

View File

@ -126,7 +126,7 @@ const Profiles: React.FC = () => {
<div className="sticky top-[49px] z-40 backdrop-blur bg-background/40"> <div className="sticky top-[49px] z-40 backdrop-blur bg-background/40">
<div className="flex p-2"> <div className="flex p-2">
<Input <Input
variant="bordered" // variant="bordered"
size="sm" size="sm"
value={url} value={url}
onValueChange={setUrl} onValueChange={setUrl}

View File

@ -22,10 +22,9 @@ const Rules: React.FC = () => {
return ( return (
<BasePage title="分流规则"> <BasePage title="分流规则">
<div className="sticky top-[50px] z-40"> <div className="sticky top-[49px] z-40">
<div className="flex p-2"> <div className="flex p-2">
<Input <Input
variant="bordered"
size="sm" size="sm"
value={filter} value={filter}
placeholder="筛选过滤" placeholder="筛选过滤"