hfs 0.26.8 → 0.26.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/admin/assets/index.bb5198ec.js +281 -0
  2. package/admin/assets/index.dcc78777.css +1 -0
  3. package/admin/assets/sha512.9dfe82e1.js +8 -0
  4. package/admin/index.html +3 -1
  5. package/admin/{public/logo.svg → logo.svg} +0 -0
  6. package/frontend/assets/index.27a78796.js +85 -0
  7. package/frontend/assets/index.93366732.css +1 -0
  8. package/frontend/assets/sha512.6af42937.js +8 -0
  9. package/frontend/{public/fontello.css → fontello.css} +0 -0
  10. package/frontend/{public/fontello.woff2 → fontello.woff2} +0 -0
  11. package/frontend/index.html +3 -1
  12. package/package.json +1 -1
  13. package/src/QuickZipStream.js +285 -0
  14. package/src/ThrottledStream.js +93 -0
  15. package/src/adminApis.js +169 -0
  16. package/src/api.accounts.js +59 -0
  17. package/src/api.auth.js +128 -0
  18. package/src/api.file_list.js +103 -0
  19. package/src/api.helpers.js +32 -0
  20. package/src/api.monitor.js +102 -0
  21. package/src/api.plugins.js +127 -0
  22. package/src/api.vfs.js +164 -0
  23. package/src/apiMiddleware.js +120 -0
  24. package/src/block.js +33 -0
  25. package/src/commands.js +124 -0
  26. package/src/config.js +168 -0
  27. package/src/connections.js +57 -0
  28. package/src/const.js +83 -0
  29. package/src/crypt.js +21 -0
  30. package/src/debounceAsync.js +48 -0
  31. package/src/events.js +9 -0
  32. package/src/frontEndApis.js +38 -0
  33. package/src/github.js +102 -0
  34. package/src/index.js +56 -0
  35. package/src/listen.js +235 -0
  36. package/src/log.js +137 -0
  37. package/src/middlewares.js +175 -0
  38. package/src/misc.js +160 -0
  39. package/src/pbkdf2.js +74 -0
  40. package/src/perm.js +181 -0
  41. package/src/plugins.js +343 -0
  42. package/src/serveFile.js +105 -0
  43. package/src/serveGuiFiles.js +113 -0
  44. package/src/sse.js +29 -0
  45. package/src/throttler.js +91 -0
  46. package/src/update.js +69 -0
  47. package/src/util-files.js +148 -0
  48. package/src/util-generators.js +30 -0
  49. package/src/util-http.js +30 -0
  50. package/src/vfs.js +230 -0
  51. package/src/watchLoad.js +73 -0
  52. package/src/zip.js +72 -0
  53. package/admin/.DS_Store +0 -0
  54. package/admin/.eslintrc +0 -8
  55. package/admin/.gitignore +0 -23
  56. package/admin/package.json +0 -67
  57. package/admin/src/AccountForm.ts +0 -92
  58. package/admin/src/AccountsPage.ts +0 -143
  59. package/admin/src/App.ts +0 -83
  60. package/admin/src/ArrayField.ts +0 -84
  61. package/admin/src/ConfigPage.ts +0 -279
  62. package/admin/src/FileField.ts +0 -52
  63. package/admin/src/FileForm.ts +0 -148
  64. package/admin/src/FilePicker.ts +0 -166
  65. package/admin/src/HomePage.ts +0 -96
  66. package/admin/src/InstalledPlugins.ts +0 -158
  67. package/admin/src/LoginRequired.ts +0 -75
  68. package/admin/src/LogoutPage.ts +0 -27
  69. package/admin/src/LogsPage.ts +0 -75
  70. package/admin/src/MainMenu.ts +0 -74
  71. package/admin/src/MenuButton.ts +0 -38
  72. package/admin/src/MonitorPage.ts +0 -200
  73. package/admin/src/OnlinePlugins.ts +0 -101
  74. package/admin/src/PermField.ts +0 -80
  75. package/admin/src/PluginsPage.ts +0 -27
  76. package/admin/src/VfsMenuBar.ts +0 -58
  77. package/admin/src/VfsPage.ts +0 -124
  78. package/admin/src/VfsTree.ts +0 -95
  79. package/admin/src/addFiles.ts +0 -59
  80. package/admin/src/api.ts +0 -246
  81. package/admin/src/dialog.ts +0 -203
  82. package/admin/src/index.css +0 -21
  83. package/admin/src/index.ts +0 -10
  84. package/admin/src/md.ts +0 -31
  85. package/admin/src/misc.ts +0 -141
  86. package/admin/src/react-app-env.d.ts +0 -1
  87. package/admin/src/reportWebVitals.ts +0 -15
  88. package/admin/src/setupTests.ts +0 -5
  89. package/admin/src/state.ts +0 -40
  90. package/admin/src/theme.ts +0 -37
  91. package/admin/tsconfig.json +0 -26
  92. package/admin/vite.config.ts +0 -32
  93. package/frontend/.DS_Store +0 -0
  94. package/frontend/.eslintrc +0 -8
  95. package/frontend/.gitignore +0 -23
  96. package/frontend/package.json +0 -51
  97. package/frontend/src/App.ts +0 -25
  98. package/frontend/src/Breadcrumbs.ts +0 -43
  99. package/frontend/src/BrowseFiles.ts +0 -141
  100. package/frontend/src/Head.ts +0 -45
  101. package/frontend/src/UserPanel.ts +0 -52
  102. package/frontend/src/api.ts +0 -78
  103. package/frontend/src/components.ts +0 -54
  104. package/frontend/src/dialog.css +0 -76
  105. package/frontend/src/dialog.ts +0 -105
  106. package/frontend/src/icons.ts +0 -46
  107. package/frontend/src/index.scss +0 -307
  108. package/frontend/src/index.ts +0 -10
  109. package/frontend/src/login.ts +0 -50
  110. package/frontend/src/menu.ts +0 -188
  111. package/frontend/src/misc.ts +0 -54
  112. package/frontend/src/options.ts +0 -52
  113. package/frontend/src/react-app-env.d.ts +0 -1
  114. package/frontend/src/reportWebVitals.ts +0 -15
  115. package/frontend/src/setupTests.ts +0 -5
  116. package/frontend/src/state.ts +0 -82
  117. package/frontend/src/useAuthorized.ts +0 -17
  118. package/frontend/src/useFetchList.ts +0 -144
  119. package/frontend/src/useTheme.ts +0 -23
  120. package/frontend/tsconfig.json +0 -26
  121. package/frontend/vite.config.ts +0 -21
  122. package/src/QuickZipStream.ts +0 -279
  123. package/src/ThrottledStream.ts +0 -98
  124. package/src/adminApis.ts +0 -161
  125. package/src/api.accounts.ts +0 -78
  126. package/src/api.auth.ts +0 -131
  127. package/src/api.file_list.ts +0 -102
  128. package/src/api.helpers.ts +0 -30
  129. package/src/api.monitor.ts +0 -106
  130. package/src/api.plugins.ts +0 -139
  131. package/src/api.vfs.ts +0 -182
  132. package/src/apiMiddleware.ts +0 -124
  133. package/src/block.ts +0 -35
  134. package/src/commands.ts +0 -122
  135. package/src/config.ts +0 -166
  136. package/src/connections.ts +0 -60
  137. package/src/const.ts +0 -57
  138. package/src/crypt.ts +0 -16
  139. package/src/debounceAsync.ts +0 -51
  140. package/src/events.ts +0 -6
  141. package/src/frontEndApis.ts +0 -17
  142. package/src/github.ts +0 -102
  143. package/src/index.ts +0 -53
  144. package/src/listen.ts +0 -220
  145. package/src/log.ts +0 -128
  146. package/src/middlewares.ts +0 -176
  147. package/src/misc.ts +0 -149
  148. package/src/pbkdf2.ts +0 -83
  149. package/src/perm.ts +0 -194
  150. package/src/plugins.ts +0 -342
  151. package/src/serveFile.ts +0 -104
  152. package/src/serveGuiFiles.ts +0 -95
  153. package/src/sse.ts +0 -29
  154. package/src/throttler.ts +0 -106
  155. package/src/update.ts +0 -67
  156. package/src/util-files.ts +0 -137
  157. package/src/util-generators.ts +0 -29
  158. package/src/util-http.ts +0 -29
  159. package/src/vfs.ts +0 -258
  160. package/src/watchLoad.ts +0 -75
  161. package/src/zip.ts +0 -69
