hfs 0.26.6 → 0.26.8
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/.DS_Store +0 -0
- package/admin/.eslintrc +8 -0
- package/admin/.gitignore +23 -0
- package/admin/index.html +1 -3
- package/admin/package.json +67 -0
- package/admin/{logo.svg → public/logo.svg} +0 -0
- package/admin/src/AccountForm.ts +92 -0
- package/admin/src/AccountsPage.ts +143 -0
- package/admin/src/App.ts +83 -0
- package/admin/src/ArrayField.ts +84 -0
- package/admin/src/ConfigPage.ts +279 -0
- package/admin/src/FileField.ts +52 -0
- package/admin/src/FileForm.ts +148 -0
- package/admin/src/FilePicker.ts +166 -0
- package/admin/src/HomePage.ts +96 -0
- package/admin/src/InstalledPlugins.ts +158 -0
- package/admin/src/LoginRequired.ts +75 -0
- package/admin/src/LogoutPage.ts +27 -0
- package/admin/src/LogsPage.ts +75 -0
- package/admin/src/MainMenu.ts +74 -0
- package/admin/src/MenuButton.ts +38 -0
- package/admin/src/MonitorPage.ts +200 -0
- package/admin/src/OnlinePlugins.ts +101 -0
- package/admin/src/PermField.ts +80 -0
- package/admin/src/PluginsPage.ts +27 -0
- package/admin/src/VfsMenuBar.ts +58 -0
- package/admin/src/VfsPage.ts +124 -0
- package/admin/src/VfsTree.ts +95 -0
- package/admin/src/addFiles.ts +59 -0
- package/admin/src/api.ts +246 -0
- package/admin/src/dialog.ts +203 -0
- package/admin/src/index.css +21 -0
- package/admin/src/index.ts +10 -0
- package/admin/src/md.ts +31 -0
- package/admin/src/misc.ts +141 -0
- package/admin/src/react-app-env.d.ts +1 -0
- package/admin/src/reportWebVitals.ts +15 -0
- package/admin/src/setupTests.ts +5 -0
- package/admin/src/state.ts +40 -0
- package/admin/src/theme.ts +37 -0
- package/admin/tsconfig.json +26 -0
- package/admin/vite.config.ts +32 -0
- package/frontend/.DS_Store +0 -0
- package/frontend/.eslintrc +8 -0
- package/frontend/.gitignore +23 -0
- package/frontend/index.html +1 -3
- package/frontend/package.json +51 -0
- package/frontend/{fontello.css → public/fontello.css} +0 -0
- package/frontend/{fontello.woff2 → public/fontello.woff2} +0 -0
- package/frontend/src/App.ts +25 -0
- package/frontend/src/Breadcrumbs.ts +43 -0
- package/frontend/src/BrowseFiles.ts +141 -0
- package/frontend/src/Head.ts +45 -0
- package/frontend/src/UserPanel.ts +52 -0
- package/frontend/src/api.ts +78 -0
- package/frontend/src/components.ts +54 -0
- package/frontend/src/dialog.css +76 -0
- package/frontend/src/dialog.ts +105 -0
- package/frontend/src/icons.ts +46 -0
- package/frontend/src/index.scss +307 -0
- package/frontend/src/index.ts +10 -0
- package/frontend/src/login.ts +50 -0
- package/frontend/src/menu.ts +188 -0
- package/frontend/src/misc.ts +54 -0
- package/frontend/src/options.ts +52 -0
- package/frontend/src/react-app-env.d.ts +1 -0
- package/frontend/src/reportWebVitals.ts +15 -0
- package/frontend/src/setupTests.ts +5 -0
- package/frontend/src/state.ts +82 -0
- package/frontend/src/useAuthorized.ts +17 -0
- package/frontend/src/useFetchList.ts +144 -0
- package/frontend/src/useTheme.ts +23 -0
- package/frontend/tsconfig.json +26 -0
- package/frontend/vite.config.ts +21 -0
- package/package.json +2 -1
- package/plugins/vhosting/plugin.js +1 -1
- package/src/QuickZipStream.ts +279 -0
- package/src/ThrottledStream.ts +98 -0
- package/src/adminApis.ts +161 -0
- package/src/api.accounts.ts +78 -0
- package/src/api.auth.ts +131 -0
- package/src/api.file_list.ts +102 -0
- package/src/api.helpers.ts +30 -0
- package/src/api.monitor.ts +106 -0
- package/src/api.plugins.ts +139 -0
- package/src/api.vfs.ts +182 -0
- package/src/apiMiddleware.ts +124 -0
- package/src/block.ts +35 -0
- package/src/commands.ts +122 -0
- package/src/config.ts +166 -0
- package/src/connections.ts +60 -0
- package/src/const.ts +57 -0
- package/src/crypt.ts +16 -0
- package/src/debounceAsync.ts +51 -0
- package/src/events.ts +6 -0
- package/src/frontEndApis.ts +17 -0
- package/src/github.ts +102 -0
- package/src/index.ts +53 -0
- package/src/listen.ts +220 -0
- package/src/log.ts +128 -0
- package/src/middlewares.ts +176 -0
- package/src/misc.ts +149 -0
- package/src/pbkdf2.ts +83 -0
- package/src/perm.ts +194 -0
- package/src/plugins.ts +342 -0
- package/src/serveFile.ts +104 -0
- package/src/serveGuiFiles.ts +95 -0
- package/src/sse.ts +29 -0
- package/src/throttler.ts +106 -0
- package/src/update.ts +67 -0
- package/src/util-files.ts +137 -0
- package/src/util-generators.ts +29 -0
- package/src/util-http.ts +29 -0
- package/src/vfs.ts +258 -0
- package/src/watchLoad.ts +75 -0
- package/src/zip.ts +69 -0
- package/admin/assets/index.3129dad1.js +0 -282
- package/admin/assets/index.dcc78777.css +0 -1
- package/admin/assets/sha512.e9b1ee42.js +0 -8
- package/frontend/assets/index.1151988f.js +0 -85
- package/frontend/assets/index.93366732.css +0 -1
- package/frontend/assets/sha512.bb881250.js +0 -8
- package/src/QuickZipStream.js +0 -285
- package/src/ThrottledStream.js +0 -93
- package/src/adminApis.js +0 -169
- package/src/api.accounts.js +0 -59
- package/src/api.auth.js +0 -130
- package/src/api.file_list.js +0 -103
- package/src/api.helpers.js +0 -32
- package/src/api.monitor.js +0 -102
- package/src/api.plugins.js +0 -127
- package/src/api.vfs.js +0 -164
- package/src/apiMiddleware.js +0 -136
- package/src/block.js +0 -33
- package/src/commands.js +0 -124
- package/src/config.js +0 -168
- package/src/connections.js +0 -57
- package/src/const.js +0 -83
- package/src/crypt.js +0 -21
- package/src/debounceAsync.js +0 -48
- package/src/events.js +0 -9
- package/src/frontEndApis.js +0 -38
- package/src/github.js +0 -102
- package/src/index.js +0 -55
- package/src/listen.js +0 -235
- package/src/log.js +0 -137
- package/src/middlewares.js +0 -154
- package/src/misc.js +0 -160
- package/src/pbkdf2.js +0 -74
- package/src/perm.js +0 -176
- package/src/plugins.js +0 -343
- package/src/serveFile.js +0 -104
- package/src/serveGuiFiles.js +0 -113
- package/src/sse.js +0 -29
- package/src/throttler.js +0 -91
- package/src/update.js +0 -69
- package/src/util-files.js +0 -148
- package/src/util-generators.js +0 -30
- package/src/util-http.js +0 -30
- package/src/vfs.js +0 -226
- package/src/watchLoad.js +0 -73
- package/src/zip.js +0 -69
|
@@ -0,0 +1,59 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
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 {
|
|
4
|
+
Box,
|
|
5
|
+
Button,
|
|
6
|
+
CircularProgress,
|
|
7
|
+
Dialog as MuiDialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogProps,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
IconButton
|
|
12
|
+
} from '@mui/material'
|
|
13
|
+
import {
|
|
14
|
+
createElement as h, Fragment,
|
|
15
|
+
isValidElement,
|
|
16
|
+
ReactElement,
|
|
17
|
+
useEffect,
|
|
18
|
+
useRef,
|
|
19
|
+
useState
|
|
20
|
+
} from 'react'
|
|
21
|
+
import { Check, Close, Error as ErrorIcon, Forward, Info, Warning } from '@mui/icons-material'
|
|
22
|
+
import { newDialog, closeDialog, dialogsDefaults, DialogOptions } from '@hfs/shared'
|
|
23
|
+
import { Form, FormProps } from '@hfs/mui-grid-form'
|
|
24
|
+
import { useBreakpoint } from './misc'
|
|
25
|
+
import { Flex } from '@hfs/frontend/src/components'
|
|
26
|
+
export * from '@hfs/shared/lib/dialogs'
|
|
27
|
+
|
|
28
|
+
dialogsDefaults.Container = function Container(d:DialogOptions) {
|
|
29
|
+
useEffect(()=>{
|
|
30
|
+
ref.current?.focus()
|
|
31
|
+
}, [])
|
|
32
|
+
const ref = useRef<HTMLElement>()
|
|
33
|
+
d = { ...dialogsDefaults, ...d }
|
|
34
|
+
const { sx, root, ...rest } = d.dialogProps||{}
|
|
35
|
+
const p = d.padding ? 2 : 0
|
|
36
|
+
return h(MuiDialog, {
|
|
37
|
+
open: true,
|
|
38
|
+
maxWidth: 'lg',
|
|
39
|
+
fullScreen: !useBreakpoint('sm'),
|
|
40
|
+
...rest,
|
|
41
|
+
...root,
|
|
42
|
+
onClose: ()=> closeDialog(),
|
|
43
|
+
},
|
|
44
|
+
d.title && h(DialogTitle, {}, d.title),
|
|
45
|
+
h(DialogContent, {
|
|
46
|
+
ref,
|
|
47
|
+
sx: { ...sx, px: p, pb: p, display: 'flex', flexDirection: 'column', }
|
|
48
|
+
}, h(d.Content) )
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type AlertType = 'error' | 'warning' | 'info' | 'success'
|
|
53
|
+
|
|
54
|
+
const type2ico = {
|
|
55
|
+
error: ErrorIcon,
|
|
56
|
+
warning: Warning,
|
|
57
|
+
info: Info,
|
|
58
|
+
success: Check,
|
|
59
|
+
}
|
|
60
|
+
export async function alertDialog(msg: ReactElement | string | Error, options?: AlertType | ({ type?:AlertType, icon?: ReactElement } & Partial<DialogOptions>)) {
|
|
61
|
+
return new Promise(resolve => {
|
|
62
|
+
const opt = typeof options === 'string' ? { type: options } : (options ?? {})
|
|
63
|
+
let { type='info', ...rest } = opt
|
|
64
|
+
if (msg instanceof Error) {
|
|
65
|
+
msg = msg.message || String(msg)
|
|
66
|
+
type = 'error'
|
|
67
|
+
}
|
|
68
|
+
const close = newDialog({
|
|
69
|
+
className: 'dialog-alert-' + type,
|
|
70
|
+
icon: '!',
|
|
71
|
+
onClose: resolve,
|
|
72
|
+
...rest,
|
|
73
|
+
Content() {
|
|
74
|
+
return h(Box, { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 },
|
|
75
|
+
h(IconButton, {
|
|
76
|
+
onClick() {
|
|
77
|
+
close()
|
|
78
|
+
},
|
|
79
|
+
size: 'small',
|
|
80
|
+
sx: { position: 'absolute', right: 0, top: 0, opacity: .5 }
|
|
81
|
+
}, h(Close)),
|
|
82
|
+
opt.icon ?? h(type2ico[type], { color: type, fontSize: 'large' }),
|
|
83
|
+
isValidElement(msg) ? msg
|
|
84
|
+
: h(Box, { fontSize: 'large', mb: 1, lineHeight: '1.8em' }, String(msg)),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ConfirmOptions { href?: string }
|
|
92
|
+
export async function confirmDialog(msg: string | ReactElement, { href }: ConfirmOptions={}) : Promise<boolean> {
|
|
93
|
+
return new Promise(resolve => newDialog({
|
|
94
|
+
className: 'dialog-confirm',
|
|
95
|
+
icon: '?',
|
|
96
|
+
onClose: resolve,
|
|
97
|
+
Content
|
|
98
|
+
}) )
|
|
99
|
+
|
|
100
|
+
function Content() {
|
|
101
|
+
return h(Fragment, {},
|
|
102
|
+
h(Box, { mb: 2 }, msg),
|
|
103
|
+
h(Flex, {},
|
|
104
|
+
h('a', {
|
|
105
|
+
href,
|
|
106
|
+
onClick: () => closeDialog(true),
|
|
107
|
+
}, h(Button, { variant: 'contained' }, "Confirm")),
|
|
108
|
+
h(Button, { onClick: () => closeDialog(false) }, "Don't"),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type FormDialog<T> = Pick<DialogProps, 'fullScreen' | 'title'>
|
|
115
|
+
& Pick<DialogOptions, 'dialogProps'>
|
|
116
|
+
& Omit<FormProps<T>, 'values' | 'save' | 'set'>
|
|
117
|
+
& Partial<Pick<FormProps<T>, 'values' | 'save'>>
|
|
118
|
+
& {
|
|
119
|
+
onChange?: (values:Partial<T>, extra: { setValues: React.Dispatch<React.SetStateAction<Partial<T>>> }) => void,
|
|
120
|
+
before?: any
|
|
121
|
+
}
|
|
122
|
+
export async function formDialog<T>({ fullScreen, title, onChange, before, ...props }: FormDialog<T>) : Promise<T> {
|
|
123
|
+
return new Promise(resolve => newDialog({
|
|
124
|
+
className: 'dialog-confirm',
|
|
125
|
+
icon: '?',
|
|
126
|
+
onClose: resolve,
|
|
127
|
+
title,
|
|
128
|
+
Content
|
|
129
|
+
}) )
|
|
130
|
+
|
|
131
|
+
function Content() {
|
|
132
|
+
const [values, setValues] = useState<Partial<T>>(props.values||{})
|
|
133
|
+
return h(Fragment, {},
|
|
134
|
+
before,
|
|
135
|
+
h(Form, {
|
|
136
|
+
...props,
|
|
137
|
+
values,
|
|
138
|
+
set(v, k) {
|
|
139
|
+
const newV = { ...values, [k]: v }
|
|
140
|
+
setValues(newV)
|
|
141
|
+
onChange?.(newV, { setValues })
|
|
142
|
+
},
|
|
143
|
+
save: {
|
|
144
|
+
...props.save,
|
|
145
|
+
onClick() {
|
|
146
|
+
closeDialog(values)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function promptDialog(msg: string, props:any={}) : Promise<string | undefined> {
|
|
155
|
+
return formDialog<{ text: string }>({
|
|
156
|
+
...props,
|
|
157
|
+
fields: [
|
|
158
|
+
{ k: 'text', label: null, autoFocus: true,
|
|
159
|
+
before: h(Box, { mb: 2 }, msg),
|
|
160
|
+
...props.field
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
save: {
|
|
164
|
+
children: "Continue",
|
|
165
|
+
startIcon: h(Forward),
|
|
166
|
+
...props.save,
|
|
167
|
+
},
|
|
168
|
+
saveOnEnter: true,
|
|
169
|
+
barSx: { gap: 2 },
|
|
170
|
+
addToBar: [
|
|
171
|
+
h(Button, { onClick: closeDialog }, "Cancel"),
|
|
172
|
+
...props.addToBar||[],
|
|
173
|
+
]
|
|
174
|
+
}).then(values => values?.text)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function waitDialog() {
|
|
178
|
+
return newDialog({ Content: CircularProgress, closable: false })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function toast(msg: string | ReactElement, type: AlertType | ReactElement='info') {
|
|
182
|
+
const ms = 3000
|
|
183
|
+
const close = newDialog({
|
|
184
|
+
Content,
|
|
185
|
+
dialogProps: {
|
|
186
|
+
PaperProps: {
|
|
187
|
+
sx: { transition: `opacity ${ms}ms ease-in` },
|
|
188
|
+
ref(x: HTMLElement) { // we need to set opacity later, to trigger transition
|
|
189
|
+
if (x)
|
|
190
|
+
x.style.opacity = '0'
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
setTimeout(close, ms)
|
|
196
|
+
|
|
197
|
+
function Content(){
|
|
198
|
+
return h(Box, { display:'flex', flexDirection: 'column', alignItems: 'center', gap: 1 },
|
|
199
|
+
isValidElement(type) ? type : h(type2ico[type], { color:type }),
|
|
200
|
+
isValidElement(msg) ? msg : h('div', {}, String(msg))
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
4
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
5
|
+
sans-serif;
|
|
6
|
+
-webkit-font-smoothing: antialiased;
|
|
7
|
+
-moz-osx-font-smoothing: grayscale;
|
|
8
|
+
height: 100vh;
|
|
9
|
+
}
|
|
10
|
+
#root { min-height: 100%; display:flex; }
|
|
11
|
+
|
|
12
|
+
code {
|
|
13
|
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
14
|
+
monospace;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.MuiTreeItem-content {
|
|
18
|
+
box-sizing: border-box; /* avoid unwanted scrolling caused by its width:100% + padding */
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ol, ul { margin-top: .2em }
|
|
@@ -0,0 +1,10 @@
|
|
|
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, StrictMode } from 'react'
|
|
4
|
+
import { createRoot } from 'react-dom/client'
|
|
5
|
+
import './index.css'
|
|
6
|
+
import '@hfs/shared/src/min-crypto-polyfill'
|
|
7
|
+
import App from './App'
|
|
8
|
+
|
|
9
|
+
createRoot(document.getElementById('root')!)
|
|
10
|
+
.render( h(StrictMode, {}, h(App)) )
|
package/admin/src/md.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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, Fragment } from 'react'
|
|
4
|
+
|
|
5
|
+
// markdown inspired syntax to transform text into react elements: * for bold, / for italic, _ for underline, ` for code
|
|
6
|
+
export default function md(text: string | TemplateStringsArray) {
|
|
7
|
+
if (typeof text !== 'string')
|
|
8
|
+
text = text[0]
|
|
9
|
+
const re = /([`*/_])(.+)\1|(\n)/g
|
|
10
|
+
const res = []
|
|
11
|
+
let last = 0
|
|
12
|
+
let match
|
|
13
|
+
while (match = re.exec(text)) { //eslint-disable-line no-cond-assign
|
|
14
|
+
res.push( text.slice(last, match.index) )
|
|
15
|
+
if (match[3])
|
|
16
|
+
res.push(h('br'))
|
|
17
|
+
else {
|
|
18
|
+
const tag = ({
|
|
19
|
+
'`': 'code',
|
|
20
|
+
'*': 'b',
|
|
21
|
+
'/': 'i',
|
|
22
|
+
'_': 'u',
|
|
23
|
+
})[ match[1] ]
|
|
24
|
+
if (!tag)
|
|
25
|
+
throw Error("should never happen")
|
|
26
|
+
res.push( h(tag,{}, match[2]) )
|
|
27
|
+
}
|
|
28
|
+
last = match.index + match[0].length
|
|
29
|
+
}
|
|
30
|
+
return h(Fragment, {}, ...res, text.slice(last, Infinity))
|
|
31
|
+
}
|