fix: admin/home: wrong instructions displayed in case of network error

Massimo Melina committed Apr 8, 2022 at 14:59 UTC a6e9db009cfaf79bb05424cc56bcec2c1f6c5d32
6 files changed +47 -42
admin/src/AccountsPage.ts
+1 -1
@@ -21,7 +21,7 @@ const useStyles = makeStyles({
21 }
22 })
23
24 -interface Account {
24 +export interface Account {
25 username: string
26 hasPassword?: boolean
27 adminActualAccess?: boolean
admin/src/ConfigPage.ts
+3 -1
@@ -25,7 +25,7 @@ export const logLabels = {
25 export default function ConfigPage() {
26 const [res, reloadConfig] = useApiComp('get_config', { omit: ['vfs'] })
27 let snap = useSnapState()
28 - const [status, reloadStatus] = useApi(res && 'get_status')
28 + const [status, reloadStatus] = useApiComp(res && 'get_status')
29 useEffect(reloadStatus, [res, reloadStatus])
30
31 exposedReloadStatus = reloadStatus
@@ -33,6 +33,8 @@ export default function ConfigPage() {
33
34 if (isValidElement(res))
35 return res
36 + if (isValidElement(status))
37 + return status
38 const { changes } = snap
39 const values = (loaded !== res) ? (state.config = loaded = res) : snap.config
40 const maxSpeedDefaults = {
admin/src/FileCard.ts
+7 -4
@@ -1,10 +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 { state, useSnapState } from './state'
4 -import { createElement as h, useEffect, useMemo, useState } from 'react'
4 +import { createElement as h, isValidElement, useEffect, useMemo, useState } from 'react'
5 import { Alert, Button, Card, CardContent, List, ListItem, ListItemText } from '@mui/material'
6 import { BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, SelectField } from './Form'
7 -import { apiCall, useApi } from './api'
7 +import { apiCall, useApiComp } from './api'
8 import { formatBytes, isEqualLax, modifiedSx, onlyTruthy } from './misc'
9 import { reloadVfs, Who } from './VfsPage'
10 import md from './md'
@@ -34,8 +34,6 @@ function FileForm({ file }: { file: ReturnType<typeof useSnapState>['selectedFil
34 setValues(Object.assign({ can_see: null, can_read: null }, rest))
35 }, [file]) //eslint-disable-line
36
37 - const accounts = useApi('get_accounts')[0]?.list
38 -
37 const { source } = file
38 const isDir = file.type === 'folder'
39 const hasSource = source !== undefined // we need a boolean
@@ -56,6 +54,11 @@ function FileForm({ file }: { file: ReturnType<typeof useSnapState>['selectedFil
54 const showCanSee = (values.can_read ?? inheritedPerms.can_read) === true
55 const showTimestamps = hasSource && Boolean(values.ctime)
56
57 + let [accountsRes] = useApiComp<{ list: Account[] }>('get_accounts')
58 + if (isValidElement(accountsRes))
59 + return accountsRes
60 + const accounts = accountsRes.list
61 +
62 return h(Form, {
63 values,
64 set(v, k) {
admin/src/HomePage.ts
+20 -24
@@ -1,26 +1,28 @@
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'
3 +import { createElement as h, isValidElement } from 'react'
4 import { Box, Button, Link } from '@mui/material'
5 -import { apiCall, useApi } from './api'
6 -import { Dict, dontBotherWithKeys, InLink, objSameKeys, onlyTruthy, spinner } from './misc'
5 +import { apiCall, useApi, useApiComp } from './api'
6 +import { Dict, dontBotherWithKeys, InLink, objSameKeys, onlyTruthy } from './misc'
7 import { CheckCircle, Error, Info, Launch, Warning } from '@mui/icons-material'
8 import md from './md'
9 import { useSnapState } from './state'
10 import { confirmDialog } from './dialog'
11 import { isCertError, makeCertAndSave } from './ConfigPage'
12 +import { VfsNode } from './VfsPage'
13 +import { Account } from './AccountsPage'
14
15 interface ServerStatus { listening: boolean, port: number, error?: string, busy?: string }
16
17 export default function HomePage() {
18 const SOLUTION_SEP = " — "
19 const { username } = useSnapState()
18 - const [status, reloadStatus] = useApi<Dict<ServerStatus>>('get_status')
19 - const [vfs] = useApi('get_vfs')
20 - const [account] = useApi(username && 'get_account')
21 - const [cfg, reloadCfg] = useApi('get_config', { only: ['https_port', 'cert', 'private_key', 'proxies', 'ignore_proxies'] })
22 - if (!status)
23 - return spinner()
20 + const [status, reloadStatus] = useApiComp<Dict<ServerStatus>>('get_status')
21 + const [vfs] = useApiComp<{ root?: VfsNode }>('get_vfs')
22 + const [account] = useApi<Account>(username && 'get_account')
23 + const [cfg, reloadCfg] = useApiComp('get_config', { only: ['https_port', 'cert', 'private_key', 'proxies', 'ignore_proxies'] })
24 + if (!status || isValidElement(status))
25 + return status
26 const { http, https } = status
27 const goSecure = !http?.listening && https?.listening ? 's' : ''
28 const srv = goSecure ? https : (http?.listening && http)
@@ -37,21 +39,15 @@ export default function HomePage() {
39 ]]))
40 return h(Box, { display:'flex', gap: 2, flexDirection:'column' },
41 username && entry('', "Welcome "+username),
40 - !cfg ? spinner() :
41 - errors.length ? dontBotherWithKeys(errors.map(msg => entry('error', dontBotherWithKeys(msg))))
42 - : entry('success', "Server is working"),
43 - !vfs ? spinner()
44 - : !vfs.root?.children?.length && !vfs.root?.source
45 - ? entry('warning', "You have no files shared", SOLUTION_SEP, fsLink("add some"))
46 - : entry('', md("Here you manage your server. There is a _separated_ interface to access your shared files: "),
47 - h(Link, { target:'frontend', href: '/' }, "Frontend interface", h(Launch, { sx: { verticalAlign: 'sub', ml: '.2em' } }))),
48 - ! href && entry('warning', "Frontend unreachable: ",
49 - !cfg ? '...'
50 - : errors.length === 2 ? "both http and https are in error"
51 - : h(Fragment, {},
52 - ['http','https'].map(k => k + " " + (errorMap[k] ? "is in error" : "is off")).join(', '),
53 - !errors.length && h(Fragment, {}, SOLUTION_SEP, cfgLink("switch http or https on"))
54 - )
42 + errors.length ? dontBotherWithKeys(errors.map(msg => entry('error', dontBotherWithKeys(msg))))
43 + : entry('success', "Server is working"),
44 + !vfs || isValidElement(vfs) ? vfs
45 + : !vfs.root?.children?.length && !vfs.root?.source ? entry('warning', "You have no files shared", SOLUTION_SEP, fsLink("add some"))
46 + : entry('', md("Here you manage your server. There is a _separated_ interface to access your shared files: "),
47 + h(Link, { target:'frontend', href: '/' }, "Frontend interface", h(Launch, { sx: { verticalAlign: 'sub', ml: '.2em' } }))),
48 + !href && entry('warning', "Frontend unreachable: ",
49 + ['http','https'].map(k => k + " " + (errorMap[k] ? "is in error" : "is off")).join(', '),
50 + !errors.length && [ SOLUTION_SEP, cfgLink("switch http or https on") ]
51 ),
52 !account?.adminActualAccess && entry('', md("You are accessing on _localhost_ where permission is not required"),
53 SOLUTION_SEP, h(InLink, { to:'accounts' }, "give admin access to an account to be able to access from other computers") ),
admin/src/LogoutPage.ts
+5 -4
@@ -1,13 +1,14 @@
1 -import { createElement as h } from "react"
1 +import { createElement as h, isValidElement } from "react"
2 import { Alert, Box, Button } from '@mui/material'
3 -import { apiCall, useApi } from './api'
3 +import { apiCall, useApiComp } from './api'
4 import { alertDialog } from "./dialog"
5 import { useSnapState } from './state'
6
7 export default function LogoutPage() {
8 - const [cfg] = useApi('get_config', { only: [] })
8 + const [cfg] = useApiComp('get_config', { only: [] })
9 const { username } = useSnapState()
10 - if (!cfg) return null
10 + if (isValidElement(cfg))
11 + return cfg
12 if (!username)
13 return h(Alert, { severity: 'info' }, "You are not logged in, because authentication is not required on localhost")
14 return h(Box, { display: 'flex', flexDirection:'column', gap: 2 },
admin/src/api.ts
+11 -8
@@ -1,6 +1,6 @@
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, ReactElement, useCallback, useEffect, useMemo, useRef } from 'react'
3 +import { createElement as h, ReactElement, Fragment, useCallback, useEffect, useMemo, useRef } from 'react'
4 import { Dict, Falsy, getCookie, IconBtn, spinner, useStateMounted } from './misc'
5 import { Alert } from '@mui/material'
6 import _ from 'lodash'
@@ -8,11 +8,12 @@ import { state } from './state'
8 import { Refresh } from '@mui/icons-material'
9
10 export function useApiComp<T=any>(...args: Parameters<typeof useApi>): [T | ReactElement, ()=>void] {
11 - const [res, reload] = useApi<T>(...args)
11 + const [res, err, reload] = useApi<T>(...args)
12 return useMemo(() =>
13 - res === undefined ? [spinner(), reload]
14 - : res && res instanceof Error ? [ h(Alert, { severity: 'error' }, String(res), h(IconBtn, { icon: Refresh, onClick: reload, sx: { m:'-8px 0 -8px 16px' } })), reload ]
15 - : [res, reload],
13 + !args[0] ? [h(Fragment), reload]
14 + : err ? [ h(Alert, { severity: 'error' }, String(err), h(IconBtn, { icon: Refresh, onClick: reload, sx: { m:'-8px 0 -8px 16px' } })), reload ]
15 + : res === undefined ? [spinner(), reload]
16 + : [res, reload],
17 [res, reload])
18 }
19
@@ -46,20 +47,22 @@ export class ApiError extends Error {
47 }
48 }
49
49 -export function useApi<T=any>(cmd: string | Falsy, params?: object) : [T | undefined, ()=>void] {
50 +export function useApi<T=any>(cmd: string | Falsy, params?: object) : [T | undefined, undefined | Error, ()=>void] {
51 const [ret, setRet] = useStateMounted<T | undefined>(undefined)
52 + const [err, setErr] = useStateMounted<Error | undefined>(undefined)
53 const [forcer, setForcer] = useStateMounted(0)
54 const loadingRef = useRef(false)
55 useEffect(()=>{
56 setRet(undefined)
57 + setErr(undefined)
58 if (!cmd) return
59 loadingRef.current = true
60 apiCall(cmd, params)
58 - .then(setRet, setRet)
61 + .then(setRet, setErr)
62 .finally(()=> loadingRef.current = false)
63 }, [cmd, JSON.stringify(params), forcer]) //eslint-disable-line -- json-ize to detect deep changes
64 const reload = useCallback(()=> loadingRef.current || setForcer(v => v+1), [setForcer])
62 - return [ret, reload]
65 + return [ret, err, reload]
66 }
67
68 type EventHandler = (type:string, data?:any) => void