leverage new useApiEx()

Massimo Melina committed Jun 3, 2022 at 14:20 UTC d5c6ac8e726d42bc2b46aaadea1e17f073b3a144
8 files changed +65 -65
admin/src/AccountsPage.ts
+9 -9
@@ -1,7 +1,7 @@
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 { isValidElement, createElement as h, useState, useEffect, Fragment, useRef } from "react"
4 -import { apiCall, useApiComp } from './api'
3 +import { createElement as h, useState, useEffect, Fragment, useRef } from "react"
4 +import { apiCall, useApiEx } from './api'
5 import { Box, Button, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
6 import { Delete, Group, MilitaryTech, Person, PersonAdd, Refresh } from '@mui/icons-material'
7 import { BoolField, Form, MultiSelectField } from '@hfs/mui-grid-form'
@@ -32,17 +32,17 @@ export interface Account {
32 }
33
34 export default function AccountsPage() {
35 - const [res, reload] = useApiComp('get_accounts')
35 + const { data, reload, element } = useApiEx('get_accounts')
36 const [sel, setSel] = useState<string[] | 'new-group' | 'new-user'>([])
37 const selectionMode = Array.isArray(sel)
38 const styles = useStyles()
39 useEffect(() => { // if accounts are reloaded, review the selection to remove elements that don't exist anymore
40 - if (Array.isArray(res?.list) && selectionMode)
41 - setSel( sel.filter(u => res.list.find((e:any) => e?.username === u)) ) // remove elements that don't exist anymore
42 - }, [res]) //eslint-disable-line -- Don't fall for its suggestion to add `sel` here: we modify it and declaring it as a dependency would cause a logical loop
43 - if (isValidElement(res))
44 - return res
45 - const { list }: { list: Account[] } = res
40 + if (Array.isArray(data?.list) && selectionMode)
41 + setSel( sel.filter(u => data.list.find((e:any) => e?.username === u)) ) // remove elements that don't exist anymore
42 + }, [data]) //eslint-disable-line -- Don't fall for its suggestion to add `sel` here: we modify it and declaring it as a dependency would cause a logical loop
43 + if (element)
44 + return element
45 + const { list }: { list: Account[] } = data
46 return h(Grid, { container: true, maxWidth: '80em' },
47 h(Grid, { item: true, xs: 12 },
48 h(Box, {
admin/src/ConfigPage.ts
+12 -11
@@ -1,8 +1,8 @@
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 { Box, Button, FormHelperText, Link } from '@mui/material';
4 -import { createElement as h, isValidElement, useEffect, useRef } from 'react';
5 -import { apiCall, useApi, useApiEx, useApiComp } from './api'
4 +import { createElement as h, useEffect, useRef } from 'react';
5 +import { apiCall, useApi, useApiEx } from './api'
6 import { state, useSnapState } from './state'
7 import { Info, Refresh } from '@mui/icons-material'
8 import { Dict, modifiedSx } from './misc'
@@ -24,23 +24,24 @@ export const logLabels = {
24 }
25
26 export default function ConfigPage() {
27 - const [res, reloadConfig] = useApiComp('get_config', { omit: ['vfs'] })
27 + const { data, reload: reloadConfig, element } = useApiEx('get_config', { omit: ['vfs'] })
28 let snap = useSnapState()
29 - const statusApi = useApiEx(res && 'get_status')
29 + const statusApi = useApiEx(data && 'get_status')
30 const status = statusApi.data
31 - useEffect(statusApi.reload, [res]) //eslint-disable-line
31 + const reloadStatus = statusApi.reload
32 + useEffect(reloadStatus, [data]) //eslint-disable-line
33
33 - exposedReloadStatus = statusApi.reload
34 + exposedReloadStatus = reloadStatus
35 useEffect(() => () => exposedReloadStatus = undefined, []) // clear on unmount
36
37 const admins = useApi('get_admins')[0]?.list
38
38 - if (isValidElement(res))
39 - return res
39 + if (element)
40 + return element
41 if (statusApi.error)
42 return statusApi.element
43 const { changes } = snap
43 - const values = (loaded !== res) ? (state.config = loaded = res) : snap.config
44 + const values = (loaded !== data) ? (state.config = loaded = data) : snap.config
45 const maxSpeedDefaults = {
46 comp: NumberField,
47 min: 1,
@@ -62,7 +63,7 @@ export default function ConfigPage() {
63 addToBar: [h(Button, {
64 onClick() {
65 reloadConfig()
65 - statusApi.reload()
66 + reloadStatus()
67 },
68 startIcon: h(Refresh),
69 }, "Reload")],
@@ -140,7 +141,7 @@ export default function ConfigPage() {
141 await alertDialog("You are being redirected but in some cases this may fail. Hold on tight!", 'warning')
142 return window.location.href = loc.protocol + '//' + loc.hostname + ':' + newPort + loc.pathname
143 }
143 - setTimeout(statusApi.reload, 2000) // in case of busy port, finding the name of the process can be a lengthy task. Worst case we'll get the generic error message
144 + setTimeout(reloadStatus, 2000) // in case of busy port, finding the name of the process can be a lengthy task. Worst case we'll get the generic error message
145 Object.assign(loaded, values) // since changes are recalculated subscribing state.config, but it depends on 'loaded' to (which cannot be subscribed), be sure to update loaded first
146 recalculateChanges()
147 toast("Changes applied", 'success')
admin/src/FileForm.ts
+6 -6
@@ -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 } from './state'
4 -import { createElement as h, isValidElement, useEffect, useMemo, useState } from 'react'
4 +import { createElement as h, useEffect, useMemo, useState } from 'react'
5 import { Alert, Button } from '@mui/material'
6 import { BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, SelectField } from '@hfs/mui-grid-form'
7 -import { apiCall, useApiComp } from './api'
7 +import { apiCall, useApiEx } from './api'
8 import { formatBytes, isEqualLax, modifiedSx, onlyTruthy } from './misc'
9 import { reloadVfs, VfsNode, Who } from './VfsPage'
10 import md from './md'
@@ -40,10 +40,10 @@ export default function FileForm({ file }: { file: VfsNode }) {
40 const showCanSee = (values.can_read ?? inheritedPerms.can_read) === true
41 const showTimestamps = hasSource && Boolean(values.ctime)
42
43 - let [accountsRes] = useApiComp<{ list: Account[] }>('get_accounts')
44 - if (isValidElement(accountsRes))
45 - return accountsRes
46 - const accounts = accountsRes.list
43 + const { data, element } = useApiEx<{ list: Account[] }>('get_accounts')
44 + if (element || !data)
45 + return element
46 + const accounts = data.list
47
48 return h(Form, {
49 values,
admin/src/HomePage.ts
+9 -9
@@ -1,8 +1,8 @@
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, isValidElement } from 'react'
4 -import { Box, Button, Link } from '@mui/material'
5 -import { apiCall, useApi, useApiComp, useApiList } from './api'
3 +import { createElement as h } from 'react'
4 +import { Box, Button, LinearProgress, Link } from '@mui/material'
5 +import { apiCall, useApi, useApiEx, useApiList } 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'
@@ -17,13 +17,13 @@ interface ServerStatus { listening: boolean, port: number, error?: string, busy?
17 export default function HomePage() {
18 const SOLUTION_SEP = " — "
19 const { username } = useSnapState()
20 - const [status, reloadStatus] = useApiComp<Dict<ServerStatus>>('get_status')
21 - const [vfs] = useApiComp<{ root?: VfsNode }>('get_vfs')
20 + const { data: status, reload: reloadStatus, element: statusEl } = useApiEx<Dict<ServerStatus>>('get_status')
21 + const { data: vfs } = useApiEx<{ 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'] })
23 + const { data: cfg, reload: reloadCfg } = useApiEx('get_config', { only: ['https_port', 'cert', 'private_key', 'proxies', 'ignore_proxies'] })
24 const { list: plugins } = useApiList('get_plugins')
25 - if (!status || isValidElement(status))
26 - return status
25 + if (statusEl || !status)
26 + return statusEl
27 const { http, https } = status
28 const goSecure = !http?.listening && https?.listening ? 's' : ''
29 const srv = goSecure ? https : (http?.listening && http)
@@ -41,7 +41,7 @@ export default function HomePage() {
41 username && entry('', "Welcome "+username),
42 errors.length ? dontBotherWithKeys(errors.map(msg => entry('error', dontBotherWithKeys(msg))))
43 : entry('success', "Server is working"),
44 - !vfs || isValidElement(vfs) ? vfs
44 + !vfs ? h(LinearProgress)
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' } }))),
admin/src/LogoutPage.ts
+5 -5
@@ -1,14 +1,14 @@
1 -import { createElement as h, isValidElement } from "react"
1 +import { createElement as h } from "react"
2 import { Alert, Box, Button } from '@mui/material'
3 -import { apiCall, useApiComp } from './api'
3 +import { apiCall, useApiEx } from './api'
4 import { alertDialog } from "./dialog"
5 import { useSnapState } from './state'
6
7 export default function LogoutPage() {
8 - const [cfg] = useApiComp('get_config', { only: [] }) // sort of noop
8 + const { element } = useApiEx('get_config', { only: [] }) // sort of noop, just to get the 'element' part
9 const { username } = useSnapState()
10 - if (isValidElement(cfg))
11 - return cfg
10 + if (element)
11 + return element
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/MonitorPage.ts
+9 -10
@@ -1,8 +1,8 @@
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 _ from "lodash"
4 -import { isValidElement, createElement as h, useMemo, Fragment, useState } from "react"
5 -import { apiCall, useApiComp, useApiList } from "./api"
4 +import { createElement as h, useMemo, Fragment, useState } from "react"
5 +import { apiCall, useApiEx, useApiList } from "./api"
6 import { PauseCircle, PlayCircle, Delete, Lock, Block, FolderZip } from '@mui/icons-material'
7 import { Box, Chip } from '@mui/material'
8 import { DataGrid } from "@mui/x-data-grid"
@@ -21,20 +21,19 @@ export default function MonitorPage() {
21 const isoDateRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
22
23 function MoreInfo() {
24 - const [res] = useApiComp('get_status')
24 + const { data, element } = useApiEx('get_status')
25 return !useBreakpoint('md') ? null
26 - : isValidElement(res) ? res :
27 - h(Box, { display: 'flex', flexWrap: 'wrap', gap: '1em', mb: 2 },
28 - pair('started'),
29 - pair('http', "HTTP", port),
30 - pair('https', "HTTPS", port),
31 - )
26 + : element || h(Box, { display: 'flex', flexWrap: 'wrap', gap: '1em', mb: 2 },
27 + pair('started'),
28 + pair('http', "HTTP", port),
29 + pair('https', "HTTPS", port),
30 + )
31
32 type Color = Parameters<typeof Chip>[0]['color']
33 type Render = (v:any) => [string, Color?]
34
35 function pair(k: string, label: string='', render?:Render) {
37 - let v = _.get(res, k)
36 + let v = _.get(data, k)
37 if (v === undefined)
38 return null
39 if (typeof v === 'string' && isoDateRe.test(v))
admin/src/PermField.ts
+6 -6
@@ -1,16 +1,16 @@
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 { Dict, useStateMounted } from './misc'
4 -import { createElement as h, Fragment, isValidElement } from 'react'
4 +import { createElement as h, Fragment } from 'react'
5 import { Button, Grid } from '@mui/material'
6 import { Field, FieldProps, SelectField } from '@hfs/mui-grid-form'
7 import _ from 'lodash'
8 -import { useApiComp } from './api'
8 +import { useApiEx } from './api'
9
10 export default function PermField({ label, value, onChange }: FieldProps<Dict<string> | null> & { keyLabel:string }) {
11 const [temp, setTemp] = useStateMounted<string|undefined>(undefined)
12 - const [res] = useApiComp('get_usernames')
13 - const usernames = res.list
12 + const { data, element } = useApiEx('get_usernames')
13 + const usernames = data?.list || []
14
15 const permOptions = [{ label:'read', value:'r' }, { label:'none', value:'' }]
16 const usernamesLeft = _.difference(usernames, Object.keys(value||{}))
@@ -18,13 +18,13 @@ export default function PermField({ label, value, onChange }: FieldProps<Dict<st
18 return h(Grid, { container: true },
19 label && h(Grid, { item: true, xs: 12, pl: 2, py: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' },
20 label,
21 - !isValidElement(res) && h(Button, {
21 + !element && h(Button, {
22 onClick(event){
23 setTemp(undefined)
24 onChange(null, { event, was: value })
25 }
26 }, 'Clear')),
27 - isValidElement(res) ? res : h(Fragment, {},
27 + element || h(Fragment, {},
28 // existing entries
29 Object.entries(value||{}).map(([username, perm]) => [
30 h(Grid, { key:'k', item: true, xs: 6 },
admin/src/VfsPage.ts
+9 -9
@@ -1,7 +1,7 @@
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, isValidElement, useEffect, useMemo, useState } from 'react'
4 -import { useApi, useApiComp } from './api'
3 +import { createElement as h, useEffect, useMemo, useState } from 'react'
4 +import { useApi, useApiEx } from './api'
5 import { Alert, Grid, Link, List, ListItem, ListItemText, Typography } from '@mui/material'
6 import { state, useSnapState } from './state'
7 import VfsMenuBar from './VfsMenuBar'
@@ -17,14 +17,14 @@ let selectOnReload: string[] | undefined
17 export default function VfsPage() {
18 const [id2node] = useState(() => new Map<string, VfsNode>())
19 const snap = useSnapState()
20 - const [res, reload] = useApiComp('get_vfs')
20 + const { data, reload, element } = useApiEx('get_vfs')
21 useMemo(() => snap.vfs || reload(), [snap.vfs, reload])
22 useEffect(() => {
23 state.vfs = undefined
24 - if (!res) return
24 + if (!data) return
25 // rebuild id2node
26 id2node.clear()
27 - const { root } = res
27 + const { root } = data
28 if (!root) return
29 recur(root) // this must be done before state change that would cause Tree to render and expecting id2node
30 root.isRoot = true
@@ -45,7 +45,7 @@ export default function VfsPage() {
45 recur(n, (pre && node.id) + '/', node)
46 }
47
48 - }, [res, id2node])
48 + }, [data, id2node])
49 const [status] = useApi(window.location.host === 'localhost' && 'get_status')
50 const urls = useMemo(() =>
51 typeof status === 'object'
@@ -54,11 +54,11 @@ export default function VfsPage() {
54 url => url.includes('[')
55 ),
56 [status])
57 - if (isValidElement(res)) {
57 + if (element) {
58 id2node.clear()
59 - return res
59 + return element
60 }
61 - const anythingShared = !res?.root?.children?.length && !res?.root?.source
61 + const anythingShared = !data?.root?.children?.length && !data?.root?.source
62 const alert: AlertProps | false = anythingShared ? {
63 severity: 'warning',
64 children: "Add something to your shared files — click Add"