mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 14:01:56 +08:00
Feature: add connection information
This commit is contained in:
parent
df0bfb5e10
commit
610d63cf7d
@ -1,4 +1,4 @@
|
|||||||
import * as React from 'react'
|
import React, { MouseEventHandler } from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { BaseComponentProps } from '@models'
|
import { BaseComponentProps } from '@models'
|
||||||
import { noop } from '@lib/helper'
|
import { noop } from '@lib/helper'
|
||||||
@ -6,18 +6,20 @@ import './style.scss'
|
|||||||
|
|
||||||
interface ButtonProps extends BaseComponentProps {
|
interface ButtonProps extends BaseComponentProps {
|
||||||
type?: 'primary' | 'normal' | 'danger' | 'success' | 'warning'
|
type?: 'primary' | 'normal' | 'danger' | 'success' | 'warning'
|
||||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
onClick?: MouseEventHandler<HTMLButtonElement>
|
||||||
|
disiabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button (props: ButtonProps) {
|
export function Button (props: ButtonProps) {
|
||||||
const { type = 'normal', onClick = noop, children, className, style } = props
|
const { type = 'normal', onClick = noop, children, className, style, disiabled } = props
|
||||||
const classname = classnames('button', `button-${type}`, className)
|
const classname = classnames('button', `button-${type}`, className, { 'button-disabled': disiabled })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classname}
|
className={classname}
|
||||||
style={style}
|
style={style}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
disabled={disiabled}
|
||||||
>{children}</button>
|
>{children}</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,10 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 150ms ease;
|
transition: all 150ms ease;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-primary {
|
.button-primary {
|
||||||
@ -84,3 +88,14 @@
|
|||||||
box-shadow: 0 0 2px rgba($color: darken($color-orange, 5%), $alpha: 0.5);
|
box-shadow: 0 0 2px rgba($color: darken($color-orange, 5%), $alpha: 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button.button-disabled {
|
||||||
|
color: $color-gray-dark;
|
||||||
|
background: linear-gradient(135deg, $color-gray-light, darken($color-gray-light, 5%));
|
||||||
|
box-shadow: 0 2px 8px rgba($color: darken($color-gray-light, 5%), $alpha: 0.5);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
box-shadow: 0 0 2px rgba($color: darken($color-gray-light, 5%), $alpha: 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import * as React from 'react'
|
import React, { forwardRef } from 'react'
|
||||||
import { BaseComponentProps } from '@models/BaseProps'
|
import { BaseComponentProps } from '@models/BaseProps'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
|
|
||||||
type CardProps = BaseComponentProps
|
interface CardProps extends BaseComponentProps {
|
||||||
|
ref?: React.ForwardedRef<HTMLDivElement>
|
||||||
|
}
|
||||||
|
|
||||||
export function Card (props: CardProps) {
|
export const Card = forwardRef<HTMLDivElement, CardProps>((props: CardProps, ref) => {
|
||||||
const { className, style, children } = props
|
const { className, style, children } = props
|
||||||
return (
|
return (
|
||||||
<div className={classnames('card', className)} style={style}>
|
<div className={classnames('card', className)} style={style} ref={ref}>
|
||||||
{ children }
|
{ children }
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
31
src/components/Drawer/index.tsx
Normal file
31
src/components/Drawer/index.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React, { useLayoutEffect, useRef, RefObject } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import { BaseComponentProps } from '@models/BaseProps'
|
||||||
|
import { Card } from '@components'
|
||||||
|
|
||||||
|
interface DrawerProps extends BaseComponentProps {
|
||||||
|
visible?: boolean
|
||||||
|
width?: number
|
||||||
|
containerRef?: RefObject<HTMLElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Drawer (props: DrawerProps) {
|
||||||
|
const portalRef = useRef<HTMLElement>(document.createElement('div'))
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const current = portalRef.current
|
||||||
|
document.body.appendChild(current)
|
||||||
|
return () => { document.body.removeChild(current) }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const cardStyle = 'absolute h-full right-0 transition-transform transform translate-x-full duration-100 pointer-events-auto'
|
||||||
|
|
||||||
|
const container = (
|
||||||
|
<div className={classnames(props.className, 'absolute inset-0 pointer-events-none z-9999')}>
|
||||||
|
<Card className={classnames(cardStyle, { 'translate-x-0': props.visible })} style={{ width: props.width ?? 400 }}>{props.children}</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return createPortal(container, props.containerRef?.current ?? portalRef.current)
|
||||||
|
}
|
@ -13,3 +13,4 @@ export * from './Message'
|
|||||||
export * from './Checkbox'
|
export * from './Checkbox'
|
||||||
export * from './Tag'
|
export * from './Tag'
|
||||||
export * from './Loading'
|
export * from './Loading'
|
||||||
|
export * from './Drawer'
|
||||||
|
86
src/containers/Connections/Info/index.tsx
Normal file
86
src/containers/Connections/Info/index.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React, { useMemo } from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import { useI18n } from '@stores'
|
||||||
|
import { formatTraffic } from '@lib/helper'
|
||||||
|
import { BaseComponentProps } from '@models'
|
||||||
|
import { Connection } from '../store'
|
||||||
|
|
||||||
|
interface ConnectionsInfoProps extends BaseComponentProps {
|
||||||
|
connection: Partial<Connection>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConnectionInfo (props: ConnectionsInfoProps) {
|
||||||
|
const { translation } = useI18n()
|
||||||
|
const t = useMemo(() => translation('Connections').t, [translation])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classnames(props.className, 'text-sm flex flex-col')}>
|
||||||
|
<div className="flex my-3">
|
||||||
|
<span className="w-14 font-bold">{t('info.id')}</span>
|
||||||
|
<span className="font-mono">{props.connection.id}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between my-3">
|
||||||
|
<div className="flex flex-1">
|
||||||
|
<span className="w-14 font-bold">{t('info.network')}</span>
|
||||||
|
<span className="font-mono">{props.connection.metadata?.network}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1">
|
||||||
|
<span className="w-14 font-bold">{t('info.inbound')}</span>
|
||||||
|
<span className="font-mono">{props.connection.metadata?.type}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex my-3">
|
||||||
|
<span className="w-14 font-bold">{t('info.host')}</span>
|
||||||
|
<span className="font-mono flex-1 break-all">{
|
||||||
|
props.connection.metadata?.host
|
||||||
|
? `${props.connection.metadata.host}:${props.connection.metadata?.destinationPort}`
|
||||||
|
: t('info.hostEmpty')
|
||||||
|
}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex my-3">
|
||||||
|
<span className="w-14 font-bold">{t('info.dstIP')}</span>
|
||||||
|
<span className="font-mono">{
|
||||||
|
props.connection.metadata?.destinationIP
|
||||||
|
? `${props.connection.metadata.destinationIP}:${props.connection.metadata?.destinationPort}`
|
||||||
|
: t('info.hostEmpty')
|
||||||
|
}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex my-3">
|
||||||
|
<span className="w-14 font-bold">{t('info.srcIP')}</span>
|
||||||
|
<span className="font-mono">{
|
||||||
|
`${props.connection.metadata?.sourceIP}:${props.connection.metadata?.sourcePort}`
|
||||||
|
}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex my-3">
|
||||||
|
<span className="w-14 font-bold">{t('info.rule')}</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
{ props.connection.rule && `${props.connection.rule}${props.connection.rulePayload && `(${props.connection.rulePayload})`}` }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex my-3">
|
||||||
|
<span className="w-14 font-bold">{t('info.chains')}</span>
|
||||||
|
<span className="font-mono flex-1 break-all">
|
||||||
|
{ props.connection.chains?.slice().reverse().join(' / ') }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between my-3">
|
||||||
|
<div className="flex flex-1">
|
||||||
|
<span className="w-14 font-bold">{t('info.upload')}</span>
|
||||||
|
<span className="font-mono">{formatTraffic(props.connection.upload ?? 0)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1">
|
||||||
|
<span className="w-14 font-bold">{t('info.download')}</span>
|
||||||
|
<span className="font-mono">{formatTraffic(props.connection.download ?? 0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex my-3">
|
||||||
|
<span className="w-14 font-bold">{t('info.status')}</span>
|
||||||
|
<span className="font-mono">{
|
||||||
|
!props.connection.completed
|
||||||
|
? <span className="text-green">{t('info.opening')}</span>
|
||||||
|
: <span className="text-red">{t('info.closed')}</span>
|
||||||
|
}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,16 +1,18 @@
|
|||||||
import React, { useMemo, useLayoutEffect, useCallback, useRef, useState } from 'react'
|
import React, { useMemo, useLayoutEffect, useCallback, useRef, useState, useEffect } from 'react'
|
||||||
import { Cell, Column, ColumnInstance, TableInstance, TableOptions, useBlockLayout, useFilters, UseFiltersInstanceProps, UseFiltersOptions, useResizeColumns, UseResizeColumnsColumnProps, UseResizeColumnsOptions, useSortBy, UseSortByColumnOptions, UseSortByColumnProps, UseSortByOptions, useTable } from 'react-table'
|
import { Cell, Column, ColumnInstance, TableInstance, TableOptions, useBlockLayout, useFilters, UseFiltersInstanceProps, UseFiltersOptions, useResizeColumns, UseResizeColumnsColumnProps, UseResizeColumnsOptions, useSortBy, UseSortByColumnOptions, UseSortByColumnProps, UseSortByOptions, useTable } from 'react-table'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { useScroll } from 'react-use'
|
import { useLatest, useScroll } from 'react-use'
|
||||||
import { groupBy } from 'lodash-es'
|
import { groupBy } from 'lodash-es'
|
||||||
import { Header, Card, Checkbox, Modal, Icon } from '@components'
|
import { Header, Checkbox, Modal, Icon, Drawer, Card, Button } from '@components'
|
||||||
import { useClient, useConnectionStreamReader, useI18n } from '@stores'
|
import { useClient, useConnectionStreamReader, useI18n } from '@stores'
|
||||||
import * as API from '@lib/request'
|
import * as API from '@lib/request'
|
||||||
import { useObject, useVisible } from '@lib/hook'
|
import { useObject, useVisible } from '@lib/hook'
|
||||||
import { fromNow } from '@lib/date'
|
import { fromNow } from '@lib/date'
|
||||||
|
import { formatTraffic } from '@lib/helper'
|
||||||
import { RuleType } from '@models'
|
import { RuleType } from '@models'
|
||||||
import { Devices } from './Devices'
|
import { Devices } from './Devices'
|
||||||
import { useConnections } from './store'
|
import { ConnectionInfo } from './Info'
|
||||||
|
import { Connection, FormatConnection, useConnections } from './store'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
|
|
||||||
enum Columns {
|
enum Columns {
|
||||||
@ -47,17 +49,6 @@ interface ITableInstance<D extends object = {}> extends
|
|||||||
TableInstance<D>,
|
TableInstance<D>,
|
||||||
UseFiltersInstanceProps<D> {}
|
UseFiltersInstanceProps<D> {}
|
||||||
|
|
||||||
function formatTraffic (num: number) {
|
|
||||||
const s = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
||||||
let idx = 0
|
|
||||||
while (~~(num / 1024) && idx < s.length) {
|
|
||||||
num /= 1024
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${idx === 0 ? num : num.toFixed(2)} ${s[idx]}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSpeed (upload: number, download: number) {
|
function formatSpeed (upload: number, download: number) {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case upload === 0 && download === 0:
|
case upload === 0 && download === 0:
|
||||||
@ -71,29 +62,12 @@ function formatSpeed (upload: number, download: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface formatConnection {
|
|
||||||
id: string
|
|
||||||
host: string
|
|
||||||
chains: string
|
|
||||||
rule: string
|
|
||||||
time: number
|
|
||||||
upload: number
|
|
||||||
download: number
|
|
||||||
type: string
|
|
||||||
network: string
|
|
||||||
sourceIP: string
|
|
||||||
speed: {
|
|
||||||
upload: number
|
|
||||||
download: number
|
|
||||||
}
|
|
||||||
completed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Connections () {
|
export default function Connections () {
|
||||||
const { translation, lang } = useI18n()
|
const { translation, lang } = useI18n()
|
||||||
const t = useMemo(() => translation('Connections').t, [translation])
|
const t = useMemo(() => translation('Connections').t, [translation])
|
||||||
const connStreamReader = useConnectionStreamReader()
|
const connStreamReader = useConnectionStreamReader()
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
|
const cardRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// total
|
// total
|
||||||
const [traffic, setTraffic] = useObject({
|
const [traffic, setTraffic] = useObject({
|
||||||
@ -109,7 +83,7 @@ export default function Connections () {
|
|||||||
|
|
||||||
// connections
|
// connections
|
||||||
const { connections, feed, save, toggleSave } = useConnections()
|
const { connections, feed, save, toggleSave } = useConnections()
|
||||||
const data: formatConnection[] = useMemo(() => connections.map(
|
const data: FormatConnection[] = useMemo(() => connections.map(
|
||||||
c => ({
|
c => ({
|
||||||
id: c.id,
|
id: c.id,
|
||||||
host: `${c.metadata.host || c.metadata.destinationIP}:${c.metadata.destinationPort}`,
|
host: `${c.metadata.host || c.metadata.destinationIP}:${c.metadata.destinationPort}`,
|
||||||
@ -123,6 +97,7 @@ export default function Connections () {
|
|||||||
network: c.metadata.network.toUpperCase(),
|
network: c.metadata.network.toUpperCase(),
|
||||||
speed: { upload: c.uploadSpeed, download: c.downloadSpeed },
|
speed: { upload: c.uploadSpeed, download: c.downloadSpeed },
|
||||||
completed: !!c.completed,
|
completed: !!c.completed,
|
||||||
|
original: c,
|
||||||
}),
|
}),
|
||||||
), [connections])
|
), [connections])
|
||||||
const devices = useMemo(() => {
|
const devices = useMemo(() => {
|
||||||
@ -133,7 +108,7 @@ export default function Connections () {
|
|||||||
// table
|
// table
|
||||||
const tableRef = useRef<HTMLDivElement>(null)
|
const tableRef = useRef<HTMLDivElement>(null)
|
||||||
const { x: scrollX } = useScroll(tableRef)
|
const { x: scrollX } = useScroll(tableRef)
|
||||||
const columns: Array<TableColumnOption<formatConnection>> = useMemo(() => [
|
const columns: Array<TableColumnOption<FormatConnection>> = useMemo(() => [
|
||||||
{ Header: t(`columns.${Columns.Host}`), accessor: Columns.Host, minWidth: 260, width: 260 },
|
{ Header: t(`columns.${Columns.Host}`), accessor: Columns.Host, minWidth: 260, width: 260 },
|
||||||
{ Header: t(`columns.${Columns.Network}`), accessor: Columns.Network, minWidth: 80, width: 80 },
|
{ Header: t(`columns.${Columns.Network}`), accessor: Columns.Network, minWidth: 80, width: 80 },
|
||||||
{ Header: t(`columns.${Columns.Type}`), accessor: Columns.Type, minWidth: 120, width: 120 },
|
{ Header: t(`columns.${Columns.Type}`), accessor: Columns.Type, minWidth: 120, width: 120 },
|
||||||
@ -142,7 +117,7 @@ export default function Connections () {
|
|||||||
{
|
{
|
||||||
id: Columns.Speed,
|
id: Columns.Speed,
|
||||||
Header: t(`columns.${Columns.Speed}`),
|
Header: t(`columns.${Columns.Speed}`),
|
||||||
accessor (originalRow: formatConnection) {
|
accessor (originalRow: FormatConnection) {
|
||||||
return [originalRow.speed.upload, originalRow.speed.download]
|
return [originalRow.speed.upload, originalRow.speed.download]
|
||||||
},
|
},
|
||||||
sortType (rowA, rowB) {
|
sortType (rowA, rowB) {
|
||||||
@ -160,7 +135,7 @@ export default function Connections () {
|
|||||||
{ Header: t(`columns.${Columns.Download}`), accessor: Columns.Download, minWidth: 100, width: 100, sortDescFirst: true },
|
{ Header: t(`columns.${Columns.Download}`), accessor: Columns.Download, minWidth: 100, width: 100, sortDescFirst: true },
|
||||||
{ Header: t(`columns.${Columns.SourceIP}`), accessor: Columns.SourceIP, minWidth: 140, width: 140 },
|
{ Header: t(`columns.${Columns.SourceIP}`), accessor: Columns.SourceIP, minWidth: 140, width: 140 },
|
||||||
{ Header: t(`columns.${Columns.Time}`), accessor: Columns.Time, minWidth: 120, width: 120, sortType (rowA, rowB) { return rowB.original.time - rowA.original.time } },
|
{ Header: t(`columns.${Columns.Time}`), accessor: Columns.Time, minWidth: 120, width: 120, sortType (rowA, rowB) { return rowB.original.time - rowA.original.time } },
|
||||||
] as Array<TableColumnOption<formatConnection>>, [t])
|
] as Array<TableColumnOption<FormatConnection>>, [t])
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
function handleConnection (snapshots: API.Snapshot[]) {
|
function handleConnection (snapshots: API.Snapshot[]) {
|
||||||
@ -195,14 +170,14 @@ export default function Connections () {
|
|||||||
autoResetSortBy: false,
|
autoResetSortBy: false,
|
||||||
autoResetFilters: false,
|
autoResetFilters: false,
|
||||||
initialState: { sortBy: [{ id: Columns.Time, desc: false }] },
|
initialState: { sortBy: [{ id: Columns.Time, desc: false }] },
|
||||||
} as ITableOptions<formatConnection>,
|
} as ITableOptions<FormatConnection>,
|
||||||
useResizeColumns,
|
useResizeColumns,
|
||||||
useBlockLayout,
|
useBlockLayout,
|
||||||
useFilters,
|
useFilters,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
) as ITableInstance<formatConnection>
|
) as ITableInstance<FormatConnection>
|
||||||
const headerGroup = useMemo(() => headerGroups[0], [headerGroups])
|
const headerGroup = useMemo(() => headerGroups[0], [headerGroups])
|
||||||
const renderCell = useCallback(function (cell: Cell<formatConnection>) {
|
const renderCell = useCallback(function (cell: Cell<FormatConnection>) {
|
||||||
switch (cell.column.id) {
|
switch (cell.column.id) {
|
||||||
case Columns.Speed:
|
case Columns.Speed:
|
||||||
return formatSpeed(cell.value[0], cell.value[1])
|
return formatSpeed(cell.value[0], cell.value[1])
|
||||||
@ -223,6 +198,37 @@ export default function Connections () {
|
|||||||
setFilter?.(Columns.SourceIP, label)
|
setFilter?.(Columns.SourceIP, label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// click item
|
||||||
|
const [drawerState, setDrawerState] = useObject({
|
||||||
|
visible: false,
|
||||||
|
selectedID: '',
|
||||||
|
connection: {} as Partial<Connection>,
|
||||||
|
})
|
||||||
|
function handleConnectionSelected (id: string) {
|
||||||
|
setDrawerState({
|
||||||
|
visible: true,
|
||||||
|
selectedID: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function handleConnectionClosed () {
|
||||||
|
setDrawerState(d => { d.connection.completed = true })
|
||||||
|
client.closeConnection(drawerState.selectedID)
|
||||||
|
}
|
||||||
|
const latestConntion = useLatest(drawerState.connection)
|
||||||
|
useEffect(() => {
|
||||||
|
const conn = data.find(c => c.id === drawerState.selectedID)?.original
|
||||||
|
if (conn) {
|
||||||
|
setDrawerState(d => {
|
||||||
|
d.connection = conn
|
||||||
|
if (drawerState.selectedID === latestConntion.current.id) {
|
||||||
|
d.connection.completed = latestConntion.current.completed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (Object.keys(latestConntion.current).length !== 0 && !latestConntion.current.completed) {
|
||||||
|
setDrawerState(d => { d.connection.completed = true })
|
||||||
|
}
|
||||||
|
}, [data, drawerState.selectedID, latestConntion, setDrawerState])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<Header title={t('title')}>
|
<Header title={t('title')}>
|
||||||
@ -233,12 +239,12 @@ export default function Connections () {
|
|||||||
<Icon className="connections-filter dangerous" onClick={show} type="close-all" size={20} />
|
<Icon className="connections-filter dangerous" onClick={show} type="close-all" size={20} />
|
||||||
</Header>
|
</Header>
|
||||||
{ devices.length > 1 && <Devices devices={devices} selected={device} onChange={handleDeviceSelected} /> }
|
{ devices.length > 1 && <Devices devices={devices} selected={device} onChange={handleDeviceSelected} /> }
|
||||||
<Card className="connections-card">
|
<Card ref={cardRef} className="connections-card relative">
|
||||||
<div {...getTableProps()} className="flex flex-col w-full flex-1 overflow-auto" style={{ flexBasis: 0 }} ref={tableRef}>
|
<div {...getTableProps()} className="flex flex-col w-full flex-1 overflow-auto" style={{ flexBasis: 0 }} ref={tableRef}>
|
||||||
<div {...headerGroup.getHeaderGroupProps()} className="connections-header">
|
<div {...headerGroup.getHeaderGroupProps()} className="connections-header">
|
||||||
{
|
{
|
||||||
headerGroup.headers.map((column, idx) => {
|
headerGroup.headers.map((column, idx) => {
|
||||||
const realColumn = column as unknown as TableColumn<formatConnection>
|
const realColumn = column as unknown as TableColumn<FormatConnection>
|
||||||
const id = realColumn.id
|
const id = realColumn.id
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -270,7 +276,11 @@ export default function Connections () {
|
|||||||
rows.map(row => {
|
rows.map(row => {
|
||||||
prepareRow(row)
|
prepareRow(row)
|
||||||
return (
|
return (
|
||||||
<div {...row.getRowProps()} className="connections-item" key={row.original.id}>
|
<div
|
||||||
|
{...row.getRowProps()}
|
||||||
|
className="connections-item cursor-default select-none"
|
||||||
|
key={row.original.id}
|
||||||
|
onClick={() => handleConnectionSelected(row.original.id)}>
|
||||||
{
|
{
|
||||||
row.cells.map(cell => {
|
row.cells.map(cell => {
|
||||||
const classname = classnames(
|
const classname = classnames(
|
||||||
@ -293,6 +303,16 @@ export default function Connections () {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Modal title={t('closeAll.title')} show={visible} onClose={hide} onOk={handleCloseConnections}>{t('closeAll.content')}</Modal>
|
<Modal title={t('closeAll.title')} show={visible} onClose={hide} onOk={handleCloseConnections}>{t('closeAll.content')}</Modal>
|
||||||
|
<Drawer containerRef={cardRef} visible={drawerState.visible} width={450}>
|
||||||
|
<div className="flex justify-between items-center h-8">
|
||||||
|
<span className="pl-3 font-bold">{t('info.title')}</span>
|
||||||
|
<Icon type="close" size={16} className="cursor-pointer" onClick={() => setDrawerState('visible', false)} />
|
||||||
|
</div>
|
||||||
|
<ConnectionInfo className="px-5 mt-3" connection={drawerState.connection} />
|
||||||
|
<div className="flex justify-end mt-3 pr-3">
|
||||||
|
<Button type="danger" disiabled={drawerState.connection.completed} onClick={() => handleConnectionClosed()}>{ t('info.closeConnection') }</Button>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,25 @@ import { useState, useMemo, useRef, useCallback } from 'react'
|
|||||||
|
|
||||||
export type Connection = API.Connections & { completed?: boolean, uploadSpeed: number, downloadSpeed: number }
|
export type Connection = API.Connections & { completed?: boolean, uploadSpeed: number, downloadSpeed: number }
|
||||||
|
|
||||||
|
export interface FormatConnection {
|
||||||
|
id: string
|
||||||
|
host: string
|
||||||
|
chains: string
|
||||||
|
rule: string
|
||||||
|
time: number
|
||||||
|
upload: number
|
||||||
|
download: number
|
||||||
|
type: string
|
||||||
|
network: string
|
||||||
|
sourceIP: string
|
||||||
|
speed: {
|
||||||
|
upload: number
|
||||||
|
download: number
|
||||||
|
}
|
||||||
|
completed: boolean
|
||||||
|
original: Connection
|
||||||
|
}
|
||||||
|
|
||||||
class Store {
|
class Store {
|
||||||
protected connections = new Map<string, Connection>()
|
protected connections = new Map<string, Connection>()
|
||||||
protected saveDisconnection = false
|
protected saveDisconnection = false
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
.connections-header {
|
.connections-header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 9999;
|
z-index: 999;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover .connections-resizer {
|
&:hover .connections-resizer {
|
||||||
@ -90,7 +90,7 @@
|
|||||||
&.fixed {
|
&.fixed {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 999;
|
z-index: 998;
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
box-shadow: inset -9px 0 8px -14px $color-black;
|
box-shadow: inset -9px 0 8px -14px $color-black;
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ export default function Settings () {
|
|||||||
{ label: t('values.global'), value: 'Global' },
|
{ label: t('values.global'), value: 'Global' },
|
||||||
{ label: t('values.rules'), value: 'Rule' },
|
{ label: t('values.rules'), value: 'Rule' },
|
||||||
{ label: t('values.direct'), value: 'Direct' },
|
{ label: t('values.direct'), value: 'Direct' },
|
||||||
]
|
] as Array<{ label: string, value: string }>
|
||||||
if (premium) {
|
if (premium) {
|
||||||
options.push({ label: t('values.script'), value: 'Script' })
|
options.push({ label: t('values.script'), value: 'Script' })
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
line-height: 17px;
|
line-height: 17px;
|
||||||
|
|
||||||
&.modify-btn {
|
&.modify-btn {
|
||||||
color: $color-primary-dark;
|
color: $color-primary;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,25 @@ const EN = {
|
|||||||
download: 'Download',
|
download: 'Download',
|
||||||
sourceIP: 'Source IP',
|
sourceIP: 'Source IP',
|
||||||
},
|
},
|
||||||
|
info: {
|
||||||
|
title: 'Connection',
|
||||||
|
id: 'ID',
|
||||||
|
host: 'Host',
|
||||||
|
hostEmpty: 'Empty',
|
||||||
|
dstIP: 'IP',
|
||||||
|
dstIPEmpty: 'Empty',
|
||||||
|
srcIP: 'Source',
|
||||||
|
upload: 'Upload',
|
||||||
|
download: 'Download',
|
||||||
|
network: 'Network',
|
||||||
|
inbound: 'Inbound',
|
||||||
|
rule: 'Rule',
|
||||||
|
chains: 'Chains',
|
||||||
|
status: 'Status',
|
||||||
|
opening: 'Open',
|
||||||
|
closed: 'Closed',
|
||||||
|
closeConnection: 'Close',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Proxies: {
|
Proxies: {
|
||||||
title: 'Proxies',
|
title: 'Proxies',
|
||||||
@ -105,6 +124,6 @@ const EN = {
|
|||||||
ok: 'Ok',
|
ok: 'Ok',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
},
|
},
|
||||||
}
|
} as const
|
||||||
|
|
||||||
export default EN
|
export default EN
|
||||||
|
@ -75,6 +75,25 @@ const CN = {
|
|||||||
download: '下载',
|
download: '下载',
|
||||||
sourceIP: '来源 IP',
|
sourceIP: '来源 IP',
|
||||||
},
|
},
|
||||||
|
info: {
|
||||||
|
title: '连接信息',
|
||||||
|
id: 'ID',
|
||||||
|
host: '域名',
|
||||||
|
hostEmpty: '空',
|
||||||
|
dstIP: 'IP',
|
||||||
|
dstIPEmpty: '空',
|
||||||
|
srcIP: '来源',
|
||||||
|
upload: '上传',
|
||||||
|
download: '下载',
|
||||||
|
network: '网络',
|
||||||
|
inbound: '入口',
|
||||||
|
rule: '规则',
|
||||||
|
chains: '代理',
|
||||||
|
status: '状态',
|
||||||
|
opening: '连接中',
|
||||||
|
closed: '已关闭',
|
||||||
|
closeConnection: '关闭连接',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Proxies: {
|
Proxies: {
|
||||||
title: '代理',
|
title: '代理',
|
||||||
@ -105,6 +124,6 @@ const CN = {
|
|||||||
ok: '确 定',
|
ok: '确 定',
|
||||||
cancel: '取 消',
|
cancel: '取 消',
|
||||||
},
|
},
|
||||||
}
|
} as const
|
||||||
|
|
||||||
export default CN
|
export default CN
|
||||||
|
@ -8,3 +8,14 @@ export function partition<T> (arr: T[], fn: (arg: T) => boolean): [T[], T[]] {
|
|||||||
}
|
}
|
||||||
return [left, right]
|
return [left, right]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTraffic (num: number) {
|
||||||
|
const s = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
let idx = 0
|
||||||
|
while (~~(num / 1024) && idx < s.length) {
|
||||||
|
num /= 1024
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${idx === 0 ? num : num.toFixed(2)} ${s[idx]}`
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { useCallback, useEffect, useMemo } from 'react'
|
|||||||
import { get } from 'lodash-es'
|
import { get } from 'lodash-es'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
|
import { Get } from 'type-fest'
|
||||||
|
|
||||||
import { Language, locales, Lang, getDefaultLanguage } from '@i18n'
|
import { Language, locales, Lang, getDefaultLanguage } from '@i18n'
|
||||||
import { useWarpImmerSetter, WritableDraft } from '@lib/jotai'
|
import { useWarpImmerSetter, WritableDraft } from '@lib/jotai'
|
||||||
@ -14,10 +15,10 @@ import * as API from '@lib/request'
|
|||||||
import * as Models from '@models'
|
import * as Models from '@models'
|
||||||
import { partition } from '@lib/helper'
|
import { partition } from '@lib/helper'
|
||||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||||
import { useAPIInfo, useClient } from './request'
|
|
||||||
import { StreamReader } from '@lib/streamer'
|
import { StreamReader } from '@lib/streamer'
|
||||||
import { Log } from '@models/Log'
|
import { Log } from '@models/Log'
|
||||||
import { Snapshot } from '@lib/request'
|
import { Snapshot } from '@lib/request'
|
||||||
|
import { useAPIInfo, useClient } from './request'
|
||||||
|
|
||||||
export const identityAtom = atom(true)
|
export const identityAtom = atom(true)
|
||||||
|
|
||||||
@ -28,9 +29,9 @@ export function useI18n () {
|
|||||||
const lang = useMemo(() => defaultLang ?? getDefaultLanguage(), [defaultLang])
|
const lang = useMemo(() => defaultLang ?? getDefaultLanguage(), [defaultLang])
|
||||||
|
|
||||||
const translation = useCallback(
|
const translation = useCallback(
|
||||||
function (namespace: keyof typeof Language['en_US']) {
|
function <Namespace extends keyof typeof Language['en_US']>(namespace: Namespace) {
|
||||||
function t (path: string) {
|
function t<Path extends string> (path: Path): Get<typeof Language['en_US'][Namespace], Path> {
|
||||||
return get(Language[lang][namespace], path) as string
|
return get(Language[lang][namespace], path)
|
||||||
}
|
}
|
||||||
return { t }
|
return { t }
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,8 @@ export default defineConfig({
|
|||||||
500: '#57befc',
|
500: '#57befc',
|
||||||
600: '#2c8af8'
|
600: '#2c8af8'
|
||||||
},
|
},
|
||||||
red: '#f56c6c'
|
red: '#f56c6c',
|
||||||
|
green: '#67c23a'
|
||||||
},
|
},
|
||||||
textShadow: {
|
textShadow: {
|
||||||
primary: '0 0 6px rgb(44 138 248 / 40%)'
|
primary: '0 0 6px rgb(44 138 248 / 40%)'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user