mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 14:01:56 +08:00
Upgrade: react 18 && connection table scroll perf
This commit is contained in:
parent
fa98e692bd
commit
2866bafc6a
18
package.json
18
package.json
@ -33,10 +33,10 @@
|
|||||||
"@types/react-table": "^7.7.10",
|
"@types/react-table": "^7.7.10",
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
"@types/react-window": "^1.8.5",
|
"@types/react-window": "^1.8.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
"@typescript-eslint/eslint-plugin": "^5.17.0",
|
||||||
"@typescript-eslint/parser": "^5.16.0",
|
"@typescript-eslint/parser": "^5.17.0",
|
||||||
"@vitejs/plugin-react": "^1.2.0",
|
"@vitejs/plugin-react": "^1.2.0",
|
||||||
"eslint": "^8.11.0",
|
"eslint": "^8.12.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^16.1.4",
|
"eslint-config-airbnb-typescript": "^16.1.4",
|
||||||
"eslint-config-standard-with-typescript": "^21.0.1",
|
"eslint-config-standard-with-typescript": "^21.0.1",
|
||||||
@ -44,18 +44,19 @@
|
|||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^6.0.0",
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.4.0",
|
||||||
"sass": "^1.49.9",
|
"sass": "^1.49.9",
|
||||||
"type-fest": "^2.12.1",
|
"type-fest": "^2.12.1",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
"vite": "^2.8.6",
|
"vite": "^2.9.0",
|
||||||
"vite-plugin-pwa": "^0.11.13",
|
"vite-plugin-pwa": "^0.11.13",
|
||||||
"vite-plugin-windicss": "^1.8.3",
|
"vite-plugin-windicss": "^1.8.3",
|
||||||
"vite-tsconfig-paths": "^3.4.1",
|
"vite-tsconfig-paths": "^3.4.1",
|
||||||
"windicss": "^3.5.1"
|
"windicss": "^3.5.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-table": "^8.0.0-alpha.8",
|
"@react-hookz/web": "^13.1.0",
|
||||||
|
"@tanstack/react-table": "^8.0.0-alpha.11",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"dayjs": "^1.11.0",
|
"dayjs": "^1.11.0",
|
||||||
@ -64,10 +65,9 @@
|
|||||||
"jotai": "^1.6.1",
|
"jotai": "^1.6.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"neverthrow": "^4.3.1",
|
"neverthrow": "^4.3.1",
|
||||||
"react": "^18.0.0-rc.3",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0-rc.3",
|
"react-dom": "^18.0.0",
|
||||||
"react-router-dom": "^6.2.2",
|
"react-router-dom": "^6.2.2",
|
||||||
"react-use": "^17.3.2",
|
|
||||||
"react-virtualized-auto-sizer": "^1.0.6",
|
"react-virtualized-auto-sizer": "^1.0.6",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"swr": "^1.2.2",
|
"swr": "^1.2.2",
|
||||||
|
721
pnpm-lock.yaml
generated
721
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,9 @@
|
|||||||
import { columnFilterRowsFn, createTable, sortRowsFn } from '@tanstack/react-table'
|
import { useIntersectionObserver, useSyncedRef } from '@react-hookz/web/esm'
|
||||||
import { type ColumnSort } from '@tanstack/react-table/build/types/features/Sorting'
|
import { useTable, columnFilterRowsFn, createTable, sortRowsFn } from '@tanstack/react-table'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import { groupBy } from 'lodash-es'
|
import { groupBy } from 'lodash-es'
|
||||||
import { useMemo, useLayoutEffect, useRef, useState, useEffect } from 'react'
|
import { useMemo, useLayoutEffect, useRef, useState, useEffect } from 'react'
|
||||||
import { useLatest, useScroll } from 'react-use'
|
|
||||||
|
|
||||||
import { Header, Checkbox, Modal, Icon, Drawer, Card, Button } from '@components'
|
import { Header, Checkbox, Modal, Icon, Drawer, Card, Button } from '@components'
|
||||||
import { fromNow } from '@lib/date'
|
import { fromNow } from '@lib/date'
|
||||||
@ -47,7 +46,7 @@ function formatSpeed (upload: number, download: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = createTable().RowType<FormatConnection>()
|
const table = createTable<FormatConnection>()
|
||||||
|
|
||||||
export default function Connections () {
|
export default function Connections () {
|
||||||
const { translation, lang } = useI18n()
|
const { translation, lang } = useI18n()
|
||||||
@ -95,8 +94,8 @@ export default function Connections () {
|
|||||||
}, [connections])
|
}, [connections])
|
||||||
|
|
||||||
// table
|
// table
|
||||||
const tableRef = useRef<HTMLDivElement>(null)
|
const pinRef = useRef<HTMLTableCellElement>(null)
|
||||||
const { x: scrollX } = useScroll(tableRef)
|
const intersection = useIntersectionObserver(pinRef, { threshold: [1] })
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => table.createColumns([
|
() => table.createColumns([
|
||||||
table.createDataColumn(Columns.Host, { minWidth: 260, width: 260, header: t(`columns.${Columns.Host}`) }),
|
table.createDataColumn(Columns.Host, { minWidth: 260, width: 260, header: t(`columns.${Columns.Host}`) }),
|
||||||
@ -113,28 +112,28 @@ export default function Connections () {
|
|||||||
width: 200,
|
width: 200,
|
||||||
sortDescFirst: true,
|
sortDescFirst: true,
|
||||||
sortType (rowA, rowB) {
|
sortType (rowA, rowB) {
|
||||||
const speedA = rowA.original.speed
|
const speedA = rowA.original?.speed ?? { upload: 0, download: 0 }
|
||||||
const speedB = rowB.original.speed
|
const speedB = rowB.original?.speed ?? { upload: 0, download: 0 }
|
||||||
return speedA.download === speedB.download
|
return speedA.download === speedB.download
|
||||||
? speedA.upload - speedB.upload
|
? speedA.upload - speedB.upload
|
||||||
: speedA.download - speedB.download
|
: speedA.download - speedB.download
|
||||||
},
|
},
|
||||||
cell: cell => formatSpeed(cell.value[0], cell.value[1]),
|
cell: (cell: { value: [number, number] }) => formatSpeed(cell.value[0], cell.value[1]),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
table.createDataColumn(Columns.Upload, { minWidth: 100, width: 100, header: t(`columns.${Columns.Upload}`), cell: cell => formatTraffic(cell.value) }),
|
table.createDataColumn(Columns.Upload, { minWidth: 100, width: 100, header: t(`columns.${Columns.Upload}`), cell: cell => formatTraffic(cell.value as number) }),
|
||||||
table.createDataColumn(Columns.Download, { minWidth: 100, width: 100, header: t(`columns.${Columns.Download}`), cell: cell => formatTraffic(cell.value) }),
|
table.createDataColumn(Columns.Download, { minWidth: 100, width: 100, header: t(`columns.${Columns.Download}`), cell: cell => formatTraffic(cell.value as number) }),
|
||||||
table.createDataColumn(Columns.SourceIP, { minWidth: 140, width: 140, header: t(`columns.${Columns.SourceIP}`), filterType: 'equals' }),
|
table.createDataColumn(Columns.SourceIP, { minWidth: 140, width: 140, header: t(`columns.${Columns.SourceIP}`), filterType: 'equals' }),
|
||||||
table.createDataColumn(
|
table.createDataColumn(
|
||||||
Columns.Time,
|
Columns.Time,
|
||||||
{
|
{
|
||||||
minWidth:
|
minWidth: 120,
|
||||||
120,
|
|
||||||
width: 120,
|
width: 120,
|
||||||
header: t(`columns.${Columns.Time}`),
|
header: t(`columns.${Columns.Time}`),
|
||||||
cell: cell => fromNow(new Date(cell.value), lang),
|
cell: cell => fromNow(new Date(cell.value as string), lang),
|
||||||
sortType: (rowA, rowB) => rowB.original.time - rowA.original.time,
|
sortType: (rowA, rowB) => (rowB.original as FormatConnection).time - (rowA.original as FormatConnection).time,
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
[lang, t],
|
[lang, t],
|
||||||
)
|
)
|
||||||
@ -158,7 +157,7 @@ export default function Connections () {
|
|||||||
}
|
}
|
||||||
}, [connStreamReader, feed, setTraffic])
|
}, [connStreamReader, feed, setTraffic])
|
||||||
|
|
||||||
const instance = table.useTable({
|
const instance = useTable(table, {
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
sortRowsFn,
|
sortRowsFn,
|
||||||
@ -191,7 +190,7 @@ export default function Connections () {
|
|||||||
setDrawerState(d => { d.connection.completed = true })
|
setDrawerState(d => { d.connection.completed = true })
|
||||||
client.closeConnection(drawerState.selectedID)
|
client.closeConnection(drawerState.selectedID)
|
||||||
}
|
}
|
||||||
const latestConntion = useLatest(drawerState.connection)
|
const latestConntion = useSyncedRef(drawerState.connection)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const conn = data.find(c => c.id === drawerState.selectedID)?.original
|
const conn = data.find(c => c.id === drawerState.selectedID)?.original
|
||||||
if (conn) {
|
if (conn) {
|
||||||
@ -206,9 +205,9 @@ export default function Connections () {
|
|||||||
}
|
}
|
||||||
}, [data, drawerState.selectedID, latestConntion, setDrawerState])
|
}, [data, drawerState.selectedID, latestConntion, setDrawerState])
|
||||||
|
|
||||||
const scrolled = useMemo(() => scrollX > 0, [scrollX])
|
const scrolled = useMemo(() => (intersection?.intersectionRatio ?? 0) < 1, [intersection])
|
||||||
const headers = headerGroup.headers.map((header, idx) => {
|
const headers = headerGroup.headers.map((header, idx) => {
|
||||||
const column = header.column // as unknown as TableColumn<FormatConnection>
|
const column = header.column
|
||||||
const id = column.id
|
const id = column.id
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
@ -223,6 +222,7 @@ export default function Connections () {
|
|||||||
props.style.width = header.getWidth()
|
props.style.width = header.getWidth()
|
||||||
}),
|
}),
|
||||||
)}
|
)}
|
||||||
|
ref={column.id === Columns.Host ? pinRef : undefined}
|
||||||
key={id}>
|
key={id}>
|
||||||
<div {...column.getToggleSortingProps()}>
|
<div {...column.getToggleSortingProps()}>
|
||||||
{header.renderHeader()}
|
{header.renderHeader()}
|
||||||
@ -233,13 +233,13 @@ export default function Connections () {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{ idx !== headerGroup.headers.length - 1 &&
|
{ idx !== headerGroup.headers.length - 1 &&
|
||||||
<div {...column.getResizerProps()} className="connections-resizer" />
|
<div {...header.getResizerProps()} className="connections-resizer" />
|
||||||
}
|
}
|
||||||
</th>
|
</th>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const content = instance.getRows().map(row => {
|
const content = instance.getRowModel().rows.map(row => {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
{...row.getRowProps()}
|
{...row.getRowProps()}
|
||||||
@ -253,7 +253,7 @@ export default function Connections () {
|
|||||||
{ 'text-center': shouldCenter.has(cell.column.id), completed: row.original?.completed },
|
{ 'text-center': shouldCenter.has(cell.column.id), completed: row.original?.completed },
|
||||||
{
|
{
|
||||||
fixed: cell.column.id === Columns.Host,
|
fixed: cell.column.id === Columns.Host,
|
||||||
shadow: scrollX > 0 && cell.column.id === Columns.Host,
|
shadow: scrolled && cell.column.id === Columns.Host,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
@ -286,7 +286,7 @@ export default function Connections () {
|
|||||||
</Header>
|
</Header>
|
||||||
{ devices.length > 1 && <Devices devices={devices} selected={device} onChange={handleDeviceSelected} /> }
|
{ devices.length > 1 && <Devices devices={devices} selected={device} onChange={handleDeviceSelected} /> }
|
||||||
<Card ref={cardRef} className="connections-card relative">
|
<Card ref={cardRef} className="connections-card relative">
|
||||||
<div className="overflow-auto min-h-full" ref={tableRef}>
|
<div className="overflow-auto min-h-full">
|
||||||
<table {...instance.getTableProps()} className="flex-1">
|
<table {...instance.getTableProps()} className="flex-1">
|
||||||
<thead>
|
<thead>
|
||||||
<tr {...headerGroup.getHeaderGroupProps()} className="connections-header">
|
<tr {...headerGroup.getHeaderGroupProps()} className="connections-header">
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
&.fixed {
|
&.fixed {
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
left: 0;
|
left: -0.1px;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
&.shadow {
|
&.shadow {
|
||||||
box-shadow: inset -9px 0 8px -14px $color-black;
|
box-shadow: inset -9px 0 8px -14px $color-black;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Suspense } from 'react'
|
import { Suspense, StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { HashRouter } from 'react-router-dom'
|
import { HashRouter } from 'react-router-dom'
|
||||||
|
|
||||||
@ -9,11 +9,13 @@ import 'virtual:windi.css'
|
|||||||
export default function renderApp () {
|
export default function renderApp () {
|
||||||
const rootEl = document.getElementById('root')
|
const rootEl = document.getElementById('root')
|
||||||
const AppInstance = (
|
const AppInstance = (
|
||||||
<HashRouter>
|
<StrictMode>
|
||||||
<Suspense fallback={<Loading visible />}>
|
<HashRouter>
|
||||||
<App />
|
<Suspense fallback={<Loading visible />}>
|
||||||
</Suspense>
|
<App />
|
||||||
</HashRouter>
|
</Suspense>
|
||||||
|
</HashRouter>
|
||||||
|
</StrictMode>
|
||||||
)
|
)
|
||||||
|
|
||||||
const root = createRoot(rootEl!)
|
const root = createRoot(rootEl!)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { atom, useAtom, useAtomValue } from 'jotai'
|
import { atom, useAtom, useAtomValue } from 'jotai'
|
||||||
import { atomWithStorage } from 'jotai/utils'
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
import { useLocation } from 'react-use'
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||||
import { Client } from '@lib/request'
|
import { Client } from '@lib/request'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig, splitVendorChunkPlugin } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import windiCSS from 'vite-plugin-windicss'
|
import windiCSS from 'vite-plugin-windicss'
|
||||||
import tsConfigPath from 'vite-tsconfig-paths'
|
import tsConfigPath from 'vite-tsconfig-paths'
|
||||||
@ -24,6 +24,7 @@ export default defineConfig(
|
|||||||
name: 'Clash Dashboard',
|
name: 'Clash Dashboard',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
splitVendorChunkPlugin(),
|
||||||
],
|
],
|
||||||
base: './',
|
base: './',
|
||||||
css: {
|
css: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user