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",