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.
- package/admin/assets/index.bb5198ec.js +281 -0
- package/admin/assets/index.dcc78777.css +1 -0
- package/admin/assets/sha512.9dfe82e1.js +8 -0
- package/admin/index.html +3 -1
- package/admin/{public/logo.svg → logo.svg} +0 -0
- package/frontend/assets/index.27a78796.js +85 -0
- package/frontend/assets/index.93366732.css +1 -0
- package/frontend/assets/sha512.6af42937.js +8 -0
- package/frontend/{public/fontello.css → fontello.css} +0 -0
- package/frontend/{public/fontello.woff2 → fontello.woff2} +0 -0
- package/frontend/index.html +3 -1
- package/package.json +1 -1
- package/src/QuickZipStream.js +285 -0
- package/src/ThrottledStream.js +93 -0
- package/src/adminApis.js +169 -0
- package/src/api.accounts.js +59 -0
- package/src/api.auth.js +128 -0
- package/src/api.file_list.js +103 -0
- package/src/api.helpers.js +32 -0
- package/src/api.monitor.js +102 -0
- package/src/api.plugins.js +127 -0
- package/src/api.vfs.js +164 -0
- package/src/apiMiddleware.js +120 -0
- package/src/block.js +33 -0
- package/src/commands.js +124 -0
- package/src/config.js +168 -0
- package/src/connections.js +57 -0
- package/src/const.js +83 -0
- package/src/crypt.js +21 -0
- package/src/debounceAsync.js +48 -0
- package/src/events.js +9 -0
- package/src/frontEndApis.js +38 -0
- package/src/github.js +102 -0
- package/src/index.js +56 -0
- package/src/listen.js +235 -0
- package/src/log.js +137 -0
- package/src/middlewares.js +175 -0
- package/src/misc.js +160 -0
- package/src/pbkdf2.js +74 -0
- package/src/perm.js +181 -0
- package/src/plugins.js +343 -0
- package/src/serveFile.js +105 -0
- package/src/serveGuiFiles.js +113 -0
- package/src/sse.js +29 -0
- package/src/throttler.js +91 -0
- package/src/update.js +69 -0
- package/src/util-files.js +148 -0
- package/src/util-generators.js +30 -0
- package/src/util-http.js +30 -0
- package/src/vfs.js +230 -0
- package/src/watchLoad.js +73 -0
- package/src/zip.js +72 -0
- package/admin/.DS_Store +0 -0
- package/admin/.eslintrc +0 -8
- package/admin/.gitignore +0 -23
- package/admin/package.json +0 -67
- package/admin/src/AccountForm.ts +0 -92
- package/admin/src/AccountsPage.ts +0 -143
- package/admin/src/App.ts +0 -83
- package/admin/src/ArrayField.ts +0 -84
- package/admin/src/ConfigPage.ts +0 -279
- package/admin/src/FileField.ts +0 -52
- package/admin/src/FileForm.ts +0 -148
- package/admin/src/FilePicker.ts +0 -166
- package/admin/src/HomePage.ts +0 -96
- package/admin/src/InstalledPlugins.ts +0 -158
- package/admin/src/LoginRequired.ts +0 -75
- package/admin/src/LogoutPage.ts +0 -27
- package/admin/src/LogsPage.ts +0 -75
- package/admin/src/MainMenu.ts +0 -74
- package/admin/src/MenuButton.ts +0 -38
- package/admin/src/MonitorPage.ts +0 -200
- package/admin/src/OnlinePlugins.ts +0 -101
- package/admin/src/PermField.ts +0 -80
- package/admin/src/PluginsPage.ts +0 -27
- package/admin/src/VfsMenuBar.ts +0 -58
- package/admin/src/VfsPage.ts +0 -124
- package/admin/src/VfsTree.ts +0 -95
- package/admin/src/addFiles.ts +0 -59
- package/admin/src/api.ts +0 -246
- package/admin/src/dialog.ts +0 -203
- package/admin/src/index.css +0 -21
- package/admin/src/index.ts +0 -10
- package/admin/src/md.ts +0 -31
- package/admin/src/misc.ts +0 -141
- package/admin/src/react-app-env.d.ts +0 -1
- package/admin/src/reportWebVitals.ts +0 -15
- package/admin/src/setupTests.ts +0 -5
- package/admin/src/state.ts +0 -40
- package/admin/src/theme.ts +0 -37
- package/admin/tsconfig.json +0 -26
- package/admin/vite.config.ts +0 -32
- package/frontend/.DS_Store +0 -0
- package/frontend/.eslintrc +0 -8
- package/frontend/.gitignore +0 -23
- package/frontend/package.json +0 -51
- package/frontend/src/App.ts +0 -25
- package/frontend/src/Breadcrumbs.ts +0 -43
- package/frontend/src/BrowseFiles.ts +0 -141
- package/frontend/src/Head.ts +0 -45
- package/frontend/src/UserPanel.ts +0 -52
- package/frontend/src/api.ts +0 -78
- package/frontend/src/components.ts +0 -54
- package/frontend/src/dialog.css +0 -76
- package/frontend/src/dialog.ts +0 -105
- package/frontend/src/icons.ts +0 -46
- package/frontend/src/index.scss +0 -307
- package/frontend/src/index.ts +0 -10
- package/frontend/src/login.ts +0 -50
- package/frontend/src/menu.ts +0 -188
- package/frontend/src/misc.ts +0 -54
- package/frontend/src/options.ts +0 -52
- package/frontend/src/react-app-env.d.ts +0 -1
- package/frontend/src/reportWebVitals.ts +0 -15
- package/frontend/src/setupTests.ts +0 -5
- package/frontend/src/state.ts +0 -82
- package/frontend/src/useAuthorized.ts +0 -17
- package/frontend/src/useFetchList.ts +0 -144
- package/frontend/src/useTheme.ts +0 -23
- package/frontend/tsconfig.json +0 -26
- package/frontend/vite.config.ts +0 -21
- package/src/QuickZipStream.ts +0 -279
- package/src/ThrottledStream.ts +0 -98
- package/src/adminApis.ts +0 -161
- package/src/api.accounts.ts +0 -78
- package/src/api.auth.ts +0 -131
- package/src/api.file_list.ts +0 -102
- package/src/api.helpers.ts +0 -30
- package/src/api.monitor.ts +0 -106
- package/src/api.plugins.ts +0 -139
- package/src/api.vfs.ts +0 -182
- package/src/apiMiddleware.ts +0 -124
- package/src/block.ts +0 -35
- package/src/commands.ts +0 -122
- package/src/config.ts +0 -166
- package/src/connections.ts +0 -60
- package/src/const.ts +0 -57
- package/src/crypt.ts +0 -16
- package/src/debounceAsync.ts +0 -51
- package/src/events.ts +0 -6
- package/src/frontEndApis.ts +0 -17
- package/src/github.ts +0 -102
- package/src/index.ts +0 -53
- package/src/listen.ts +0 -220
- package/src/log.ts +0 -128
- package/src/middlewares.ts +0 -176
- package/src/misc.ts +0 -149
- package/src/pbkdf2.ts +0 -83
- package/src/perm.ts +0 -194
- package/src/plugins.ts +0 -342
- package/src/serveFile.ts +0 -104
- package/src/serveGuiFiles.ts +0 -95
- package/src/sse.ts +0 -29
- package/src/throttler.ts +0 -106
- package/src/update.ts +0 -67
- package/src/util-files.ts +0 -137
- package/src/util-generators.ts +0 -29
- package/src/util-http.ts +0 -29
- package/src/vfs.ts +0 -258
- package/src/watchLoad.ts +0 -75
- package/src/zip.ts +0 -69
package/admin/src/VfsPage.ts
DELETED
|
@@ -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
|
package/admin/src/VfsTree.ts
DELETED
|
@@ -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
|
-
}
|
package/admin/src/addFiles.ts
DELETED
|
@@ -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
|
-
}
|