@@ -1,124 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { createElement as h, useEffect, useMemo, useState } from 'react'
4
- import { useApi, useApiEx } from './api'
5
- import { Alert, Grid, Link, List, ListItem, ListItemText, Typography } from '@mui/material'
6
- import { state, useSnapState } from './state'
7
- import VfsMenuBar from './VfsMenuBar'
8
- import VfsTree from './VfsTree'
9
- import { onlyTruthy, prefix } from './misc'
10
- import { reactJoin } from '@hfs/shared'
11
- import _ from 'lodash'
12
- import { AlertProps } from '@mui/material/Alert/Alert'
13
- import FileForm from './FileForm'
14
-
15
- let selectOnReload: string[] | undefined
16
-
17
- export default function VfsPage() {
18
- const [id2node] = useState(() => new Map<string, VfsNode>())
19
- const snap = useSnapState()
20
- const { data, reload, element } = useApiEx('get_vfs')
21
- useMemo(() => snap.vfs || reload(), [snap.vfs, reload])
22
- useEffect(() => {
23
- state.vfs = undefined
24
- if (!data) return
25
- // rebuild id2node
26
- id2node.clear()
27
- const { root } = data
28
- if (!root) return
29
- recur(root) // this must be done before state change that would cause Tree to render and expecting id2node
30
- root.isRoot = true
31
- state.vfs = root
32
- // refresh objects of selectedFiles
33
- const ids = selectOnReload || state.selectedFiles.map(x => x.id)
34
- selectOnReload = undefined
35
- state.selectedFiles = onlyTruthy(ids.map(id =>
36
- id2node.get(id)))
37
-
38
- // calculate id and parent fields, and builds the map id2node
39
- function recur(node: VfsNode, pre='', parent: VfsNode|undefined=undefined) {
40
- node.parent = parent
41
- node.id = prefix(pre, node.name) || '/' // root
42
- id2node.set(node.id, node)
43
- if (!node.children) return
44
- for (const n of node.children)
45
- recur(n, (pre && node.id) + '/', node)
46
- }
47
-
48
- }, [data, id2node])
49
- const [status] = useApi(window.location.host === 'localhost' && 'get_status')
50
- const urls = useMemo(() =>
51
- typeof status === 'object'
52
- && _.sortBy(
53
- onlyTruthy(Object.values(status.urls?.https || status.urls?.http || {}).map(u => typeof u === 'string' && u)),
54
- url => url.includes('[')
55
- ),
56
- [status])
57
- if (element) {
58
- id2node.clear()
59
- return element
60
- }
61
- const anythingShared = !data?.root?.children?.length && !data?.root?.source
62
- const alert: AlertProps | false = anythingShared ? {
63
- severity: 'warning',
64
- children: "Add something to your shared files — click Add"
65
- } : urls && {
66
- severity: 'info',
67
- children: [
68
- "Your shared files can be browsed from ",
69
- reactJoin(" or ", urls.slice(0,3).map(href => h(Link, { href }, href)))
70
- ]
71
- }
72
- return h(Grid, { container:true, rowSpacing: 1, maxWidth: '80em', columnSpacing: 2 },
73
- alert && h(Grid, { item: true, mb: 2, xs: 12 }, h(Alert, alert)),
74
- h(Grid, { item:true, sm: 6, lg: 5 },
75
- h(Typography, { variant: 'h6', mb:1, }, "Virtual File System"),
76
- h(VfsMenuBar),
77
- snap.vfs && h(VfsTree, { id2node })),
78
- h(Grid, { item:true, sm: 6, lg: 7, maxWidth:'100%' },
79
- h(SidePanel))
80
- )
81
- }
82
-
83
- function SidePanel() {
84
- const { selectedFiles: files } = useSnapState()
85
- return files.length === 0 ? null
86
- : files.length === 1 ? h(FileForm, { file: files[0] as VfsNode }) // it's actually Snapshot<VfsNode> but it's easier this way
87
- : h(List, {},
88
- files.length + ' selected',
89
- files.map(f => h(ListItem, { key: f.name },
90
- h(ListItemText, { primary: f.name, secondary: f.source }) )))
91
- }
92
-
93
- export function reloadVfs(pleaseSelect?: string[]) {
94
- selectOnReload = pleaseSelect
95
- state.vfs = undefined
96
- }
97
-
98
- export type VfsNode = {
99
- id: string
100
- name: string
101
- type?: 'folder'
102
- source?: string
103
- size?: number
104
- ctime?: string
105
- mtime?: string
106
- default?: string
107
- children?: VfsNode[]
108
- parent?: VfsNode
109
- can_see: Who
110
- can_read: Who
111
- website?: true
112
- masks?: any
113
-
114
- isRoot?: true
115
- }
116
-
117
- const WHO_ANYONE = true
118
- const WHO_NO_ONE = false
119
- const WHO_ANY_ACCOUNT = '*'
120
- type AccountList = string[]
121
- export type Who = typeof WHO_ANYONE
122
- | typeof WHO_NO_ONE
123
- | typeof WHO_ANY_ACCOUNT
124
- | AccountList
@@ -1,95 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { state, useSnapState } from './state'
4
- import { createElement as h, ReactElement, useState } from 'react'
5
- import { TreeItem, TreeView } from '@mui/lab'
6
- import {
7
- ChevronRight,
8
- ExpandMore,
9
- TheaterComedy,
10
- Folder,
11
- Home,
12
- InsertDriveFileOutlined,
13
- Lock,
14
- RemoveRedEye,
15
- Web
16
- } from '@mui/icons-material'
17
- import { Box } from '@mui/material'
18
- import { VfsNode, Who } from './VfsPage'
19
- import { iconTooltip, isWindowsDrive, onlyTruthy } from './misc'
20
-
21
- export const FolderIcon = Folder
22
- export const FileIcon = InsertDriveFileOutlined
23
-
24
- export default function VfsTree({ id2node }:{ id2node: Map<string, VfsNode> }) {
25
- const { vfs, selectedFiles } = useSnapState()
26
- const [selected, setSelected] = useState<string[]>(selectedFiles.map(x => x.id)) // try to restore selection after reload
27
- const [expanded, setExpanded] = useState(Array.from(id2node.keys()))
28
- if (!vfs)
29
- return null
30
- return h(TreeView, {
31
- expanded,
32
- selected,
33
- multiSelect: true,
34
- sx: { overflowX: 'auto' },
35
- onNodeSelect(ev, ids) {
36
- setSelected(ids)
37
- state.selectedFiles = onlyTruthy(ids.map(id => id2node.get(id)))
38
- }
39
- }, recur(vfs as Readonly<VfsNode>))
40
-
41
- function isRestricted(who: Who) {
42
- return who !== undefined && who !== true
43
- }
44
-
45
- function recur(node: Readonly<VfsNode>): ReactElement {
46
- let { id, name, source, isRoot } = node
47
- if (!id)
48
- debugger
49
- const folder = node.type === 'folder'
50
- if (folder && !isWindowsDrive(source) && source === name) // we need a way to show that the name we are displaying is a source in this ambiguous case, so we add a redundant ./
51
- source = './' + source
52
- return h(TreeItem, {
53
- label: h(Box, {
54
- sx: {
55
- display: 'flex',
56
- gap: '.5em',
57
- lineHeight: '2em',
58
- alignItems: 'center',
59
- }
60
- },
61
- isRoot ? iconTooltip(Home, "home, or root if you like")
62
- : folder ? iconTooltip(FolderIcon, "Folder")
63
- : iconTooltip(FileIcon, "File"),
64
- isRestricted(node.can_see) && iconTooltip(RemoveRedEye, "Restrictions on who can see"),
65
- isRestricted(node.can_read) && iconTooltip(Lock, "Restrictions on who can download"),
66
- node.default && iconTooltip(Web, "Act as website"),
67
- node.masks && iconTooltip(TheaterComedy, "Masks"),
68
- isRoot ? "Home"
69
- // special rendering if the whole source is not too long, and the name was not customized
70
- : source?.length! < 45 && source?.endsWith(name) ? h('span', {},
71
- h('span', { style: { opacity: .4 } }, source.slice(0,-name.length)),
72
- h('span', {}, source.slice(-name.length)),
73
- )
74
- : name
75
- ),
76
- key: name,
77
- collapseIcon: h(ExpandMore, {
78
- onClick(ev) {
79
- setExpanded( expanded.filter(x => x !== id) )
80
- ev.preventDefault()
81
- ev.stopPropagation()
82
- }
83
- }),
84
- expandIcon: h(ChevronRight, {
85
- onClick(ev) {
86
- setExpanded( [...expanded, id] )
87
- ev.preventDefault()
88
- ev.stopPropagation()
89
- }
90
- }),
91
- nodeId: id
92
- }, isRoot && !node.children?.length ? h('i', {}, "nothing here") : node.children?.map(recur))
93
- }
94
-
95
- }
@@ -1,59 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { alertDialog, newDialog, promptDialog } from './dialog'
4
- import { createElement as h, Fragment } from 'react'
5
- import { Box } from '@mui/material'
6
- import { VfsNode, reloadVfs } from './VfsPage'
7
- import { state } from './state'
8
- import { apiCall } from './api'
9
- import FilePicker from './FilePicker'
10
- import { onlyTruthy } from './misc'
11
-
12
- export default function addFiles() {
13
- const close = newDialog({
14
- title: "Add files or folders",
15
- dialogProps: { sx:{ minWidth: 'min(90vw, 40em)', minHeight: 'calc(100vh - 9em)' } },
16
- Content() {
17
- const under = getUnder()
18
- return h(Fragment, {},
19
- h(Box, { sx:{ typography: 'body1', px: 1, py: 2 } },
20
- "Selected elements will be added to virtual path " + (under || '(home)')),
21
- h(FilePicker, {
22
- async onSelect(sel) {
23
- let failed = await Promise.all(sel.map(source =>
24
- apiCall('add_vfs', { under, source }).then(() => '', () => source) ))
25
- failed = onlyTruthy(failed)
26
- if (failed.length)
27
- await alertDialog("Some elements have been rejected: "+failed.join(', '), 'error')
28
- reloadVfs()
29
- close()
30
- }
31
- })
32
- )
33
- }
34
- })
35
- }
36
-
37
- export async function addVirtual() {
38
- try {
39
- const name = await promptDialog("Enter folder name")
40
- if (!name) return
41
- const under = getUnder()
42
- await apiCall('add_vfs', { under, name })
43
- reloadVfs([ (under||'') + '/' + name ])
44
- await alertDialog(`Folder "${name}" created`, 'success')
45
- }
46
- catch(e) {
47
- await alertDialog(e as Error)
48
- }
49
- }
50
-
51
- function getUnder() {
52
- let f: VfsNode | undefined = state.selectedFiles[0]
53
- if (!f)
54
- return ''
55
- if (f.type !== 'folder')
56
- f = f.parent
57
- const { id } = f!
58
- return id === '/' ? '' : id
59
- }
package/admin/src/api.ts DELETED
@@ -1,246 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { createElement as h, useCallback, useEffect, useMemo, useRef } from 'react'
4
- import { Dict, err2msg, Falsy, getCookie, IconBtn, spinner, useStateMounted, wantArray } from './misc'
5
- import { Alert } from '@mui/material'
6
- import _ from 'lodash'
7
- import { state } from './state'
8
- import { Refresh } from '@mui/icons-material'
9
- import produce, { Draft } from 'immer'
10
-
11
- export function useApiEx<T=any>(...args: Parameters<typeof useApi>) {
12
- const [data, error, reload] = useApi<T>(...args)
13
- const cmd = args[0]
14
- const loading = data === undefined
15
- const element = useMemo(() =>
16
- !cmd ? null
17
- : error ? h(Alert, { severity: 'error' }, String(error), h(IconBtn, { icon: Refresh, onClick: reload, sx: { m:'-8px 0 -8px 16px' } }))
18
- : loading ? spinner()
19
- : null,
20
- [error, cmd, loading, reload])
21
- return { data, error, reload, loading, element }
22
- }
23
-
24
- const PREFIX = '/~/api/'
25
-
26
- const timeoutByApi: Dict = {
27
- get_status: 20 // can be lengthy on slow machines because of the find-process-on-busy-port feature
28
- }
29
- export function apiCall(cmd: string, params?: Dict, { timeout=undefined }={}) : Promise<any> {
30
- const csrf = getCsrf()
31
- if (csrf)
32
- params = { csrf, ...params }
33
-
34
- const controller = new AbortController()
35
- if (timeout !== false)
36
- setTimeout(() => controller.abort('timeout'), 1000*(timeoutByApi[cmd] ?? timeout ?? 10))
37
- return fetch(PREFIX+cmd, {
38
- method: 'POST',
39
- headers: { 'content-type': 'application/json' },
40
- signal: controller.signal,
41
- body: params && JSON.stringify(params),
42
- }).then(async res => {
43
- if (res.ok)
44
- return res.json().then(json => {
45
- console.debug('API', cmd, params, '>>', json)
46
- return json
47
- })
48
- const msg = await res.text() || 'Failed API ' + cmd
49
- console.warn(msg + (params ? ' ' + JSON.stringify(params) : ''))
50
- if (res.status === 401)
51
- state.loginRequired = true
52
- throw new ApiError(res.status, msg)
53
- }, err => {
54
- if (err?.message?.includes('fetch'))
55
- throw Error("Network error")
56
- throw err
57
- })
58
- }
59
-
60
- export class ApiError extends Error {
61
- constructor(readonly code:number, message: string) {
62
- super(message);
63
- }
64
- }
65
-
66
- export function useApi<T=any>(cmd: string | Falsy, params?: object) : [T | undefined, undefined | Error, ()=>void] {
67
- const [ret, setRet] = useStateMounted<T | undefined>(undefined)
68
- const [err, setErr] = useStateMounted<Error | undefined>(undefined)
69
- const [forcer, setForcer] = useStateMounted(0)
70
- const loadingRef = useRef(false)
71
- useEffect(()=>{
72
- setRet(undefined)
73
- setErr(undefined)
74
- if (!cmd) return
75
- loadingRef.current = true
76
- apiCall(cmd, params)
77
- .then(setRet, setErr)
78
- .finally(()=> loadingRef.current = false)
79
- }, [cmd, JSON.stringify(params), forcer]) //eslint-disable-line -- json-ize to detect deep changes
80
- const reload = useCallback(()=> loadingRef.current || setForcer(v => v+1), [setForcer])
81
- return [ret, err, reload]
82
- }
83
-
84
- type EventHandler = (type:string, data?:any) => void
85
-
86
- export function apiEvents(cmd: string, params: Dict, cb:EventHandler) {
87
- console.debug('API EVENTS', cmd, params)
88
- const csrf = getCsrf()
89
- const processed: Record<string,string> = { csrf: csrf && JSON.stringify(csrf) }
90
- for (const k in params) {
91
- const v = params[k]
92
- if (v === undefined) continue
93
- processed[k] = JSON.stringify(v)
94
- }
95
- const source = new EventSource(PREFIX + cmd + '?' + new URLSearchParams(processed))
96
- source.onopen = () => cb('connected')
97
- source.onerror = err => cb('error', err)
98
- source.onmessage = ({ data }) => {
99
- if (!data) {
100
- cb('closed')
101
- return source.close()
102
- }
103
- try { data = JSON.parse(data) }
104
- catch {
105
- return cb('string', data)
106
- }
107
- console.debug('SSE msg', data)
108
- cb('msg', data)
109
- }
110
- return source
111
- }
112
-
113
- function getCsrf() {
114
- return getCookie('csrf')
115
- }
116
-
117
- export function useApiEvents(cmd: string, params: Dict={}) {
118
- const [data, setData] = useStateMounted<any>(undefined)
119
- const [error, setError] = useStateMounted<any>(undefined)
120
- const [loading, setLoading] = useStateMounted(false)
121
- useEffect(() => {
122
- const src = apiEvents(cmd, params, (type, data) => {
123
- switch (type) {
124
- case 'error':
125
- setError("Connection error")
126
- return stop()
127
- case 'closed':
128
- return stop()
129
- case 'msg':
130
- if (src?.readyState === src?.CLOSED)
131
- return stop()
132
- return setData(data)
133
- }
134
- })
135
- return () => {
136
- src.close()
137
- stop()
138
- }
139
-
140
- function stop() {
141
- setLoading(false)
142
- }
143
- }, [cmd, JSON.stringify(params)]) //eslint-disable-line
144
- return { data, loading, error }
145
- }
146
-
147
- export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { addId=false, map=((x:any)=>x) }={}) {
148
- const [list, setList] = useStateMounted<T[]>([])
149
- const [error, setError] = useStateMounted<any>(undefined)
150
- const [connecting, setConnecting] = useStateMounted(true)
151
- const [loading, setLoading] = useStateMounted(false)
152
- const [initializing, setInitializing] = useStateMounted(true)
153
- const idRef = useRef(0)
154
- useEffect(() => {
155
- if (!cmd) return
156
- const buffer: T[] = []
157
- const apply = _.debounce(() => {
158
- const chunk = buffer.splice(0, Infinity)
159
- if (chunk.length)
160
- setList(list => [ ...list, ...chunk ])
161
- }, 1000, { maxWait: 1000 })
162
- setError(undefined)
163
- setLoading(true)
164
- setConnecting(true)
165
- setInitializing(true)
166
- setList([])
167
- const src = apiEvents(cmd, params, (type, data) => {
168
- switch (type) {
169
- case 'connected':
170
- setConnecting(false)
171
- return setTimeout(() => apply.flush()) // this trick we'll cause first entries to be rendered almost immediately, while the rest will be subject to normal debouncing
172
- case 'error':
173
- setError("Connection error")
174
- return stop()
175
- case 'closed':
176
- return stop()
177
- case 'msg':
178
- wantArray(data).forEach(data => {
179
- if (data === 'ready') {
180
- apply.flush()
181
- setInitializing(false)
182
- return
183
- }
184
- if (data.error)
185
- return setError(err2msg(data.error))
186
- if (data.add) {
187
- const rec = map(data.add)
188
- if (addId)
189
- rec.id = ++idRef.current
190
- buffer.push(rec)
191
- apply()
192
- return
193
- }
194
- if (data.remove) {
195
- const matchOnList: ReturnType<typeof _.matches>[] = []
196
- // first remove from the buffer
197
- for (const key of data.remove) {
198
- const match1 = _.matches(key)
199
- if (_.isEmpty(_.remove(buffer, match1)))
200
- matchOnList.push(match1)
201
- }
202
- // then work the hooked state
203
- if (_.isEmpty(matchOnList))
204
- return
205
- setList(list => {
206
- const filtered = list.filter(rec => !matchOnList.some(match1 => match1(rec)))
207
- return filtered.length < list.length ? filtered : list // avoid unnecessary changes
208
- })
209
- return
210
- }
211
- if (data.update) {
212
- apply.flush() // avoid treating buffer
213
- setList(list => {
214
- const modified = [...list]
215
- for (const { search, change } of data.update) {
216
- const idx = modified.findIndex(_.matches(search))
217
- if (idx >= 0)
218
- modified[idx] = { ...modified[idx], ...change }
219
- }
220
- return modified
221
- })
222
- return
223
- }
224
- console.debug('unknown api event', type, data)
225
- })
226
- if (src?.readyState === src?.CLOSED)
227
- stop()
228
- }
229
- })
230
-
231
- return () => src.close()
232
-
233
- function stop() {
234
- setInitializing(false)
235
- setLoading(false)
236
- apply.flush()
237
- }
238
- }, [cmd, JSON.stringify(params)]) //eslint-disable-line
239
- return { list, loading, error, initializing, connecting, setList, updateList }
240
-
241
- function updateList(cb: (toModify: Draft<typeof list>) => void) {
242
- setList(produce(list, x => {
243
- cb(x)
244
- }))
245
- }
246
- }