FilePicker: now faster on large folders (thousands)
Massimo Melina committed
Mar 17, 2022 at 20:18 UTC
095438eb3a1a0bfe44bd314fc39bbb368d0e8daa
3 files changed
+148
-31
admin/package.json
+8
-2
@@ -21,10 +21,12 @@
21
"@testing-library/react": "^12.1.2",
22
"@testing-library/user-event": "^13.5.0",
23
"js-sha512": "^0.8.0",
24
- "tssrp6a": "^3.0.0",
24
"react": "^17.0.2",
25
"react-dom": "^17.0.2",
26
"react-router-dom": "^6.2.1",
27
+ "react-virtualized-auto-sizer": "^1.0.6",
28
+ "react-window": "^1.8.6",
29
+ "tssrp6a": "^3.0.0",
30
"valtio": "^1.2.9",
31
"web-vitals": "^2.1.4"
32
},
@@ -33,6 +35,8 @@
35
"@types/node": "^16.11.21",
36
"@types/react": "^17.0.38",
37
"@types/react-dom": "^17.0.11",
38
+ "@types/react-window": "^1.8.5",
39
+ "@types/react-virtualized-auto-sizer": "^1.0.1",
40
"react-scripts": "5.0.0"
41
},
42
"eslintConfig": {
@@ -42,7 +46,9 @@
46
],
47
"overrides": [
48
{
45
- "files": ["*.ts"],
49
+ "files": [
50
+ "*.ts"
51
+ ],
52
"rules": {
53
"no-mixed-operators": "off"
54
}
admin/src/FilePicker.ts
+43
-29
@@ -11,7 +11,6 @@ import {
11
ListItemIcon,
12
ListItemText,
13
MenuItem,
14
- MenuList,
14
TextField,
15
Typography
16
} from '@mui/material'
@@ -19,6 +18,8 @@ import { enforceFinal, formatBytes, isWindowsDrive, spinner } from './misc'
18
import { ArrowUpward, Home } from '@mui/icons-material'
19
import { StringField } from './Form'
20
import { FileIcon, FolderIcon } from './VfsTree'
21
+import { FixedSizeList } from 'react-window'
22
+import AutoSizer from "react-virtualized-auto-sizer"
23
24
export interface DirEntry { n:string, s?:number, m?:string, c?:string, k?:'d' }
25
@@ -45,11 +46,12 @@ export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }
46
const re = new RegExp(_.escapeRegExp(filter), 'i')
47
return (v:string) => re.test(v)
48
}, [filter])
49
+
50
+ const filteredList = useMemo(() => list.filter(it => filterMatch(it.n)), [list,filterMatch])
51
if (loading)
52
return spinner()
53
const pathDelimiter = /[:\\]/.test(cwd) ? '\\' : '/'
54
const cwdPostfixed = enforceFinal(pathDelimiter, cwd)
52
- let displayed = 0
55
return h(Fragment, {},
56
h(Box, { display:'flex', gap: 1 },
57
h(Button, {
@@ -72,32 +74,44 @@ export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }
74
error ? h(Alert, { severity:'error' }, String(error))
75
: !list.length ? h(Typography, { p:1 }, 'No elements in this folder')
76
: h(Fragment, {},
75
- h(MenuList, { sx:{ overflow:'auto', flex: 1 } },
76
- list.map((it:DirEntry) =>
77
- h(MenuItem, {
78
- key: it.n,
79
- sx: { display: filterMatch(it.n) ? (displayed++,undefined) : 'none' },
80
- onClick(){
81
- if (it.k === 'd')
82
- setCwd( cwdPostfixed + it.n )
83
- else
84
- onSelect([ cwdPostfixed + it.n ])
85
- }
86
- },
87
- h(Checkbox, {
88
- checked: sel.includes(it.n),
89
- onClick(ev){
90
- const id = it.n
91
- const removed = sel.filter(x => x !== id)
92
- setSel(removed.length < sel.length ? removed : [...sel, id])
93
- ev.stopPropagation()
94
- },
95
- }),
96
- h(ListItemIcon, {}, h(it.k ? FolderIcon : FileIcon)),
97
- h(ListItemText, {}, it.n),
98
- it.k !== 'd' && it.s !== undefined && h(Typography, { variant:'body2', color:'text.secondary', ml:4 }, formatBytes(it.s) )
99
- )
100
- )
77
+ h(Box, { sx: { flex: 1 } },
78
+ h(AutoSizer, {
79
+ children: size =>
80
+ h(FixedSizeList, {
81
+ ...size, itemSize: 46, itemCount: filteredList.length, overscanCount: 5,
82
+ /*sx:{ overflow:'auto', flex: 1 }*/
83
+ children({ index, style }) {
84
+ const it: DirEntry = filteredList[index]
85
+ return h(MenuItem, {
86
+ style,
87
+ key: it.n,
88
+ onClick() {
89
+ if (it.k === 'd')
90
+ setCwd(cwdPostfixed + it.n)
91
+ else
92
+ onSelect([cwdPostfixed + it.n])
93
+ }
94
+ },
95
+ h(Checkbox, {
96
+ checked: sel.includes(it.n),
97
+ onClick(ev) {
98
+ const id = it.n
99
+ const removed = sel.filter(x => x !== id)
100
+ setSel(removed.length < sel.length ? removed : [...sel, id])
101
+ ev.stopPropagation()
102
+ },
103
+ }),
104
+ h(ListItemIcon, {}, h(it.k ? FolderIcon : FileIcon)),
105
+ h(ListItemText, { sx: { whiteSpace: 'pre-wrap', wordBreak: 'break-all' } }, it.n),
106
+ it.k !== 'd' && it.s !== undefined && h(Typography, {
107
+ variant: 'body2',
108
+ color: 'text.secondary',
109
+ ml: 4
110
+ }, formatBytes(it.s))
111
+ )
112
+ }
113
+ })
114
+ }),
115
),
116
h(Box, { display:'flex', gap: 1 },
117
h(Button, {
@@ -110,7 +124,7 @@ export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }
124
}, `Select (${sel.length})`),
125
h(TextField, {
126
value: filter,
113
- label: `Filter results (${displayed}${displayed < list.length ? '/'+list.length : ''})`,
127
+ label: `Filter results (${filteredList.length}${filteredList.length < list.length ? '/'+list.length : ''})`,
128
onChange(ev) {
129
setFilterBounced(ev.target.value)
130
},
package-lock.json
+97
@@ -38,6 +38,8 @@
38
"react": "^17.0.2",
39
"react-dom": "^17.0.2",
40
"react-router-dom": "^6.2.1",
41
+ "react-virtualized-auto-sizer": "^1.0.6",
42
+ "react-window": "^1.8.6",
43
"tssrp6a": "^3.0.0",
44
"valtio": "^1.2.9",
45
"web-vitals": "^2.1.4"
@@ -47,6 +49,8 @@
49
"@types/node": "^16.11.21",
50
"@types/react": "^17.0.38",
51
"@types/react-dom": "^17.0.11",
52
+ "@types/react-virtualized-auto-sizer": "^1.0.1",
53
+ "@types/react-window": "^1.8.5",
54
"react-scripts": "5.0.0"
55
}
56
},
@@ -4872,6 +4876,24 @@
4876
"@types/react": "*"
4877
}
4878
},
4879
+ "node_modules/@types/react-virtualized-auto-sizer": {
4880
+ "version": "1.0.1",
4881
+ "resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz",
4882
+ "integrity": "sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==",
4883
+ "dev": true,
4884
+ "dependencies": {
4885
+ "@types/react": "*"
4886
+ }
4887
+ },
4888
+ "node_modules/@types/react-window": {
4889
+ "version": "1.8.5",
4890
+ "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
4891
+ "integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==",
4892
+ "dev": true,
4893
+ "dependencies": {
4894
+ "@types/react": "*"
4895
+ }
4896
+ },
4897
"node_modules/@types/resolve": {
4898
"version": "1.17.1",
4899
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -13607,6 +13629,11 @@
13629
"node": ">= 4.0.0"
13630
}
13631
},
13632
+ "node_modules/memoize-one": {
13633
+ "version": "5.2.1",
13634
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
13635
+ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
13636
+ },
13637
"node_modules/meow": {
13638
"version": "9.0.0",
13639
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
@@ -16702,6 +16729,34 @@
16729
"react-dom": ">=16.6.0"
16730
}
16731
},
16732
+ "node_modules/react-virtualized-auto-sizer": {
16733
+ "version": "1.0.6",
16734
+ "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz",
16735
+ "integrity": "sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ==",
16736
+ "engines": {
16737
+ "node": ">8.0.0"
16738
+ },
16739
+ "peerDependencies": {
16740
+ "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0",
16741
+ "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0"
16742
+ }
16743
+ },
16744
+ "node_modules/react-window": {
16745
+ "version": "1.8.6",
16746
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
16747
+ "integrity": "sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==",
16748
+ "dependencies": {
16749
+ "@babel/runtime": "^7.0.0",
16750
+ "memoize-one": ">=3.1.1 <6"
16751
+ },
16752
+ "engines": {
16753
+ "node": ">8.0.0"
16754
+ },
16755
+ "peerDependencies": {
16756
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0",
16757
+ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0"
16758
+ }
16759
+ },
16760
"node_modules/read-pkg": {
16761
"version": "5.2.0",
16762
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -22081,11 +22136,15 @@
22136
"@types/node": "^16.11.21",
22137
"@types/react": "^17.0.38",
22138
"@types/react-dom": "^17.0.11",
22139
+ "@types/react-virtualized-auto-sizer": "^1.0.1",
22140
+ "@types/react-window": "^1.8.5",
22141
"js-sha512": "^0.8.0",
22142
"react": "^17.0.2",
22143
"react-dom": "^17.0.2",
22144
"react-router-dom": "^6.2.1",
22145
"react-scripts": "5.0.0",
22146
+ "react-virtualized-auto-sizer": "^1.0.6",
22147
+ "react-window": "^1.8.6",
22148
"tssrp6a": "^3.0.0",
22149
"valtio": "^1.2.9",
22150
"web-vitals": "^2.1.4"
@@ -23854,6 +23913,24 @@
23913
"@types/react": "*"
23914
}
23915
},
23916
+ "@types/react-virtualized-auto-sizer": {
23917
+ "version": "1.0.1",
23918
+ "resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz",
23919
+ "integrity": "sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==",
23920
+ "dev": true,
23921
+ "requires": {
23922
+ "@types/react": "*"
23923
+ }
23924
+ },
23925
+ "@types/react-window": {
23926
+ "version": "1.8.5",
23927
+ "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
23928
+ "integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==",
23929
+ "dev": true,
23930
+ "requires": {
23931
+ "@types/react": "*"
23932
+ }
23933
+ },
23934
"@types/resolve": {
23935
"version": "1.17.1",
23936
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -30563,6 +30640,11 @@
30640
"fs-monkey": "1.0.3"
30641
}
30642
},
30643
+ "memoize-one": {
30644
+ "version": "5.2.1",
30645
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
30646
+ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
30647
+ },
30648
"meow": {
30649
"version": "9.0.0",
30650
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
@@ -32741,6 +32823,21 @@
32823
"prop-types": "^15.6.2"
32824
}
32825
},
32826
+ "react-virtualized-auto-sizer": {
32827
+ "version": "1.0.6",
32828
+ "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz",
32829
+ "integrity": "sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ==",
32830
+ "requires": {}
32831
+ },
32832
+ "react-window": {
32833
+ "version": "1.8.6",
32834
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
32835
+ "integrity": "sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==",
32836
+ "requires": {
32837
+ "@babel/runtime": "^7.0.0",
32838
+ "memoize-one": ">=3.1.1 <6"
32839
+ }
32840
+ },
32841
"read-pkg": {
32842
"version": "5.2.0",
32843
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",