admin/home: more warnings to help user correct problems
Massimo Melina committed
Feb 12, 2022 at 11:56 UTC
44ffe0bc6a048667fa754228e43b62b535e8de8a
7 files changed
+119
-41
admin/src/ConfigPage.ts
+2
-2
@@ -52,8 +52,8 @@ export default function ConfigPage() {
52
},
53
{ k: 'port', comp: ServerPort, label:'HTTP port' },
54
{ k: 'https_port', comp: ServerPort, label: 'HTTPS port' },
55
- { k: 'cert', comp: StringField, label: 'HTTPS certificate file' },
56
- { k: 'private_key', comp: StringField, label: 'HTTPS private key file' },
55
+ config.https_port >= 0 && { k: 'cert', comp: StringField, label: 'HTTPS certificate file' },
56
+ config.https_port >= 0 && { k: 'private_key', comp: StringField, label: 'HTTPS private key file' },
57
{ k: 'max_kbps', comp: NumberField, label: 'Max KB/s' },
58
{ k: 'max_kbps_per_ip', comp: NumberField, label: 'Max KB/s per-ip' },
59
{ k: 'log', comp: StringField, label: 'Main log file' },
admin/src/HomePage.ts
+50
-22
@@ -1,31 +1,59 @@
1
import { createElement as h } from 'react'
2
import { Alert, Box, Link } from '@mui/material'
3
import { useApi } from './api'
4
-import { spinner } from './misc'
4
+import { Dict, dontBotherWithKeys, InLink, objSameKeys, onlyTruthy, spinner } from './misc'
5
import { Launch } from '@mui/icons-material'
6
-import { Link as RouterLink } from 'react-router-dom'
6
+import md from './md'
7
+
8
+interface ServerStatus { listening: boolean, port: number, error?: string, busy?: string }
9
10
export default function HomePage() {
9
- const [status] = useApi('get_status')
11
+ const [status] = useApi<Dict<ServerStatus>>('get_status')
12
const [vfs] = useApi('get_vfs')
11
- const { http, https } = status || {}
12
- const secure = !http?.active && https?.active ? 's' : ''
13
- const srv = secure ? https : (http?.active && http)
14
- const href = srv && `http${secure}://`+window.location.hostname + (srv.port === (secure ? 443 : 80) ? '' : ':'+srv.port)
15
- return !status ? spinner() :
16
- h(Box, { display:'flex', gap: 2, flexDirection:'column' },
17
- href ? h(Box, {},
18
- h(Alert, { severity: 'success' }, "Server is working"),
19
- h(Box, { mt:2, fontSize:'200%' },
20
- h(Link, { target:'frontend', href }, "Open frontend interface",
21
- h(Launch, { sx: { ml:1, mt:1 } }),
22
- )
23
- ),
24
- h(Box, { color:'text.secondary' },
25
- `Inside frontend your users can see the files and folders you decide in the File System.`)
26
- ) : h(Alert, { severity: 'warning' }, "Frontend switched off"),
13
+ const [cfg] = useApi('get_config', { only: ['https_port', 'cert', 'private_key'] })
14
+ if (!status)
15
+ return spinner()
16
+ const { http, https } = status
17
+ const goSecure = !http?.listening && https?.listening ? 's' : ''
18
+ const srv = goSecure ? https : (http?.listening && http)
19
+ const href = srv && `http${goSecure}://`+window.location.hostname + (srv.port === (goSecure ? 443 : 80) ? '' : ':'+srv.port)
20
+ const errorMap = objSameKeys(status, v =>
21
+ v.busy ? [`port ${v.port} already used by ${v.busy} - choose a `, cfgLink('different port'), ` or stop ${v.busy}`]
22
+ : v.error )
23
+ if (!errorMap.https && cfg?.https_port >= 0 && !status.https.listening)
24
+ errorMap.https = !cfg.cert ? 'missing certificate' : !cfg.private_key ? 'missing private key' : ''
25
+ const errors = errorMap && onlyTruthy(Object.entries(errorMap).map(([k,v]) =>
26
+ v && [md(`Protocol _${k}_ cannot work: `), v, typeof v === 'string' && /certificate|key/.test(v) && [' - ', cfgLink("provide adequate files")]]))
27
+ return h(Box, { display:'flex', gap: 2, flexDirection:'column' },
28
+ !cfg ? spinner() :
29
+ errors.length ? errors.map((msg, i) => h(Alert, { key: i, severity: 'error' }, dontBotherWithKeys(msg)))
30
+ : href && h(Alert, { severity: 'success' }, "Server is working"),
31
+ href ? h(Box, { my:1, ml:1 },
32
+ h(Box, { fontSize:'200%' },
33
+ h(Link, { target:'frontend', href }, "Open frontend interface",
34
+ h(Launch, { sx: { ml:1, mt:1 } }),
35
+ )
36
+ ),
37
+ h(Box, { color:'text.secondary' },
38
+ `Inside frontend your users can see the files and folders you decide in the File System.`)
39
+ ) : h(Alert, { severity: 'warning' }, "Frontend unreachable: ",
40
+ !cfg ? '...'
41
+ : errors.length === 2 ? "both http and https are in error"
42
+ : [
43
+ ['http','https'].map(k => k + " " + (errorMap[k] ? "is in error" : "is off")).join(', '),
44
+ !errors.length && [' - ', cfgLink("switch http or https on")]
45
+ ]
46
+ ),
47
+
48
+ vfs?.root && !vfs.root.children?.length && !vfs.root.source &&
49
+ h(Alert, { severity: 'warning' }, "You have no files shares - ", fsLink("add some files"))
50
+ )
51
+}
52
+
53
+function fsLink(text=`File System page`) {
54
+ return h(InLink, { to:'fs' }, text)
55
+}
56
28
- vfs?.root && !vfs.root.children?.length && !vfs.root.source &&
29
- h(Alert, { severity: 'warning', sx:{ mt:2 } }, "You have no files shared. Go add some in the ", h(RouterLink, { to:'fs' }, h(Link, {}, `File System page.`)))
30
- )
57
+function cfgLink(text=`Configuration page`) {
58
+ return h(InLink, { to:'configuration' }, text)
59
}
admin/src/MonitorPage.ts
+3
-2
@@ -7,6 +7,7 @@ import { DataGrid } from "@mui/x-data-grid"
7
import { Alert } from '@mui/material'
8
import { formatBytes, IconBtn } from "./misc"
9
import { alertDialog } from "./dialog"
10
+import { prefix } from './misc'
11
12
export default function MonitorPage() {
13
return h(Box, { flex: 1, display: 'flex', flexDirection: 'column' },
@@ -33,8 +34,8 @@ function MoreInfo() {
34
isValidElement(res) ? res :
35
h('ul', {},
36
pair('started'),
36
- pair('http', 'HTTP', v => v.active ? 'port '+v.port : 'off'),
37
- pair('https', 'HTTPS', v => v.active ? 'port '+v.port : 'off'),
37
+ pair('http', 'HTTP', v => v.listening ? 'port '+v.port : ('off' + prefix(': configured port is used by ',v.busy))),
38
+ pair('https', 'HTTPS', v => v.listening ? 'port '+v.port : ('off' + prefix(': configured port is used by ',v.busy))),
39
)
40
)
41
admin/src/api.ts
+5
-6
@@ -1,11 +1,10 @@
1
-import { createElement as h, useCallback, useEffect, useMemo, useRef } from 'react'
1
+import { createElement as h, ReactElement, useCallback, useEffect, useMemo, useRef } from 'react'
2
import { Dict, Falsy, getCookie, spinner, useStateMounted } from './misc'
3
import { Alert } from '@mui/material'
4
import _ from 'lodash'
5
6
-export function useApiComp(...args: any[]): ReturnType<typeof useApi> {
7
- // @ts-ignore
8
- const [res, reload] = useApi(...args)
6
+export function useApiComp<T=any>(...args: Parameters<typeof useApi>): [T | ReactElement, ()=>void] {
7
+ const [res, reload] = useApi<T>(...args)
8
return useMemo(() =>
9
res === undefined ? [spinner(), reload]
10
: res && res instanceof Error ? [h(Alert, { severity: 'error' }, String(res)), reload]
@@ -41,8 +40,8 @@ export class ApiError extends Error {
40
}
41
}
42
44
-export function useApi(cmd: string | Falsy, params?: object) : [any, ()=>void] {
45
- const [ret, setRet] = useStateMounted(undefined)
43
+export function useApi<T=any>(cmd: string | Falsy, params?: object) : [T | undefined, ()=>void] {
44
+ const [ret, setRet] = useStateMounted<T | undefined>(undefined)
45
const [forcer, setForcer] = useStateMounted(0)
46
const loadingRef = useRef(false)
47
useEffect(()=>{
admin/src/md.ts
new
+22
@@ -0,0 +1,22 @@
1
+import { createElement as h, Fragment } from 'react'
2
+
3
+// markdown inspired syntax to transform text into react elements: * for bold, / for italic, _ for underline, ` for code
4
+export default function md(text: string) {
5
+ const re = /([`*/_])(.+)\1/g
6
+ const res = []
7
+ let last = 0
8
+ let match
9
+ while (match = re.exec(text)) { //eslint-disable-line no-cond-assign
10
+ const tag = ({
11
+ '`': 'code',
12
+ '*': 'b',
13
+ '/': 'i',
14
+ '_': 'u',
15
+ })[ match[1] ]
16
+ if (!tag)
17
+ throw Error("should never happen")
18
+ res.push( text.slice(last, match.index), h(tag,{}, match[2]) )
19
+ last = match.index + match[0].length
20
+ }
21
+ return h(Fragment, {}, ...res, text.slice(last, Infinity))
22
+}
admin/src/misc.ts
+30
-2
@@ -1,5 +1,7 @@
1
-import { createElement as h, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
2
-import { CircularProgress, IconButton, Tooltip } from '@mui/material'
1
+import { createElement as h, Fragment, FunctionComponent, ReactElement,
2
+ ReactNode, useCallback, useEffect, useRef, useState } from 'react'
3
+import { CircularProgress, IconButton, Link, Tooltip } from '@mui/material'
4
+import { Link as RouterLink } from 'react-router-dom'
5
6
export type Dict<T = any> = Record<string, T>
7
export type Falsy = false | null | undefined | '' | 0
@@ -95,3 +97,29 @@ export function IconBtn({ title, icon, ...rest }: { title?: string, icon:Functio
97
return title ? h(Tooltip, { title, children: ret }) : ret
98
}
99
100
+export function prefix(pre:string, v:string|number, post:string='') {
101
+ return v ? pre+v+post : ''
102
+}
103
+
104
+export function reactFilter(elements: any[]) {
105
+ return elements.filter(x=> x===0 || x && (!Array.isArray(x) || x.length))
106
+}
107
+
108
+export function reactJoin(joiner: string | ReactElement, elements: Parameters<typeof reactFilter>[0]) {
109
+ const ret = []
110
+ for (const x of reactFilter(elements))
111
+ ret.push(x, joiner)
112
+ ret.splice(-1,1)
113
+ return dontBotherWithKeys(ret)
114
+}
115
+
116
+export function dontBotherWithKeys(elements: ReactNode[]): (ReactNode|string)[] {
117
+ return elements.map((e,i)=>
118
+ !e || typeof e === 'string' ? e
119
+ : Array.isArray(e) ? dontBotherWithKeys(e)
120
+ : h(Fragment, { key:i, children:e }) )
121
+}
122
+
123
+export function InLink(props:any) {
124
+ return h(Link, { component: RouterLink, ...props })
125
+}
src/adminApis.ts
+7
-7
@@ -1,12 +1,12 @@
1
import { ApiHandlers } from './apis'
2
-import { getWholeConfig, setConfig } from './config'
2
+import { getConfig, getWholeConfig, setConfig } from './config'
3
import { getStatus } from './listen'
4
import { app, HFS_STARTED } from './index'
5
-import { Server } from 'http'
5
import vfsApis from './api.vfs'
6
import accountsApis from './api.accounts'
7
import { Connection, getConnections } from './connections'
8
import { generatorAsCallback, onOffMap, pendingPromise } from './misc'
9
+import _ from 'lodash'
10
11
export const adminApis: ApiHandlers = {
12
@@ -27,14 +27,14 @@ export const adminApis: ApiHandlers = {
27
const st = getStatus()
28
return {
29
started: HFS_STARTED,
30
- http: serverStatus(st.httpSrv),
31
- https: serverStatus(st.httpsSrv),
30
+ http: serverStatus(st.httpSrv, getConfig('port')),
31
+ https: serverStatus(st.httpsSrv, getConfig('https_port')),
32
}
33
34
- function serverStatus(h: Server) {
34
+ function serverStatus(h: typeof st.httpSrv, configuredPort?: number) {
35
return {
36
- active: h.listening,
37
- port: (h.address() as any)?.port,
36
+ ..._.pick(h, ['listening', 'busy', 'error']),
37
+ port: (h.address() as any)?.port || configuredPort,
38
}
39
}
40
},