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 },