admin/config: better field for https files

Massimo Melina committed Apr 9, 2022 at 18:42 UTC 8dd1da5315271d3552bfb73907e5cffeeee71688
5 files changed +104 -18
admin/src/ConfigPage.ts
+8 -4
@@ -2,13 +2,14 @@
2
3 import { Box, Button, FormHelperText, Link } from '@mui/material';
4 import { createElement as h, isValidElement, useEffect, useRef } from 'react';
5 -import { apiCall, useApi, useApiComp } from './api'
5 +import { apiCall, useApiComp } from './api'
6 import { state, useSnapState } from './state'
7 import { Info, Refresh } from '@mui/icons-material'
8 import { Dict, modifiedSx } from './misc'
9 import { subscribeKey } from 'valtio/utils'
10 import { Form, BoolField, NumberField, SelectField, FieldProps, Field } from './Form';
11 import StringStringField from './StringStringField'
12 +import FileField from './FileField'
13 import { alertDialog, closeDialog, confirmDialog, formDialog, newDialog, waitDialog } from './dialog'
14 import { proxyWarning } from './HomePage'
15
@@ -78,8 +79,8 @@ export default function ConfigPage() {
79 return v
80 }
81 },
81 - values.https_port >= 0 && { k: 'cert', label: "HTTPS certificate file" },
82 - values.https_port >= 0 && { k: 'private_key', label: "HTTPS private key file" },
82 + values.https_port >= 0 && { k: 'cert', comp: FileField, label: "HTTPS certificate file" },
83 + values.https_port >= 0 && { k: 'private_key', comp: FileField, label: "HTTPS private key file" },
84 { k: 'max_kbps', ...maxSpeedDefaults, label: "Limit output KB/s", helperText: "Doesn't apply to localhost" },
85 { k: 'max_kbps_per_ip', ...maxSpeedDefaults, label: "Limit output KB/s per-ip" },
86 ...Object.entries(logLabels).map(a => ({ k: a[0], label: a[1] })),
@@ -178,7 +179,10 @@ function suggestMakingCert() {
179 h(Info), "You are enabling HTTPs. It needs a valid certificate + private key to work."
180 ),
181 h(Box, { mt: 4, display: 'flex', gap: 1, justifyContent: 'space-around', },
181 - h(Button, { variant: 'contained', onClick(){ closeDialog(); makeCertAndSave() } }, "Help me!"),
182 + h(Button, { variant: 'contained', onClick(){
183 + closeDialog()
184 + makeCertAndSave().then()
185 + } }, "Help me!"),
186 h(Button, { onClick: closeDialog }, "I will handle the matter myself"),
187 ),
188 )
admin/src/FileField.ts new
+47
@@ -0,0 +1,47 @@
1 +import { FieldProps, StringField } from './Form'
2 +import { createElement as h } from 'react'
3 +import { InputAdornment } from '@mui/material'
4 +import { Eject } from '@mui/icons-material'
5 +import { IconBtn } from './misc'
6 +import { newDialog } from '@hfs/shared/lib/dialogs'
7 +import FilePicker from './FilePicker'
8 +import { apiCall } from './api'
9 +
10 +export default function FileField({ value, onChange, ...props }: FieldProps<string>) {
11 + return h(StringField, {
12 + ...props,
13 + value,
14 + onChange,
15 + InputProps: {
16 + endAdornment: h(InputAdornment, { position: 'end' },
17 + h(IconBtn, {
18 + icon: Eject,
19 + title: "Browse files...",
20 + edge: 'end',
21 + onClick() {
22 + const close = newDialog({
23 + title: "Pick a file",
24 + dialogProps: { sx:{ minWidth:'min(90vw, 40em)', minHeight: 'calc(100vh - 9em)' } },
25 + Content,
26 + })
27 +
28 + function Content() {
29 + return h(FilePicker, {
30 + multiple: false,
31 + from: value,
32 + async onSelect(sel) {
33 + let one = sel?.[0]
34 + if (!one) return
35 + const cwd = (await apiCall('get_cwd'))?.path
36 + if (one.startsWith(cwd))
37 + one = one.slice(cwd.length+1)
38 + onChange(one, { was: value, event: 'picker' })
39 + close()
40 + }
41 + })
42 + }
43 + },
44 + }))
45 + }
46 + })
47 +}
admin/src/FilePicker.ts
+19 -12
@@ -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, Fragment, useEffect, useMemo, useState } from 'react'
4 -import { useApi, useApiList } from './api'
4 +import { apiCall, useApiList } from './api'
5 import _ from 'lodash'
6 import {
7 Alert,
@@ -14,7 +14,7 @@ import {
14 TextField,
15 Typography
16 } from '@mui/material'
17 -import { enforceFinal, formatBytes, isWindowsDrive, spinner } from './misc'
17 +import { enforceFinal, formatBytes, isWindowsDrive, spinner, pathJoin, dirname, isAbsolutePath } from './misc'
18 import { ArrowUpward, Home } from '@mui/icons-material'
19 import { StringField } from './Form'
20 import { FileIcon, FolderIcon } from './VfsTree'
@@ -23,16 +23,24 @@ import AutoSizer from "react-virtualized-auto-sizer"
23
24 export interface DirEntry { n:string, s?:number, m?:string, c?:string, k?:'d' }
25
26 -export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }) {
27 - const [cwd, setCwd] = useState('')
26 +interface FilePickerProps {
27 + onSelect:(v:string[])=>void
28 + multiple?: boolean
29 + from?: string
30 +}
31 +export default function FilePicker({ onSelect, multiple=true, from }: FilePickerProps) {
32 + const passedDir = useMemo(() => from && dirname(from), [from])
33 + const [cwd, setCwd] = useState(from && passedDir || '')
34 const [ready, setReady] = useState(false)
29 - const [gotCwd] = useApi('get_cwd')
35 useEffect(() => {
31 - if (gotCwd) {
32 - setCwd(gotCwd.path)
36 + if (passedDir && isAbsolutePath(passedDir))
37 + return setReady(true)
38 + apiCall('get_cwd').then(res => {
39 + if (typeof res.path === 'string')
40 + setCwd(pathJoin(res.path, passedDir))
41 setReady(true)
34 - }
35 - }, [gotCwd])
42 + })
43 + }, [])
44 const { list, error, loading } = useApiList<DirEntry>(ready && 'ls', { path: cwd })
45 useEffect(() => {
46 setSel([])
@@ -92,7 +100,7 @@ export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }
100 onSelect([cwdPostfixed + it.n])
101 }
102 },
95 - h(Checkbox, {
103 + multiple && h(Checkbox, {
104 checked: sel.includes(it.n),
105 onClick(ev) {
106 const id = it.n
@@ -114,7 +122,7 @@ export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }
122 }),
123 ),
124 h(Box, { display:'flex', gap: 1 },
117 - h(Button, {
125 + multiple && h(Button, {
126 variant: 'contained',
127 disabled: !sel.length,
128 sx: { minWidth: 'max-content' },
@@ -135,4 +143,3 @@ export default function FilePicker({ onSelect }: { onSelect:(v:string[])=>void }
143 )
144 )
145 }
138 -
admin/src/misc.ts
+27 -1
@@ -7,7 +7,7 @@ import { SxProps } from '@mui/system'
7 import { SvgIconComponent } from '@mui/icons-material'
8 import { alertDialog } from './dialog'
9 import { apiCall } from './api'
10 -import { useStateMounted } from '@hfs/shared'
10 +import { onlyTruthy, useStateMounted } from '@hfs/shared'
11 import {} from '@hfs/shared' // without this we get weird warnings by webpack
12 export * from '@hfs/shared'
13
@@ -68,3 +68,29 @@ export async function manipulateConfig(k: string, work:(data:any) => any) {
68 export function typedKeys<T>(o: T) {
69 return Object.keys(o) as (keyof T)[]
70 }
71 +
72 +export function dirname(s: string) {
73 + let i = s.lastIndexOf('/')
74 + if (i < 0)
75 + i = s.lastIndexOf('\\')
76 + return i < 0 ? '' : s.slice(0, i)
77 +}
78 +
79 +export function isAbsolutePath(s: string) {
80 + return s && (s[0] === '/' || isWindowsDrive(s.slice(0,2)))
81 +}
82 +
83 +export function pathJoin(...args: any[]) {
84 + const delimiter = findFirst(args, x => /\\|\//.exec('\\a/b')?.[0])
85 + const good = onlyTruthy(args.map(x => x == null ? '' : String(x)))
86 + return good.map((x, i) => i === good.length-1 || x.endsWith('\\') || x.endsWith('/') ? x : x + delimiter)
87 + .join('')
88 +}
89 +
90 +export function findFirst<I=any, O=any>(a: I[], cb:(v:I)=>O): any {
91 + for (const x of a) {
92 + const ret = cb(x)
93 + if (ret !== undefined)
94 + return ret
95 + }
96 +}
todo.md
+3 -1
@@ -7,10 +7,12 @@
7 - admin/fs: support insert/delete key
8 - admin/fs: button "copy url to clipboard"
9 - admin/monitor: show some info on what folder is browsing
10 +- anti-brute for logins
11 +- admin/stats: total bytes sent
12 +- admin/stats: list of IPs seen
13 - if specified config is a folder, check for file config.yaml inside
14 - frontend: ok button to inputDialogs
15 - admin: in a group, show linked accounts
13 -- admin/config: use filepicker for https files
16 - admin: warn in case of items with same name
17 - command line help --help
18 - admin/plugins