admin/options: reorganized in sections
Massimo Melina committed
May 8, 2024 at 16:20 UTC
9a32636b755562573f9e0adb62b4cf8504e58f0e
6 files changed
+115
-97
admin/src/ConfigForm.ts
+10
-5
@@ -4,25 +4,30 @@ import { createElement as h, useEffect, useState, Dispatch } from 'react'
4
import _ from 'lodash'
5
import { IconBtn, modifiedProps } from './mui'
6
import { RestartAlt } from '@mui/icons-material'
7
-import { Callback } from '../../src/cross'
7
+import { Callback, onlyTruthy } from '../../src/cross'
8
9
type FormRest<T> = Omit<FormProps<T>, 'values' | 'set' | 'save'> & Partial<Pick<FormProps<T>, 'save'>>
10
export function ConfigForm<T=any>({ keys, form, saveOnChange, onSave, ...rest }: Partial<FormRest<T>> & {
11
- keys: (keyof T)[],
11
+ keys?: (keyof T)[],
12
form: FormRest<T> | ((values: T, optional: { setValues: Dispatch<T> }) => FormRest<T>),
13
onSave?: Callback,
14
saveOnChange?: boolean
15
}) {
16
- const config = useApiEx('get_config', { only: keys })
16
+ const [keys_, setKeys_] = useState(keys)
17
+ const config = useApiEx(keys_ && 'get_config', { only: keys_ })
18
const [values, setValues] = useState<any>(config.data)
19
useEffect(() => setValues((v: any) => config.data || v), [config.data])
20
const modified = values && !_.isEqual(values, config.data)
21
useEffect(() => {
22
if (modified && saveOnChange) save()
23
}, [modified])
24
+ const formProps = _.isFunction(form) ? form(values, { setValues }) : form
25
+ useEffect(() => {
26
+ if (!keys) // autodetect keys
27
+ setKeys_(onlyTruthy(formProps.fields.map(x => (x as any)?.k)))
28
+ }, [keys])
29
if (!values)
30
return config.element
25
- const formProps = _.isFunction(form) ? form(values, { setValues }) : form
31
return h(Form, {
32
values,
33
set(v, k) {
@@ -32,7 +37,7 @@ export function ConfigForm<T=any>({ keys, form, saveOnChange, onSave, ...rest }:
37
onClick: save,
38
...modifiedProps(modified),
39
},
35
- ...Array.isArray(formProps) ? { fields: formProps } : formProps,
40
+ ...formProps,
41
...rest,
42
barSx: { gap: 1, ...rest.barSx },
43
addToBar: [
admin/src/HomePage.ts
+28
-18
@@ -15,6 +15,8 @@ import { Account } from './AccountsPage'
15
import _ from 'lodash'
16
import { subscribeKey } from 'valtio/utils'
17
import { SwitchThemeBtn } from './theme'
18
+import { BoolField } from '@hfs/mui-grid-form'
19
+import { ConfigForm } from './ConfigForm'
20
21
interface ServerStatus { listening: boolean, port: number, error?: string, busy?: string }
22
@@ -33,7 +35,7 @@ export default function HomePage() {
35
const { data: status, reload: reloadStatus, element: statusEl } = useApiEx<Status>('get_status')
36
const { data: vfs } = useApiEx<{ root?: VfsNode }>('get_vfs')
37
const { data: account } = useApiEx<Account>(username && 'get_account')
36
- const cfg = useApiEx('get_config', { only: ['https_port', 'cert', 'private_key', 'proxies', 'update_to_beta'] })
38
+ const cfg = useApiEx('get_config', { only: ['https_port', 'cert', 'private_key', 'proxies'] })
39
const { list: plugins } = useApiList('get_plugins')
40
const [checkPlugins, setCheckPlugins] = useState(false)
41
const { list: pluginUpdates} = useApiList(checkPlugins && 'get_plugin_updates')
@@ -95,23 +97,31 @@ export default function HomePage() {
97
icon: UpdateIcon,
98
onClick: () => update()
99
}, "Update from local file")
98
- : !updates ? h(Btn, {
99
- variant: 'outlined',
100
- icon: UpdateIcon,
101
- onClick() {
102
- setCheckPlugins(true)
103
- return apiCall('check_update').then(x => setUpdates(x.options), alertDialog)
104
- },
105
- async onContextMenu(ev) {
106
- ev.preventDefault()
107
- if (!status.updatePossible)
108
- return alertDialog("Automatic update is only for binary versions", 'warning')
109
- const res = await promptDialog("Enter a link to the zip to install")
110
- if (res)
111
- await update(res)
112
- },
113
- title: status.updatePossible && "Right-click if you want to install a zip",
114
- }, "Check for updates")
100
+ : !updates ? h(Flex, { flexWrap: 'wrap' },
101
+ h(Btn, {
102
+ variant: 'outlined',
103
+ icon: UpdateIcon,
104
+ onClick() {
105
+ setCheckPlugins(true)
106
+ return apiCall('check_update').then(x => setUpdates(x.options), alertDialog)
107
+ },
108
+ async onContextMenu(ev) {
109
+ ev.preventDefault()
110
+ if (!status.updatePossible)
111
+ return alertDialog("Automatic update is only for binary versions", 'warning')
112
+ const res = await promptDialog("Enter a link to the zip to install")
113
+ if (res)
114
+ await update(res)
115
+ },
116
+ title: status.updatePossible && "Right-click if you want to install a zip",
117
+ }, "Check for updates"),
118
+ h(ConfigForm, {
119
+ saveOnChange: true,
120
+ form: { fields: [
121
+ { k: 'update_to_beta', comp: BoolField, label: "Include beta versions" },
122
+ ] }
123
+ })
124
+ )
125
: with_(_.find(updates, 'isNewer'), newer =>
126
!updates.length || !status.updatePossible && !newer ? entry('', "No update available")
127
: newer && !status.updatePossible ? entry('success', `Version ${newer.name} available`)
admin/src/InternetPage.ts
+2
-4
@@ -72,7 +72,6 @@ export default function InternetPage() {
72
h(ConfigForm<{
73
[CFG.dynamic_dns_url]: string,
74
}>, {
75
- keys: [CFG.dynamic_dns_url],
75
form: (v, { setValues }) => ({
76
fields: [
77
h(Flex, {},
@@ -123,7 +122,7 @@ export default function InternetPage() {
122
keys: [ CFG.geo_enable, CFG.geo_allow, CFG.geo_list, CFG.geo_allow_unknown ],
123
form: values => ({ fields: [
124
{ k: CFG.geo_enable, comp: BoolField, label: "Enable", helperText: md("Necessary database will be downloaded every month (2MB). Service is made possibly thanks to [IP2Location](https://www.ip2location.com).") },
126
- ...!values[CFG.geo_enable] ? [] : [
125
+ ...!values?.[CFG.geo_enable] ? [] : [
126
{
127
k: CFG.geo_allow,
128
comp: SelectField,
@@ -259,11 +258,10 @@ export default function InternetPage() {
258
onSave() {
259
status.reload() // this config is affecting status data
260
},
262
- keys: [CFG.roots, CFG.force_address],
261
form: {
262
fields: [
263
{
266
- k: 'roots',
264
+ k: CFG.roots,
265
label: false,
266
helperText: "You can decide different home-folders (in the VFS) for different domains, a bit like virtual hosts. If none is matched, the default home will be used.",
267
comp: ArrayField,
admin/src/LogsPage.ts
+1
-10
@@ -51,16 +51,7 @@ export default function LogsPage() {
51
title: "Log options",
52
dialogProps: { sx: { maxWidth: '40em' } },
53
Content() {
54
- return h(ConfigForm<{
55
- [CFG.log]: string
56
- [CFG.error_log]: string
57
- [CFG.log_rotation]: string
58
- [CFG.dont_log_net]: string
59
- [CFG.log_gui]: boolean
60
- [CFG.log_api]: boolean
61
- [CFG.log_ua]: boolean
62
- }>, {
63
- keys: [ CFG.log, CFG.error_log, CFG.log_rotation, CFG.dont_log_net, CFG.log_gui, CFG.log_api, CFG.log_ua ],
54
+ return h(ConfigForm, {
55
barSx: { gap: 2, width: '100%', ...useDialogBarColors() },
56
form: {
57
stickyBar: true,
admin/src/OptionsPage.ts
+72
-58
@@ -100,81 +100,52 @@ export default function OptionsPage() {
100
return { sm: 6 }
101
},
102
fields: [
103
- { k: 'port', comp: PortField, md: 4, label:"HTTP port", status: status?.http||true, suggestedPort: 80 },
104
- { k: 'https_port', comp: PortField, md: 4, label: "HTTPS port", status: status?.https||true, suggestedPort: 443,
103
+ h(Section, { title: "Networking" }),
104
+ { k: 'port', comp: PortField, label:"HTTP port", status: status?.http||true, suggestedPort: 80 },
105
+ { k: 'https_port', comp: PortField, label: "HTTPS port", status: status?.https||true, suggestedPort: 443,
106
onChange(v: number) {
107
if (v >= 0 && !httpsEnabled && !values.cert)
108
void suggestMakingCert()
109
return v
110
}
111
},
111
- status && { k: 'listen_interface', comp: SelectField, md: 4, options: [{ label: "any", value: '' }, '127.0.0.1', '::1', ...status?.ips] },
112
- httpsEnabled && values.port >= 0 && { k: 'force_https', comp: BoolField, md: 4, label: "Force HTTPS",
113
- helperText: "Not applied to localhost"
114
- },
115
- httpsEnabled && { k: 'cert', comp: FileField, md: 4, label: "HTTPS certificate file",
112
+ httpsEnabled && { k: 'cert', comp: FileField, sm: 4, label: "HTTPS certificate file",
113
helperText: wikiLink('HTTPS#certificate', "What is this?"),
114
error: with_(status?.https.error, e => isCertError(e) && (
115
status.https.listening ? e
116
: [e, ' - ', h(LinkBtn, { key: 'fix', onClick: suggestMakingCert }, "make one")] )),
117
},
121
- httpsEnabled && { k: 'private_key', comp: FileField, md: 4, label: "HTTPS private key file",
118
+ httpsEnabled && { k: 'private_key', comp: FileField, sm: 4, label: "HTTPS private key file",
119
...with_(status?.https.error, e => isKeyError(e) ? { error: true, helperText: e } : null)
120
},
124
- { k: 'favicon', comp: FileField, placeholder: "None", fileMask: '*.png|*.ico|*.jpg|*.jpeg|*.gif|*.svg',
125
- helperText: "The icon associated to your website" },
126
- { k: 'allowed_referer', placeholder: "any", label: "Links from other websites", comp: AllowedReferer, sm: 12, md: 6 },
127
- { k: 'open_browser_at_start', comp: BoolField, label: "Open Admin-panel at start",
128
- helperText: "Browser is automatically launched with HFS"
129
- },
130
- { k: 'localhost_admin', comp: BoolField, label: "Unprotected admin on localhost",
131
- getError: x => !x && admins?.length===0 && "First create at least one admin account",
132
- helperText: "Access Admin-panel without entering credentials"
121
+
122
+ httpsEnabled && { k: 'force_https', comp: BoolField, label: "Force HTTPS", sm: 4, disabled: !httpsEnabled || values.port < 0,
123
+ helperText: "Not applied to localhost"
124
},
134
- { k: 'max_kbps', ...maxSpeedDefaults, label: "Limit output", helperText: "Doesn't apply to localhost" },
135
- { k: 'max_kbps_per_ip', ...maxSpeedDefaults, label: "Limit output per-IP" },
125
+
126
+ { k: 'listen_interface', comp: SelectField, sm: 4, options: [{ label: "any", value: '' }, '127.0.0.1', '::1', ...status?.ips||[]] },
127
+ { k: 'max_kbps', ...maxSpeedDefaults, sm: 4, label: "Limit output", helperText: "Doesn't apply to localhost" },
128
+ { k: 'max_kbps_per_ip', ...maxSpeedDefaults, sm: 4, label: "Limit output per-IP" },
129
+
130
{ k : CFG.max_downloads, ...maxDownloadsDefaults, helperText: "Number of simultaneous downloads" },
131
{ k : CFG.max_downloads_per_ip, ...maxDownloadsDefaults, label: "Max downloads per-IP" },
132
{ k : CFG.max_downloads_per_account, ...maxDownloadsDefaults, label: "Max downloads per-account", helperText: "Overrides other limits" },
139
- { k: 'dont_overwrite_uploading', comp: BoolField, sm: 12, md: 6, label: "Don't overwrite uploading",
140
- helperText: "Files will be numbered to avoid overwriting" },
141
- { k: 'delete_unfinished_uploads_after', comp: NumberField, md: 3, min : 0, unit: "seconds", placeholder: "Never",
142
- helperText: "Leave empty to never delete" },
143
- { k: 'min_available_mb', comp: NumberField, md: 3, min : 0, unit: "MBytes", placeholder: "None",
144
- label: "Min. available disk space", helperText: "Reject uploads that don't comply" },
145
- { k: 'keep_session_alive', comp: BoolField, helperText: "Keeps you logged in while the page is left open and the computer is on" },
146
- { k: 'session_duration', comp: NumberField, sm: 3, min: 5, unit: "seconds", required: true },
147
- { k: 'zip_calculate_size_for_seconds', comp: NumberField, sm: 3, label: "Calculate ZIP size for", unit: "seconds",
148
- helperText: "If time is not enough, the browser will not show download percentage" },
149
- { k: 'update_to_beta', comp: BoolField, helperText: "Include betas searching updates" },
133
+
134
{ k: 'admin_net', comp: NetmaskField, label: "Admin-panel accessible from", placeholder: "any address",
135
helperText: h(Fragment, {}, "IP address of browser machine. ", h(WildcardsSupported))
136
},
153
- { k: 'descript_ion', comp: BoolField, label: "Support file DESCRIPT.ION", helperText: "Old file format, used for comments" },
154
- { k: 'descript_ion_encoding', label: "Encoding of file DESCRIPT.ION", comp: SelectField, disabled: !values.descript_ion,
155
- options: ['utf8',720,775,819,850,852,862,869,874,808, ..._.range(1250,1257),10029,20866,21866] },
156
- { k: 'proxies', comp: NumberField, min: 0, max: 9, label: "How many HTTP proxies between this server and users?",
137
+ { k: 'localhost_admin', comp: BoolField, label: "Unprotected admin on localhost",
138
+ getError: x => !x && admins?.length===0 && "First create at least one admin account",
139
+ helperText: "Access Admin-panel without entering credentials"
140
+ },
141
+
142
+ { k: 'proxies', comp: NumberField, min: 0, max: 9, label: "Number of HTTP proxies",
143
error: proxyWarning(values, status),
144
helperText: "Wrong number will prevent detection of users' IP address"
145
},
160
- { k: 'mime', comp: ArrayField, label: false, reorder: true, prepend: true, md: 6,
161
- fields: [
162
- { k: 'k', label: "File mask", helperText: h(WildcardsSupported), $width: 1, $column: {
163
- renderCell: ({ value, id }: any) => h('code', {},
164
- value,
165
- value === '*' && id < _.size(values.mime) - 1
166
- && iconTooltip(Warning, md("Mime with `*` should be the last, because first matching row applies"), {
167
- color: 'warning.main', ml: 1
168
- }))
169
- } },
170
- { k: 'v', label: "Mime type", placeholder: "auto", $width: 2,
171
- toField: (x: any) => x === 'auto' ? '' : x, fromField: (x: string) => !x ? 'auto' : x.toLowerCase(),
172
- helperText: "Leave empty to get automatic value", },
173
- ],
174
- toField: x => Object.entries(x || {}).map(([k,v]) => ({ k, v })),
175
- fromField: x => Object.fromEntries(x.map((row: any) => [row.k, row.v])),
176
- },
177
- { k: 'block', label: false, comp: ArrayField, prepend: true, sm: 12,
146
+ { k: 'allowed_referer', placeholder: "any", label: "Links from other websites", comp: AllowedReferer, },
147
+
148
+ { k: 'block', label: false, comp: ArrayField, prepend: true, sm: true,
149
fields: [
150
{ k: 'ip', label: "Blocked IP", sm: 6, required: true, helperText: h(WildcardsSupported) },
151
{ k: 'expire', $type: 'dateTime', minDate: new Date(), sm: 6, helperText: "Leave empty for no expiration" },
@@ -189,22 +160,65 @@ export default function OptionsPage() {
160
{ k: 'comment' },
161
],
162
},
192
- { k: 'server_code', comp: TextEditorField, sm: 12, getError: v => try_(() => new Function(v) && null, e => e.message),
193
- helperText: md(`This code works similarly to [a plugin](${REPO_URL}blob/main/dev-plugins.md) (with some limitations)`)
194
- },
163
164
h(Section, { title: "Front-end", subtitle: "Following options affect only the front-end" }),
165
{ k: 'file_menu_on_link', comp: SelectField, label: "Access file menu", md: 4,
166
options: { "by clicking on file name": true, "by dedicated button": false }
167
},
168
{ k: 'title', md: 8, helperText: "You can see this in the tab of your browser" },
169
+
170
{ k: 'auto_play_seconds', comp: NumberField, xs: 6, sm: 3, min: 1, max: 10000, label: "Auto-play seconds delay" },
171
{ k: 'tile_size', comp: NumberField, xs: 6, sm: 3, min: 0, max: MAX_TILE_SIZE, label: "Default tiles size", helperText: "Zero = list mode" },
172
{ k: 'theme', comp: SelectField, xs: 6, sm: 3, options: THEME_OPTIONS },
173
{ k: 'sort_by', comp: SelectField, xs: 6, sm: 3, options: SORT_BY_OPTIONS },
205
- { k: 'invert_order', comp: BoolField, xs: 6, sm: 4, md: 3, },
206
- { k: 'folders_first', comp: BoolField, xs: 6, sm: 4, md: 3, },
207
- { k: 'sort_numerics', comp: BoolField, xs: 6, sm: 4, md: 3, label: "Sort numeric names" },
174
+
175
+ { k: 'invert_order', comp: BoolField, xs: 6, sm: 4, md: 3, },
176
+ { k: 'folders_first', comp: BoolField, xs: 6, sm: 4, md: 3, },
177
+ { k: 'sort_numerics', comp: BoolField, xs: 6, sm: 4, md: true, label: "Sort numeric names" },
178
+ { k: 'favicon', comp: FileField, placeholder: "None", fileMask: '*.png|*.ico|*.jpg|*.jpeg|*.gif|*.svg', sm: 12,
179
+ helperText: "The icon associated to your website" },
180
+
181
+ h(Section, { title: "Others" }),
182
+ { k: 'dont_overwrite_uploading', comp: BoolField, sm: 4, md: 6, label: "Don't overwrite uploading",
183
+ helperText: "Files will be numbered to avoid overwriting" },
184
+ { k: 'delete_unfinished_uploads_after', comp: NumberField, sm: 4, md: 3, min : 0, unit: "seconds", placeholder: "Never",
185
+ helperText: "Leave empty to never delete" },
186
+ { k: 'min_available_mb', comp: NumberField, sm: 4, md: 3, min : 0, unit: "MBytes", placeholder: "None",
187
+ label: "Min. available disk space", helperText: "Reject uploads that don't comply" },
188
+
189
+ { k: 'keep_session_alive', comp: BoolField, sm: true, helperText: "Keeps you logged in while the page is left open and the computer is on" },
190
+ { k: 'session_duration', comp: NumberField, sm: 4, md: 3, min: 5, unit: "seconds", required: true },
191
+ { k: 'zip_calculate_size_for_seconds', comp: NumberField, sm: 4, md: 3, label: "Calculate ZIP size for", unit: "seconds",
192
+ helperText: "If time is not enough, the browser will not show download percentage" },
193
+
194
+ { k: 'descript_ion', comp: BoolField, label: "Enable comments", helperText: "In file DESCRIPT.ION" },
195
+ { k: 'descript_ion_encoding', label: "Encoding of file DESCRIPT.ION", comp: SelectField, disabled: !values.descript_ion,
196
+ options: ['utf8',720,775,819,850,852,862,869,874,808, ..._.range(1250,1257),10029,20866,21866] },
197
+
198
+ { k: 'open_browser_at_start', comp: BoolField, label: "Open Admin-panel at start",
199
+ helperText: "Browser is automatically launched with HFS"
200
+ },
201
+ { k: 'mime', comp: ArrayField, label: false, reorder: true, prepend: true, md: 6,
202
+ fields: [
203
+ { k: 'k', label: "File mask", helperText: h(WildcardsSupported), $width: 1, $column: {
204
+ renderCell: ({ value, id }: any) => h('code', {},
205
+ value,
206
+ value === '*' && id < _.size(values.mime) - 1
207
+ && iconTooltip(Warning, md("Mime with `*` should be the last, because first matching row applies"), {
208
+ color: 'warning.main', ml: 1
209
+ }))
210
+ } },
211
+ { k: 'v', label: "Mime type", placeholder: "auto", $width: 2,
212
+ toField: (x: any) => x === 'auto' ? '' : x, fromField: (x: string) => !x ? 'auto' : x.toLowerCase(),
213
+ helperText: "Leave empty to get automatic value", },
214
+ ],
215
+ toField: x => Object.entries(x || {}).map(([k,v]) => ({ k, v })),
216
+ fromField: x => Object.fromEntries(x.map((row: any) => [row.k, row.v])),
217
+ },
218
+ { k: 'server_code', comp: TextEditorField, sm: 12, getError: v => try_(() => new Function(v) && null, e => e.message),
219
+ helperText: md(`This code works similarly to [a plugin](${REPO_URL}blob/main/dev-plugins.md) (with some limitations)`)
220
+ },
221
+
222
]
223
})
224
mui-grid-form/misc-fields.ts
+2
-2
@@ -66,9 +66,9 @@ export function BoolField({ label='', value, onChange, setApi, helperText, error
66
onChange((event.target as any).checked, { event, was: value })
67
}
68
})
69
- return h(Box, { ml: 1, mt: 1, sx: error ? { color: 'error.main', outlineOffset: 6, outline: '1px solid' } : undefined },
69
+ return h(Box, { ml: 1, sx: error ? { color: 'error.main', outlineOffset: 6, outline: '1px solid' } : undefined },
70
h(FormControlLabel, { label, control, labelPlacement: 'end', ...props.size==='small' && { sx: { '& .MuiFormControlLabel-label': { fontSize: '.9rem' } } } }),
71
- helperText && h(FormHelperText, { error }, helperText)
71
+ helperText && h(FormHelperText, { sx: { mt: 0 }, error }, helperText)
72
)
73
}
74