mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 14:01:56 +08:00
Feature: add connections filter
This commit is contained in:
parent
9f602e23a4
commit
d5fa59f477
35
src/containers/Connections/Devices/index.tsx
Normal file
35
src/containers/Connections/Devices/index.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import { BaseComponentProps } from '@models'
|
||||||
|
import './style.scss'
|
||||||
|
|
||||||
|
interface DevicesProps extends BaseComponentProps {
|
||||||
|
devices: Array<{ label: string, number: number }>
|
||||||
|
selected: string
|
||||||
|
onChange?: (label: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Devices (props: DevicesProps) {
|
||||||
|
const { className, style } = props
|
||||||
|
const classname = classnames('connections-devices', className)
|
||||||
|
function handleSelected (label: string) {
|
||||||
|
props.onChange?.(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classname} style={style}>
|
||||||
|
<div className={classnames('connections-devices-item', { selected: props.selected === '' })} onClick={() => handleSelected('')}>全部</div>
|
||||||
|
{
|
||||||
|
props.devices.map(
|
||||||
|
device => (
|
||||||
|
<div
|
||||||
|
className={classnames('connections-devices-item', { selected: props.selected === device.label })}
|
||||||
|
onClick={() => handleSelected(device.label)}>
|
||||||
|
{ device.label } ({ device.number })
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
21
src/containers/Connections/Devices/style.scss
Normal file
21
src/containers/Connections/Devices/style.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@import '~@styles/variables';
|
||||||
|
|
||||||
|
.connections-devices {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-devices-item {
|
||||||
|
padding: 4px 10px;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $color-gray-darken;
|
||||||
|
background-color: $color-gray-light;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color .3s ease;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
color: $color-primary-dark;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
import React, { useMemo, useLayoutEffect, useCallback, useRef } from 'react'
|
import React, { useMemo, useLayoutEffect, useCallback, useRef, useState } from 'react'
|
||||||
import { Cell, Column, ColumnInstance, TableOptions, useBlockLayout, 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 { useScroll } from 'react-use'
|
||||||
|
import { groupBy } from 'lodash'
|
||||||
import { Header, Card, Checkbox, Modal, Icon } from '@components'
|
import { Header, Card, Checkbox, Modal, Icon } from '@components'
|
||||||
import { useI18n } from '@stores'
|
import { useI18n } from '@stores'
|
||||||
import * as API from '@lib/request'
|
import * as API from '@lib/request'
|
||||||
@ -9,6 +10,7 @@ import { StreamReader } from '@lib/streamer'
|
|||||||
import { useObject, useVisible } from '@lib/hook'
|
import { useObject, useVisible } from '@lib/hook'
|
||||||
import { fromNow } from '@lib/date'
|
import { fromNow } from '@lib/date'
|
||||||
import { RuleType } from '@models'
|
import { RuleType } from '@models'
|
||||||
|
import { Devices } from './Devices'
|
||||||
import { useConnections } from './store'
|
import { useConnections } from './store'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
|
|
||||||
@ -39,7 +41,12 @@ type TableColumnOption<D extends object = {}> =
|
|||||||
|
|
||||||
interface ITableOptions<D extends object = {}> extends
|
interface ITableOptions<D extends object = {}> extends
|
||||||
TableOptions<D>,
|
TableOptions<D>,
|
||||||
UseSortByOptions<D> {}
|
UseSortByOptions<D>,
|
||||||
|
UseFiltersOptions<D> {}
|
||||||
|
|
||||||
|
interface ITableInstance<D extends object = {}> extends
|
||||||
|
TableInstance<D>,
|
||||||
|
UseFiltersInstanceProps<D> {}
|
||||||
|
|
||||||
function formatTraffic(num: number) {
|
function formatTraffic(num: number) {
|
||||||
const s = ['B', 'KB', 'MB', 'GB', 'TB']
|
const s = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
@ -117,6 +124,10 @@ export default function Connections() {
|
|||||||
completed: !!c.completed
|
completed: !!c.completed
|
||||||
})
|
})
|
||||||
), [connections])
|
), [connections])
|
||||||
|
const devices = useMemo(() => {
|
||||||
|
const gb = groupBy(connections, 'metadata.sourceIP')
|
||||||
|
return Object.keys(gb).map(key => ({ label: key, number: gb[key].length }))
|
||||||
|
}, [connections])
|
||||||
|
|
||||||
// table
|
// table
|
||||||
const tableRef = useRef<HTMLDivElement>(null)
|
const tableRef = useRef<HTMLDivElement>(null)
|
||||||
@ -181,18 +192,21 @@ export default function Connections() {
|
|||||||
getTableBodyProps,
|
getTableBodyProps,
|
||||||
headerGroups,
|
headerGroups,
|
||||||
rows,
|
rows,
|
||||||
prepareRow
|
prepareRow,
|
||||||
|
setFilter
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
autoResetSortBy: false,
|
autoResetSortBy: 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,
|
||||||
useSortBy
|
useSortBy
|
||||||
)
|
) 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) {
|
||||||
@ -208,6 +222,13 @@ export default function Connections() {
|
|||||||
}
|
}
|
||||||
}, [lang])
|
}, [lang])
|
||||||
|
|
||||||
|
// filter
|
||||||
|
const [device, setDevice] = useState('')
|
||||||
|
function handleDeviceSelected (label: string) {
|
||||||
|
setDevice(label)
|
||||||
|
setFilter?.(Columns.SourceIP, label)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<Header title={t('title')}>
|
<Header title={t('title')}>
|
||||||
@ -217,6 +238,7 @@ export default function Connections() {
|
|||||||
<Checkbox className="connections-filter" checked={save} onChange={toggleSave}>{t('keepClosed')}</Checkbox>
|
<Checkbox className="connections-filter" checked={save} onChange={toggleSave}>{t('keepClosed')}</Checkbox>
|
||||||
<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} /> }
|
||||||
<Card className="connections-card">
|
<Card className="connections-card">
|
||||||
<div {...getTableProps()} className="connections" ref={tableRef}>
|
<div {...getTableProps()} className="connections" ref={tableRef}>
|
||||||
<div {...headerGroup.getHeaderGroupProps()} className="connections-header">
|
<div {...headerGroup.getHeaderGroupProps()} className="connections-header">
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $color-gray-darken;
|
color: $color-gray-darken;
|
||||||
background: #f3f6f9;
|
background: $color-gray-light;
|
||||||
height: $height;
|
height: $height;
|
||||||
line-height: $height;
|
line-height: $height;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@ -105,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.completed {
|
&.completed {
|
||||||
background-color: darken(#f3f6f9, 3%);
|
background-color: darken($color-gray-light, 3%);
|
||||||
color: rgba($color-primary-darken, 50%);
|
color: rgba($color-primary-darken, 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@import '~@styles/variables';
|
||||||
|
|
||||||
.logs-card {
|
.logs-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -13,7 +15,7 @@
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: #f3f6f9;
|
background-color: $color-gray-light;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #73808f;
|
color: #73808f;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
@ -14,6 +14,7 @@ $color-primary-lightly: #e4eaef;
|
|||||||
|
|
||||||
// common colors
|
// common colors
|
||||||
$color-gray: #d8dee2;
|
$color-gray: #d8dee2;
|
||||||
|
$color-gray-light: #f3f6f9;
|
||||||
$color-gray-dark: #b7c5d6;
|
$color-gray-dark: #b7c5d6;
|
||||||
$color-gray-darken: #909399;
|
$color-gray-darken: #909399;
|
||||||
$color-white: #fff;
|
$color-white: #fff;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user