@samitouri / QOSami-HFS / commits / 9a32636b

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