admin/config: helper text for server status

Massimo Melina committed Feb 13, 2022 at 19:56 UTC 7b4792657f4ef7695c7ced4af24bcb8f7360b402
5 files changed +63 -56
admin/src/ConfigPage.ts
+33 -21
@@ -1,6 +1,6 @@
1 -import { Box, Button } from '@mui/material';
2 -import { createElement as h, isValidElement, useRef } from 'react';
3 -import { apiCall, useApiComp } from './api'
1 +import { Box, Button, FormHelperText } from '@mui/material';
2 +import { createElement as h, isValidElement, useEffect, useRef } from 'react';
3 +import { apiCall, useApi, useApiComp } from './api'
4 import { state, useSnapState } from './state'
5 import { Refresh } from '@mui/icons-material'
6 import { Dict } from './misc'
@@ -14,10 +14,12 @@ let loaded: Dict | undefined
14 subscribeKey(state, 'config', recalculateChanges)
15
16 export default function ConfigPage() {
17 - const [res, reload] = useApiComp('get_config', {
17 + const [res, reloadConfig] = useApiComp('get_config', {
18 omit: ['vfs', 'accounts']
19 })
20 let snap = useSnapState()
21 + const [status, reloadStatus] = useApi(res && 'get_status')
22 + useEffect(reloadStatus, [res])
23 if (isValidElement(res))
24 return res
25 const { changes } = snap
@@ -35,7 +37,10 @@ export default function ConfigPage() {
37 disabled: !Object.keys(changes).length,
38 },
39 addToBar: [h(Button, {
38 - onClick: reload,
40 + onClick() {
41 + reloadConfig()
42 + reloadStatus()
43 + },
44 startIcon: h(Refresh),
45 }, 'Reload')],
46 defaults({ comp }) {
@@ -43,8 +48,8 @@ export default function ConfigPage() {
48 return { md: shortField ? 3 : 6 }
49 },
50 fields: [
46 - { k: 'port', comp: ServerPort, label:'HTTP port' },
47 - { k: 'https_port', comp: ServerPort, label: 'HTTPS port' },
51 + { k: 'port', comp: ServerPort, label:'HTTP port', status: status?.http||true },
52 + { k: 'https_port', comp: ServerPort, label: 'HTTPS port', status: status?.https||true },
53 config.https_port >= 0 && { k: 'cert', comp: StringField, label: 'HTTPS certificate file' },
54 config.https_port >= 0 && { k: 'private_key', comp: StringField, label: 'HTTPS private key file' },
55 { k: 'admin_port', comp: ServerPort, label: 'Admin port' },
@@ -71,6 +76,7 @@ export default function ConfigPage() {
76
77 async function save() {
78 await apiCall('set_config', { values: state.changes })
79 + setTimeout(reloadStatus, 1000)
80 Object.assign(loaded, state.changes) // since changes are recalculated subscribing state.config, but it depends on 'loaded' to (which cannot be subscribed), be sure to update loaded first
81 recalculateChanges()
82 await alertDialog("Changes applied", 'success')
@@ -86,23 +92,29 @@ function recalculateChanges() {
92 state.changes = changes
93 }
94
89 -function ServerPort({ label, value, onChange }: FieldProps<number | null>) {
95 +function ServerPort({ label, value, onChange, status }: FieldProps<number | null>) {
96 const lastCustom = useRef(1)
97 if (value! > 0)
98 lastCustom.current = value!
99 const selectValue = Number(value! > 0 ? lastCustom.current : value) || 0
94 - return h(Box, { display:'flex' },
95 - h(SelectField as Field<number>, {
96 - sx: { flexGrow: 1 },
97 - label,
98 - value: selectValue,
99 - options: [
100 - { label: 'off', value: -1 },
101 - { label: 'automatic port', value: 0 },
102 - { label: 'choose port number', value: lastCustom.current },
103 - ],
104 - onChange,
105 - }),
106 - value! > 0 && h(NumberField, { label: 'Number', fullWidth: false, value, onChange }),
100 + const error = status?.error
101 + return h(Box, {},
102 + h(Box, { display:'flex' },
103 + h(SelectField as Field<number>, {
104 + sx: { flexGrow: 1 },
105 + label,
106 + value: selectValue,
107 + options: [
108 + { label: 'off', value: -1 },
109 + { label: 'automatic port', value: 0 },
110 + { label: 'choose port number', value: lastCustom.current },
111 + ],
112 + onChange,
113 + }),
114 + value! > 0 && h(NumberField, { label: 'Number', fullWidth: false, value, onChange }),
115 + ),
116 + status && h(FormHelperText, { error: Boolean(error) },
117 + status === true ? '...'
118 + : error ?? (status?.listening && 'working on port '+ status.port) )
119 )
120 }
admin/src/HomePage.ts
-2
@@ -20,8 +20,6 @@ export default function HomePage() {
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' : ''
23 const errors = errorMap && onlyTruthy(Object.entries(errorMap).map(([k,v]) =>
24 v && [md(`Protocol _${k}_ cannot work: `), v, typeof v === 'string' && /certificate|key/.test(v) && [' - ', cfgLink("provide adequate files")]]))
25 return h(Box, { display:'flex', gap: 2, flexDirection:'column' },
src/adminApis.ts
+1 -1
@@ -37,7 +37,7 @@ export const adminApis: ApiHandlers = {
37 function serverStatus(h: typeof st.httpSrv, configuredPort?: number) {
38 return {
39 ..._.pick(h, ['listening', 'busy', 'error']),
40 - port: (h.address() as any)?.port || configuredPort,
40 + port: (h?.address() as any)?.port || configuredPort,
41 }
42 }
43 },
src/listen.ts
+28 -32
@@ -6,15 +6,15 @@ import { watchLoad } from './watchLoad'
6 import { networkInterfaces } from 'os';
7 import { newConnection } from './connections'
8 import open from 'open'
9 -import { debounceAsync } from './misc'
9 +import { debounceAsync, prefix } from './misc'
10 import { DEV } from './const'
11 import findProcess from 'find-process'
12 +import _ from 'lodash'
13
14 interface ServerExtra { error?: string, busy?: string }
15 let httpSrv: http.Server & ServerExtra
16 let httpsSrv: http.Server & ServerExtra
17 let adminSrv: http.Server & ServerExtra
17 -let cert:string, key: string
18
19 subscribeConfig<number>({ k:'port', defaultValue: 80 }, async port => {
20 await stopServer(httpSrv)
@@ -50,50 +50,46 @@ defineConfig('open_browser_at_start', { defaultValue: !DEV })
50 subscribeConfig<string>({ k:'admin_network', defaultValue: '127.0.0.1' }, considerAdmin)
51 subscribeConfig<number>({ k:'admin_port', defaultValue: 63636 }, considerAdmin)
52
53 -subscribeConfig({ k:'cert' }, async (v: string) => {
54 - await stopServer(httpsSrv)
55 - cert = v
56 - if (!cert) return
57 - if (cert.includes('\n'))
58 - return considerHttps()
59 - // it's a path
60 - watchLoad(cert, data => {
61 - cert = data
62 - considerHttps()
53 +const httpsNeeds = { cert:'', private_key:'' }
54 +const httpsNeedsNames = { cert: 'certificate', private_key: 'private key' }
55 +for (const k of Object.keys(httpsNeeds) as (keyof typeof httpsNeeds)[]) { // please be smarter typescript
56 + let unwatch: ReturnType<typeof watchLoad>['unwatch']
57 + subscribeConfig({ k }, async (v: string) => {
58 + unwatch?.()
59 + httpsNeeds[k] = v
60 + if (!v || v.includes('\n'))
61 + return considerHttps()
62 + // it's a path
63 + httpsNeeds[k] = ''
64 + unwatch = watchLoad(v, data => {
65 + httpsNeeds[k] = data
66 + considerHttps()
67 + }).unwatch
68 + await considerHttps()
69 })
64 - cert = ''
65 -})
66 -
67 -subscribeConfig({ k:'private_key' }, async (v: string) => {
68 - await stopServer(httpsSrv)
69 - key = v
70 - if (!key) return
71 - if (key.includes('\n'))
72 - return considerHttps()
73 - // it's a path
74 - watchLoad(key, data => {
75 - key = data
76 - considerHttps()
77 - })
78 - key = ''
79 -})
70 +}
71
72 const CFG_HTTPS_PORT = 'https_port'
73 subscribeConfig({ k:CFG_HTTPS_PORT, defaultValue: -1 }, considerHttps)
74
75 async function considerHttps() {
76 await stopServer(httpsSrv)
77 + let port = getConfig('https_port')
78 try {
87 - httpsSrv = https.createServer({ key, cert }, app.callback())
88 - httpsSrv.error = undefined
79 + httpsSrv = https.createServer({ key: httpsNeeds.private_key, cert: httpsNeeds.cert }, app.callback())
80 + const missingKey = _.findKey(httpsNeeds, v => !v) as keyof typeof httpsNeeds
81 + httpsSrv.error = port < 0 ? undefined
82 + : missingKey && prefix(getConfig(missingKey) ? "cannot read file for " : "missing ", httpsNeedsNames[missingKey])
83 + if (httpsSrv.error)
84 + return
85 }
86 catch(e) {
87 httpsSrv.error = "bad private key or certificate"
88 console.log("failed to create https server: check your private key and certificate", String(e))
89 return
90 }
95 - const port = await startServer(httpsSrv, {
96 - port: !cert || !key ? -1 : getConfig('https_port'),
91 + port = await startServer(httpsSrv, {
92 + port: getConfig('https_port'),
93 name: 'https'
94 })
95 if (!port) return
todo.md
+1
@@ -1,4 +1,5 @@
1 # To do
2 +- admin/config: use filepicker for https files
3 - admin: warn in case of items with same name
4 - password protect admin
5 - allowed referer