frontend: load/show first 100 entries to react faster to user navigation
Massimo Melina committed
Dec 19, 2021 at 21:45 UTC
dae82cd7929643e5e6f475f4591cb76b5b31a859
5 files changed
+66
-25
frontend/src/BrowseFiles.ts
+37
-13
@@ -1,34 +1,58 @@
1
import { Link, useLocation } from 'react-router-dom'
2
import { useApi } from './api'
3
-import { createElement as h, Fragment } from 'react'
4
-import { formatBytes, hError, hIcon, Loading } from './misc'
3
+import { createContext, createElement as h, Fragment, useContext, useEffect, useMemo, useState } from 'react'
4
+import { formatBytes, hError, hIcon, Loading, Spinner } from './misc'
5
import { Head } from './Head'
6
7
function usePath() {
8
return decodeURI(useLocation().pathname)
9
}
10
11
+export const ListContext = createContext<{ list:DirList, unfinished:boolean }>({ list:[], unfinished: false })
12
+
13
export function BrowseFiles() {
12
- const path = usePath()
13
- let res = useApi('file_list', { path })
14
- if (!res)
14
+ const [list, unfinished] = useFetchList()
15
+ if (!list)
16
return h(Loading)
16
- if (res instanceof Error)
17
- return hError(res)
18
- const { list } = res
19
- return h(Fragment, {},
20
- h(Head, { list }),
21
- h(FilesList, { list }))
17
+ if (list instanceof Error)
18
+ return hError(list)
19
+ return h(ListContext.Provider, { value:{ list, unfinished } },
20
+ h(Head),
21
+ h(FilesList))
22
+}
23
+
24
+function useFetchList() {
25
+ const desiredPath = usePath()
26
+ const PRELOAD_SIZE = 100
27
+ const [preloading, setPreloading] = useState(true)
28
+ const [path, setPath] = useState('')
29
+ useEffect(()=>{
30
+ setPreloading(true)
31
+ setPath(desiredPath)
32
+ }, [desiredPath])
33
+ const API = 'file_list'
34
+ const preload = useApi(path && API, { path, limit: PRELOAD_SIZE })
35
+ const rest = useApi(!preloading && API, { path, offset: PRELOAD_SIZE })
36
+ const list = useMemo(() => !preload ? null
37
+ : !rest ? (preload.list||preload) // the || is for an Error instance
38
+ : [...preload.list, ...rest.list],
39
+ [preload, rest])
40
+ const unfinished = preload && !rest && list.length === PRELOAD_SIZE
41
+ if (unfinished && preloading) // let load it all
42
+ setPreloading(false)
43
+ return [ list, unfinished ]
44
}
45
46
interface DirEntry { n:string, s?:number, m?:string, c?:string }
47
export type DirList = DirEntry[]
48
27
-function FilesList({ list }:{ list:DirList }) {
49
+function FilesList() {
50
+ const { list, unfinished } = useContext(ListContext)
51
return h('ul', { className: 'dir' },
52
!list.length ? 'Nothing here'
53
: list.map((entry: DirEntry) =>
31
- h(File, { key: entry.n, ...entry })))
54
+ h(File, { key: entry.n, ...entry })),
55
+ unfinished && h(Spinner))
56
}
57
58
function File({ n, m, c, s }: DirEntry) {
frontend/src/Head.ts
+8
-6
@@ -1,15 +1,15 @@
1
-import { createElement as h, Fragment, useMemo } from 'react'
1
+import { createElement as h, Fragment, useContext, useMemo } from 'react'
2
import { Link, useLocation } from 'react-router-dom'
3
-import { DirList } from './BrowseFiles'
3
+import { ListContext } from './BrowseFiles'
4
import { login, logout } from './login'
5
-import { formatBytes, hIcon, prefix } from './misc'
5
+import { formatBytes, hIcon, prefix, Spinner } from './misc'
6
import { useSnapState } from './state'
7
8
-export function Head({ list }:{ list:DirList }) {
8
+export function Head() {
9
return h(Fragment, {},
10
h(MenuPanel),
11
h(Breadcrumbs),
12
- h(FolderStats, { list })
12
+ h(FolderStats)
13
)
14
}
15
@@ -44,7 +44,8 @@ function MenuButton({ icon, label, onClick }:{ icon:string, label:string, onClic
44
h('label',{}, label))
45
}
46
47
-function FolderStats({ list }:{ list:DirList }) {
47
+function FolderStats() {
48
+ const { list, unfinished } = useContext(ListContext)
49
const stats = useMemo(() =>{
50
let files = 0, folders = 0, size = 0
51
for (const x of list) {
@@ -57,6 +58,7 @@ function FolderStats({ list }:{ list:DirList }) {
58
return { files, folders, size }
59
}, [list])
60
return h('div', { id:'folder-stats' },
61
+ unfinished && h(Spinner),
62
[
63
prefix('', stats.files,' file(s)'),
64
prefix('', stats.folders, ' folder(s)'),
frontend/src/api.ts
+5
-3
@@ -1,4 +1,5 @@
1
import { useEffect, useState } from 'react';
2
+import { Falsy } from './misc'
3
4
export function apiCall(cmd: string, params?: object) : Promise<any> {
5
return fetch('/~/api/'+cmd, {
@@ -14,11 +15,12 @@ export function apiCall(cmd: string, params?: object) : Promise<any> {
15
})
16
}
17
17
-export function useApi(cmd: string, params?: object) : any {
18
+export function useApi(cmd: string | Falsy, params?: object) : any {
19
const [x, setX] = useState()
20
useEffect(()=>{
21
setX(undefined)
21
- apiCall(cmd, params).then(setX, setX)
22
+ if (cmd)
23
+ apiCall(cmd, params).then(setX, setX)
24
}, [cmd, JSON.stringify(params)]) //eslint-disable-line
25
return x
24
-}
\ No newline at end of file
26
+}
frontend/src/index.scss
+5
@@ -37,6 +37,11 @@ button {
37
color: #833;
38
}
39
40
+@keyframes spin {
41
+ from { transform: rotate(0deg); }
42
+ to { transform: rotate(360deg); }
43
+}
44
+
45
#folder-path {
46
margin: 0.2em;
47
& a {
frontend/src/misc.ts
+11
-3
@@ -1,4 +1,6 @@
1
-import React, { createElement as h } from 'react'
1
+import { createElement as h } from 'react'
2
+
3
+export type Falsy = false | null | undefined | '' | 0
4
5
export function hIcon(name: string) {
6
return h(Icon, { name })
@@ -12,16 +14,22 @@ const SYS_ICONS: Record<string,string> = {
14
login: 'person',
15
user: 'account_circle',
16
file: 'description',
17
+ spinner: 'sports_baseball',
18
}
16
-export function Icon({ name }: { name:string }) {
19
+export function Icon({ name, ...props }: { name:string }) {
20
name = SYS_ICONS[name] || name
21
return h('span',{
22
className: 'material-icons-outlined icon',
23
+ ...props
24
}, name)
25
}
26
27
export function Loading() {
24
- return 'loading' as React.ReactNode as React.ReactElement
28
+ return h(Spinner)
29
+}
30
+
31
+export function Spinner() {
32
+ return h(Icon, { name:'spinner', style: { animation:'1s spin infinite' } })
33
}
34
35
export function formatBytes(n: number, post: string = 'B') {