@samitouri / QOSami-HFS / commits / 80723385

better code

Massimo Melina committed Aug 23, 2023 at 11:15 UTC 80723385003446213aafcbf9cf8f0efdc1c4b69d
7 files changed +36 -34
admin/src/FileForm.ts
+4 -13
@@ -15,17 +15,8 @@ import {
15 } from '@hfs/mui-grid-form'
16 import { apiCall, useApiEx } from './api'
17 import {
18 - basename,
19 - Btn,
20 - formatBytes,
21 - IconBtn,
22 - isEqualLax,
23 - modifiedSx,
24 - newDialog,
25 - objSameKeys,
26 - onlyTruthy,
27 - prefix,
28 - wikiLink
18 + basename, Btn, formatBytes, IconBtn, isEqualLax, LinkBtn, modifiedSx, newDialog, objSameKeys,
19 + onlyTruthy, prefix, wikiLink
20 } from './misc'
21 import { reloadVfs, VfsNode, VfsPerms, Who } from './VfsPage'
22 import md from './md'
@@ -214,8 +205,8 @@ function WhoField({ value, onChange, parent, inherit, accounts, helperText, show
205 }) ),
206 h(FormHelperText, {},
207 helperText,
217 - !isChildren && isDir && h(Link, {
218 - sx: { display: 'block', cursor: 'pointer', mt: -.5 },
208 + !isChildren && isDir && h(LinkBtn, {
209 + sx: { display: 'block', mt: -.5 },
210 onClick(event) {
211 if (thisValue === undefined) return
212 onChange(objectMode ? thisValue : { this: value }, { was: value, event })
admin/src/HomePage.ts
+2 -2
@@ -8,6 +8,7 @@ import {
8 dontBotherWithKeys,
9 Flex,
10 InLink,
11 + LinkBtn,
12 objSameKeys,
13 onlyTruthy,
14 prefix,
@@ -59,8 +60,7 @@ export default function HomePage() {
60 const errors = serverErrors && onlyTruthy(Object.entries(serverErrors).map(([k,v]) =>
61 v && [md(`Protocol <u>${k}</u> cannot work: `), v,
62 (isCertError(v) || isKeyError(v)) && [
62 - SOLUTION_SEP, h(Link, {
63 - sx: { cursor: 'pointer' },
63 + SOLUTION_SEP, h(LinkBtn, {
64 onClick() { makeCertAndSave().then(reloadCfg).then(reloadStatus) } },
65 "make one"
66 ), " or ", SOLUTION_SEP, cfgLink("provide adequate files")
admin/src/InternetPage.ts
+15 -9
@@ -3,7 +3,7 @@ import { Alert, Box, Button, CircularProgress, LinearProgress, Link } from '@mui
3 import { HomeWorkTwoTone, PublicTwoTone, RouterTwoTone } from '@mui/icons-material'
4 import { apiCall, useApiEx } from './api'
5 import { closeDialog, with_ } from '@hfs/shared'
6 -import { Flex } from './misc'
6 +import { Flex, LinkBtn } from './misc'
7 import { alertDialog, confirmDialog, promptDialog, toast } from './dialog'
8 import { NumberField } from '@hfs/mui-grid-form'
9 import md from './md'
@@ -29,9 +29,15 @@ export default function InternetPage() {
29 setVerifyAgain(false)
30 verify().then()
31 }, [verifyAgain, nat, loading])
32 - return h(Box, {},
33 - h(Alert, { severity: 'info', sx: { mb: 2 } }, "This page helps you making your server work on the Internet"),
34 - error ? "Error" : !nat ? h(CircularProgress) : h(Flex, { justifyContent: 'space-around', alignItems: 'center', maxWidth: '40em' },
32 + return h(Flex, { vert: true },
33 + h(Alert, { severity: 'info' }, "This page helps you making your server work on the Internet"),
34 + networkBox(),
35 + )
36 +
37 + function networkBox() {
38 + if (error) return "Error"
39 + if (!nat) return h(CircularProgress)
40 + return h(Flex, { justifyContent: 'space-around', alignItems: 'center', maxWidth: '40em' },
41 h(Device, { name: "Local network", icon: HomeWorkTwoTone, color: localColor, ip: nat?.localIp,
42 below: port && h(Box, { fontSize: 'smaller' }, "port ", port),
43 }),
@@ -40,21 +46,21 @@ export default function InternetPage() {
46 name: "Router", icon: RouterTwoTone, ip: nat?.gatewayIp,
47 color: nat?.mapped && (wrongMap ? 'warning' : 'success'),
48 below: mapping ? h(LinearProgress, { sx: { height: '1em' } })
43 - : h(Link, { fontSize: 'smaller', display: 'block', onClick: configure, sx: { cursor: 'pointer' } },
49 + : h(LinkBtn, { fontSize: 'smaller', display: 'block', onClick: configure },
50 "port ", wrongMap ? 'is wrong' : nat?.mapped ? nat.mapped.public.port : "unknown"),
51 }),
52 h(Sep),
53 h(Device, { name: "Internet", icon: PublicTwoTone, ip: nat?.publicIp,
54 color: checkResult ? 'success' : checkResult === false ? 'error' : doubleNat ? 'warning' : undefined,
55 below: checking ? h(LinearProgress, { sx: { height: '1em' } }) : h(Box, { fontSize: 'smaller' },
50 - doubleNat && h(Link, { sx: { cursor: 'pointer', display: 'block' }, onClick: () => alertDialog(MSG_ISP, 'warning') }, "Double NAT"),
56 + doubleNat && h(LinkBtn, { display: 'block', onClick: () => alertDialog(MSG_ISP, 'warning') }, "Double NAT"),
57 checkResult ? "Working!" : checkResult === false ? "Failed!" : '',
58 ' ',
53 - nat?.publicIp && h(Link, { onClick: verify, sx: { cursor: 'pointer' } }, "Verify")
59 + nat?.publicIp && h(LinkBtn, { onClick: verify }, "Verify")
60 )
61 }),
56 - ),
57 - )
62 + )
63 + }
64
65 async function verify(): Promise<any> {
66 setCheckResult(undefined)
admin/src/OptionsPage.ts
+2 -2
@@ -5,7 +5,7 @@ import { createElement as h, Fragment, useEffect, useRef } from 'react';
5 import { apiCall, useApi, useApiEx } from './api'
6 import { state, useSnapState } from './state'
7 import { Info, Refresh, Warning } from '@mui/icons-material'
8 -import { Dict, Flex, iconTooltip, modifiedSx, wikiLink, with_ } from './misc'
8 +import { Dict, Flex, iconTooltip, LinkBtn, modifiedSx, wikiLink, with_ } from './misc'
9 import { Form, BoolField, NumberField, SelectField, FieldProps, Field, StringField } from '@hfs/mui-grid-form';
10 import { ArrayField } from './ArrayField'
11 import FileField from './FileField'
@@ -98,7 +98,7 @@ export default function OptionsPage() {
98 },
99 httpsEnabled && { k: 'cert', comp: FileField, md: 4, label: "HTTPS certificate file",
100 helperText: wikiLink('HTTPS#certificate', "What is this?"),
101 - error: with_(status?.https.error, e => isCertError(e) && [e, ' - ', h(Link, { key: 'fix', sx: { cursor: 'pointer' }, onClick: makeCertAndSave }, "make one")]),
101 + error: with_(status?.https.error, e => isCertError(e) && [e, ' - ', h(LinkBtn, { key: 'fix', onClick: makeCertAndSave }, "make one")]),
102 },
103 httpsEnabled && { k: 'private_key', comp: FileField, md: 4, label: "HTTPS private key file",
104 ...with_(status?.https.error, e => isKeyError(e) ? { error: true, helperText: e } : null)
admin/src/misc.ts
+7 -2
@@ -8,7 +8,7 @@ import {
8 CircularProgress,
9 IconButton,
10 IconButtonProps,
11 - Link,
11 + Link, LinkProps,
12 Tooltip, TooltipProps,
13 useMediaQuery
14 } from '@mui/material'
@@ -133,6 +133,10 @@ export function InLink(props:any) {
133 export const Center = forwardRef((props: BoxProps, ref) =>
134 h(Box, { ref, display:'flex', height:'100%', width:'100%', justifyContent:'center', alignItems:'center', flexDirection: 'column', ...props }))
135
136 +export function LinkBtn({ ...rest }: LinkProps) {
137 + return h(Link, { ...rest, sx: { cursor: 'pointer', ...rest.sx } })
138 +}
139 +
140 export async function manipulateConfig(k: string, work:(data:any) => any) {
141 const cfg = await apiCall('get_config', { only: [k] })
142 const was = cfg[k]
@@ -199,12 +203,13 @@ export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgr
203 )
204 }
205
202 -export function Flex({ gap='.8em', vert=false, children=null, props={}, ...rest }) {
206 +export function Flex({ gap='.8em', vert=false, center=false, children=null, props={}, ...rest }) {
207 return h(Box, {
208 sx: {
209 display: 'flex',
210 gap,
211 flexDirection: vert ? 'column' : undefined,
212 + ...center && { alignItems: 'center', justifyContent: 'center' },
213 ...rest,
214 },
215 ...props
dev.md
+1
@@ -38,6 +38,7 @@ Alternatively you can run a development server, just be sure to load config from
38 - All objects that go in yaml should use snake_case.
39 - Reason: we want something that is both easy for the user and maps directly in our code.
40 Spaces and kebab-case don't play well with javascript and camel is less readable for the user.
41 +- API names should start with get_ if and only if they provide information without making changes.
42
43 # Project design
44
src/api.file_list.ts
+5 -6
@@ -20,17 +20,16 @@ import { HTTP_FOOL, HTTP_METHOD_NOT_ALLOWED, HTTP_NOT_FOUND } from './const'
20 import Koa from 'koa'
21
22 export const get_file_list: ApiHandler = async ({ uri, offset, limit, search, c }, ctx) => {
23 - const node = await urlToNode( uri || '/', ctx)
24 - const list = new SendListReadable()
23 + const node = await urlToNode(uri || '/', ctx)
24 + const list = ctx.get('accept') === 'text/event-stream' ? new SendListReadable() : undefined
25 if (!node)
26 return fail(HTTP_NOT_FOUND)
27 if (statusCodeForMissingPerm(node,'can_list',ctx))
28 return fail()
29 if (dirTraversal(search))
30 return fail(HTTP_FOOL)
31 - const sse = ctx.get('accept') === 'text/event-stream'
31 if (node.default)
33 - return (sse ? list.custom : _.identity)({ // sse will wrap the object in a 'custom' message, otherwise we plainly return the object
32 + return (list?.custom ?? _.identity)({ // sse will wrap the object in a 'custom' message, otherwise we plainly return the object
33 redirect: uri // tell the browser to access the folder (instead of using this api), so it will get the default file
34 })
35 if (!await nodeIsDirectory(node))
@@ -43,7 +42,7 @@ export const get_file_list: ApiHandler = async ({ uri, offset, limit, search, c
42 const can_upload = hasPermission(node, 'can_upload', ctx)
43 const can_delete = hasPermission(node, 'can_delete', ctx)
44 const props = { can_upload, can_delete, accept: node.accept }
46 - if (!sse)
45 + if (!list)
46 return { ...props, list: await asyncGeneratorToArray(produceEntries()) }
47 setTimeout(async () => {
48 if (can_upload || can_delete)
@@ -55,7 +54,7 @@ export const get_file_list: ApiHandler = async ({ uri, offset, limit, search, c
54 return list
55
56 function fail(code=ctx.status) {
58 - if (!sse)
57 + if (!list)
58 return new ApiError(code)
59 list.error(code, true)
60 return list