mirror of
https://github.com/pompurin404/mihomo-party.git
synced 2024-11-16 03:32:17 +08:00
refactor connections
This commit is contained in:
parent
a5b7ed2378
commit
c5c90caa1b
|
@ -16,8 +16,8 @@ const SettingItem: React.FC<Props> = (props) => {
|
|||
<>
|
||||
<div className="h-[32px] w-full flex justify-between">
|
||||
<div className="h-full flex items-center">
|
||||
<h4 className="h-full text-md leading-[32px]">{title}</h4>
|
||||
<div>{actions}</div>
|
||||
<h4 className="h-full text-md leading-[32px] whitespace-nowrap mr-2">{title}</h4>
|
||||
<div className="mr-2">{actions}</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,31 @@
|
|||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
|
||||
import React from 'react'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
import { calcTraffic } from '@renderer/utils/calc'
|
||||
import dayjs from 'dayjs'
|
||||
interface Props {
|
||||
connection: IMihomoConnectionDetail
|
||||
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 { connection, onClose } = props
|
||||
|
||||
return (
|
||||
<Modal
|
||||
backdrop="blur"
|
||||
|
@ -16,12 +35,80 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
|
|||
onOpenChange={onClose}
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalContent className="flag-emoji break-all">
|
||||
<ModalHeader className="flex">连接详情</ModalHeader>
|
||||
<ModalBody>
|
||||
<pre>
|
||||
<code className="select-text">{JSON.stringify(connection, null, 2)}</code>
|
||||
</pre>
|
||||
<SettingItem title="连接类型">
|
||||
{connection.metadata.type}({connection.metadata.network})
|
||||
</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>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
|
|
89
src/renderer/src/components/connections/connection-item.tsx
Normal file
89
src/renderer/src/components/connections/connection-item.tsx
Normal 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
|
|
@ -5,12 +5,12 @@ import {
|
|||
startMihomoConnections,
|
||||
stopMihomoConnections
|
||||
} from '@renderer/utils/ipc'
|
||||
import { Key, useEffect, useMemo, useState } from 'react'
|
||||
import { Button, Divider, Input } from '@nextui-org/react'
|
||||
import { IoCloseCircle } from 'react-icons/io5'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Button, Divider, Input, Select, SelectItem } from '@nextui-org/react'
|
||||
import { calcTraffic } from '@renderer/utils/calc'
|
||||
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@nextui-org/react'
|
||||
import ConnectionDetailModal from '@renderer/components/connections/connection-detail-modal'
|
||||
import ConnectionItem from '@renderer/components/connections/connection-item'
|
||||
import { Virtuoso } from 'react-virtuoso'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
let preData: IMihomoConnectionDetail[] = []
|
||||
|
||||
|
@ -18,10 +18,42 @@ const Connections: React.FC = () => {
|
|||
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 [direction, setDirection] = useState(true)
|
||||
const [sortBy, setSortBy] = useState('time')
|
||||
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
|
||||
return connections?.filter((connection) => {
|
||||
const raw = JSON.stringify(connection)
|
||||
|
@ -70,99 +102,70 @@ const Connections: React.FC = () => {
|
|||
className="ml-1"
|
||||
size="sm"
|
||||
color="primary"
|
||||
onPress={() => mihomoCloseAllConnections()}
|
||||
onPress={() => {
|
||||
if (filter === '') {
|
||||
mihomoCloseAllConnections()
|
||||
} else {
|
||||
filteredConnections.forEach((conn) => {
|
||||
mihomoCloseConnection(conn.id)
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
关闭所有连接
|
||||
关闭所有连接({filteredConnections.length})
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{isDetailModalOpen && selectedConnection && (
|
||||
<ConnectionDetailModal
|
||||
onClose={() => setIsDetailModalOpen(false)}
|
||||
connection={selectedConnection}
|
||||
/>
|
||||
)}
|
||||
<div className="overflow-x-auto sticky top-[49px] z-40">
|
||||
<div className="flex p-2">
|
||||
<div className="flex p-2 gap-2">
|
||||
<Input
|
||||
variant="bordered"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
value={filter}
|
||||
placeholder="筛选过滤"
|
||||
isClearable
|
||||
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>
|
||||
<Divider />
|
||||
</div>
|
||||
<Table
|
||||
onRowAction={(id: Key) => {
|
||||
setSelectedConnection(connections.find((c) => c.id === (id as string)))
|
||||
setIsDetailModalOpen(true)
|
||||
}}
|
||||
isHeaderSticky
|
||||
isStriped
|
||||
className="h-[calc(100vh-100px)] p-2"
|
||||
>
|
||||
<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>
|
||||
<div className="h-[calc(100vh-100px)] mt-[1px]">
|
||||
<Virtuoso
|
||||
data={filteredConnections}
|
||||
itemContent={(i, connection) => (
|
||||
<ConnectionItem
|
||||
close={mihomoCloseConnection}
|
||||
index={i}
|
||||
key={connection.id}
|
||||
info={connection}
|
||||
/>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/* {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}
|
||||
/>
|
||||
)
|
||||
})} */}
|
||||
/>
|
||||
</div>
|
||||
</BasePage>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ const Logs: React.FC = () => {
|
|||
<div className="sticky top-[49px] z-40">
|
||||
<div className="w-full flex p-2">
|
||||
<Input
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
value={filter}
|
||||
placeholder="筛选过滤"
|
||||
|
|
|
@ -136,7 +136,6 @@ const Override: React.FC = () => {
|
|||
<div className="sticky top-[49px] z-40 backdrop-blur bg-background/40">
|
||||
<div className="flex p-2">
|
||||
<Input
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
value={url}
|
||||
onValueChange={setUrl}
|
||||
|
|
|
@ -126,7 +126,7 @@ const Profiles: React.FC = () => {
|
|||
<div className="sticky top-[49px] z-40 backdrop-blur bg-background/40">
|
||||
<div className="flex p-2">
|
||||
<Input
|
||||
variant="bordered"
|
||||
// variant="bordered"
|
||||
size="sm"
|
||||
value={url}
|
||||
onValueChange={setUrl}
|
||||
|
|
|
@ -22,10 +22,9 @@ const Rules: React.FC = () => {
|
|||
|
||||
return (
|
||||
<BasePage title="分流规则">
|
||||
<div className="sticky top-[50px] z-40">
|
||||
<div className="sticky top-[49px] z-40">
|
||||
<div className="flex p-2">
|
||||
<Input
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
value={filter}
|
||||
placeholder="筛选过滤"
|
||||
|
|
Loading…
Reference in New Issue
Block a user