admin/logs: new Options button
Massimo Melina committed
Dec 21, 2023 at 19:12 UTC
634d173534c7d788508252eb6d14c0e7622f04dd
7 files changed
+71
-37
admin/src/LogsPage.ts
+52
-4
@@ -4,13 +4,17 @@ import { createElement as h, Fragment, useMemo, useState } from 'react';
4
import { Box, Tab, Tabs, Tooltip } from '@mui/material'
5
import { API_URL, useApiList } from './api'
6
import { DataTable } from './DataTable'
7
-import { Dict, formatBytes, HTTP_UNAUTHORIZED, NBSP, prefix, shortenAgent, splitAt, tryJson, typedKeys } from '@hfs/shared'
7
+import { CFG, Dict, formatBytes, HTTP_UNAUTHORIZED, newDialog, prefix, shortenAgent, splitAt, tryJson, typedKeys, NBSP
8
+} from '@hfs/shared'
9
import { logLabels } from './OptionsPage'
9
-import { Flex, useBreakpoint, usePauseButton, useToggleButton, Country } from './mui';
10
+import { NetmaskField, Flex, IconBtn, useBreakpoint, usePauseButton, useToggleButton, WildcardsSupported, Country } from './mui';
11
import { GridColDef } from '@mui/x-data-grid'
12
import _ from 'lodash'
12
-import { SmartToy } from '@mui/icons-material'
13
+import { Settings, SmartToy } from '@mui/icons-material'
14
import md from './md'
15
+import { ConfigForm } from './ConfigForm'
16
+import { BoolField, SelectField } from '@hfs/mui-grid-form'
17
+import { useDialogBarColors } from './dialog'
18
19
export default function LogsPage() {
20
const [tab, setTab] = useState(0)
@@ -26,13 +30,57 @@ export default function LogsPage() {
30
return h(Fragment, {},
31
h(Flex, { gap: 0 },
32
h(Tabs, { value: tab, onChange(ev,i){ setTab(i) } },
29
- files.map(f => h(Tab, { label: _.get(shorterLabels, f) || logLabels[f], key: f })) ),
33
+ files.map(f => h(Tab, {
34
+ label: _.get(shorterLabels, f) || logLabels[f],
35
+ key: f,
36
+ sx: { minWidth: 0, px: { xs: 1.5, sm: 2 } } // save space
37
+ }))),
38
h(Box, { flex: 1 }),
39
showApiButton,
40
pauseButton,
41
+ h(IconBtn, { icon: Settings, title: "Options", onClick: showLogOptions })
42
),
43
h(LogFile, { key: tab, pause, showApi, file: files[tab] }), // without key, some state is accidentally preserved across files
44
)
45
+
46
+ function showLogOptions() {
47
+ newDialog({
48
+ title: "Log options",
49
+ dialogProps: { sx: { maxWidth: '40em' } },
50
+ Content() {
51
+ return h(ConfigForm<{
52
+ [CFG.log]: string
53
+ [CFG.error_log]: string
54
+ [CFG.log_rotation]: string
55
+ [CFG.dont_log_net]: string
56
+ [CFG.log_gui]: boolean
57
+ [CFG.log_api]: boolean
58
+ [CFG.log_ua]: boolean
59
+ }>, {
60
+ keys: [ CFG.log, CFG.error_log, CFG.log_rotation, CFG.dont_log_net, CFG.log_gui, CFG.log_api, CFG.log_ua ],
61
+ barSx: { gap: 2, width: '100%', ...useDialogBarColors() },
62
+ form: {
63
+ stickyBar: true,
64
+ fields: [
65
+ { k: CFG.log, label: logLabels.log, sm: 6, helperText: "Requests are logged here" },
66
+ { k: CFG.error_log, label: logLabels.error_log, sm: 6, placeholder: "errors go to main log",
67
+ helperText: "If you want errors in a different log"
68
+ },
69
+ { k: CFG.log_rotation, comp: SelectField, sm: 6, options: [{ value:'', label:"disabled" }, 'daily', 'weekly', 'monthly' ],
70
+ helperText: "To keep log-files smaller"
71
+ },
72
+ { k: CFG.dont_log_net, comp: NetmaskField, label: "Don't log address", sm: 6, placeholder: "no exception",
73
+ helperText: h(WildcardsSupported)
74
+ },
75
+ { k: CFG.log_gui, sm: 6, comp: BoolField, label: "Log interface loading", helperText: "Some requests are necessary to load the interface" },
76
+ { k: CFG.log_api, sm: 6, comp: BoolField, label: "Log API requests", helperText: "Requests for commands" },
77
+ { k: CFG.log_ua, sm: 6, comp: BoolField, label: "Log User-Agent", helperText: "Contains browser and possibly OS information" },
78
+ ]
79
+ }
80
+ })
81
+ }
82
+ })
83
+ }
84
}
85
86
function LogFile({ file, pause, showApi }: { file: string, pause?: boolean, showApi: boolean }) {
admin/src/OptionsPage.ts
+1
-22
@@ -8,7 +8,7 @@ import { Link as RouterLink } from 'react-router-dom'
8
import { CardMembership, EditNote, Refresh, Warning } from '@mui/icons-material'
9
import { Dict, MAX_TILE_SIZE, REPO_URL, isIpLocalHost, wait, with_, try_, ipForUrl, SORT_BY_OPTIONS, THEME_OPTIONS,
10
CFG } from './misc'
11
-import { iconTooltip, InLink, LinkBtn, modifiedSx, wikiLink, useBreakpoint } from './mui'
11
+import { iconTooltip, InLink, LinkBtn, modifiedSx, wikiLink, useBreakpoint, NetmaskField, WildcardsSupported } from './mui'
12
import { Form, BoolField, NumberField, SelectField, FieldProps, Field, StringField } from '@hfs/mui-grid-form';
13
import { ArrayField } from './ArrayField'
14
import FileField from './FileField'
@@ -37,8 +37,6 @@ export const logLabels = {
37
console: "Console",
38
}
39
40
-const NetmaskField = StringField
41
-
40
export default function OptionsPage() {
41
const { data, reload: reloadConfig, element } = useApiEx('get_config', { omit: ['vfs'] })
42
const snap = useSnapState()
@@ -188,21 +186,6 @@ export default function OptionsPage() {
186
helperText: md(`This code works similarly to [a plugin](${REPO_URL}blob/main/dev-plugins.md) (with some limitations)`)
187
},
188
191
- h(Section, { title: "Log" }),
192
- { k: 'log', label: logLabels.log, md: 3, helperText: "Requests are logged here" },
193
- { k: 'error_log', label: logLabels.error_log, md: 3, placeholder: "errors go to main log",
194
- helperText: "If you want errors in a different log"
195
- },
196
- { k: 'log_rotation', comp: SelectField, md: 3, options: [{ value:'', label:"disabled" }, 'daily', 'weekly', 'monthly' ],
197
- helperText: "To keep log-files smaller"
198
- },
199
- { k: 'dont_log_net', comp: NetmaskField, label: "Don't log address", md: 3, placeholder: "no exception",
200
- helperText: h(WildcardsSupported)
201
- },
202
- { k: 'log_gui', sm: 4, comp: BoolField, label: "Log interface loading", helperText: "Some requests are necessary to load the interface" },
203
- { k: 'log_api', sm: 4, comp: BoolField, label: "Log API requests", helperText: "Requests for commands" },
204
- { k: 'log_ua', sm: 4, comp: BoolField, label: "Log User-Agent", helperText: "Contains browser and possibly OS information" },
205
-
189
h(Section, { title: "Front-end", subtitle: "Following options affect only the front-end" }),
190
{ k: 'file_menu_on_link', comp: SelectField, label: "Access file menu", md: 4,
191
options: { "by clicking on file name": true, "by dedicated button": false }
@@ -347,10 +330,6 @@ function AllowedReferer({ label, value, onChange, error }: FieldProps<string>) {
330
)
331
}
332
350
-function WildcardsSupported() {
351
- return wikiLink('Wildcards', "Wildcards supported")
352
-}
353
-
333
export async function suggestMakingCert() {
334
return new Promise(resolve => {
335
const { close } = newDialog({
admin/src/mui.ts
+7
-1
@@ -8,7 +8,7 @@ import { Box, BoxProps, Breakpoint, ButtonProps, CircularProgress, IconButton, I
8
Tooltip, TooltipProps, useMediaQuery } from '@mui/material'
9
import { formatPerc, isIpLan, isIpLocalHost, prefix, WIKI_URL } from '../../src/cross'
10
import { dontBotherWithKeys, useBatch, useStateMounted } from '@hfs/shared'
11
-import { Promisable } from '@hfs/mui-grid-form'
11
+import { Promisable, StringField } from '@hfs/mui-grid-form'
12
import { alertDialog, confirmDialog, toast } from './dialog'
13
import { LoadingButton, LoadingButtonProps } from '@mui/lab'
14
import { Link as RouterLink } from 'react-router-dom'
@@ -79,6 +79,10 @@ export function wikiLink(uri: string, content: ReactNode) {
79
return h(Link, { href: WIKI_URL + uri, target: 'help' }, content)
80
}
81
82
+export function WildcardsSupported() {
83
+ return wikiLink('Wildcards', "Wildcards supported")
84
+}
85
+
86
export function reloadBtn(onClick: any, props?: any) {
87
return h(IconBtn, { icon: Refresh, title: "Reload", onClick, ...props })
88
}
@@ -234,6 +238,8 @@ export function useToggleButton(iconBtn: (state:boolean) => Omit<IconBtnProps, '
238
return [state, el] as const
239
}
240
241
+export const NetmaskField = StringField
242
+
243
export function Country({ code, ip, def, long, short }: { code: string, ip?: string, def?: ReactNode, long?: boolean, short?: boolean }) {
244
const good = ip && !isIpLocalHost(ip) && !isIpLan(ip)
245
const { data } = useBatch(code === undefined && good && ip2countryBatch, ip, { delay: 100 }) // query if necessary
src/apiMiddleware.ts
+2
-2
@@ -3,7 +3,7 @@
3
import Koa from 'koa'
4
import createSSE from './sse'
5
import { Readable } from 'stream'
6
-import { asyncGeneratorToReadable, removeStarting } from './misc'
6
+import { asyncGeneratorToReadable, CFG, removeStarting } from './misc'
7
import { HTTP_BAD_REQUEST, HTTP_FOOL, HTTP_NOT_FOUND } from './const'
8
import { defineConfig } from './config'
9
@@ -16,7 +16,7 @@ type ApiHandlerResult = Record<string,any> | ApiError | Readable | AsyncGenerato
16
export type ApiHandler = (params:any, ctx:Koa.Context) => ApiHandlerResult | Promise<ApiHandlerResult>
17
export type ApiHandlers = Record<string, ApiHandler>
18
19
-const logApi = defineConfig('log_api', true)
19
+const logApi = defineConfig(CFG.log_api, true)
20
21
export function apiMiddleware(apis: ApiHandlers) : Koa.Middleware {
22
return async (ctx) => {
src/cross.ts
+1
@@ -23,6 +23,7 @@ export const FRONTEND_OPTIONS = {
23
export const SORT_BY_OPTIONS = ['name', 'extension', 'size', 'time']
24
export const THEME_OPTIONS = { auto: '', light: 'light', dark: 'dark' }
25
export const CFG = constMap(['geo_enable', 'geo_allow', 'geo_list', 'geo_allow_unknown',
26
+ 'log', 'error_log', 'log_rotation', 'dont_log_net', 'log_gui', 'log_api', 'log_ua',
27
'max_downloads', 'max_downloads_per_ip', 'max_downloads_per_account', 'roots', 'roots_mandatory'])
28
export const LIST = { add: '+', remove: '-', update: '=', props: 'props', ready: 'ready', error: 'e' }
29
export type Dict<T=any> = Record<string, T>
src/log.ts
+6
-6
@@ -9,7 +9,7 @@ import { stat } from 'fs/promises'
9
import _ from 'lodash'
10
import { createFileWithPath, prepareFolder } from './util-files'
11
import { getCurrentUsername } from './auth'
12
-import { DAY, makeNetMatcher, tryJson, Dict, Falsy } from './misc'
12
+import { DAY, makeNetMatcher, tryJson, Dict, Falsy, CFG } from './misc'
13
import events from './events'
14
import { getConnection } from './connections'
15
import { app } from './index'
@@ -47,8 +47,8 @@ class Logger {
47
}
48
49
// we'll have names same as config keys. These are used also by the get_log api.
50
-const accessLogger = new Logger('log')
51
-const accessErrorLog = new Logger('error_log')
50
+const accessLogger = new Logger(CFG.log)
51
+const accessErrorLog = new Logger(CFG.error_log)
52
export const loggers = [accessLogger, accessErrorLog]
53
54
defineConfig(accessLogger.name, 'logs/access.log').sub(path => {
@@ -62,9 +62,9 @@ errorLogFile.sub(path => {
62
accessErrorLog.setPath(path)
63
})
64
65
-const logRotation = defineConfig('log_rotation', 'weekly')
66
-const dontLogNet = defineConfig('dont_log_net', '127.0.0.1|::1', v => makeNetMatcher(v))
67
-const logUA = defineConfig('log_ua', false)
65
+const logRotation = defineConfig(CFG.log_rotation, 'weekly')
66
+const dontLogNet = defineConfig(CFG.dont_log_net, '127.0.0.1|::1', v => makeNetMatcher(v))
67
+const logUA = defineConfig(CFG.log_ua, false)
68
69
const debounce = _.debounce(cb => cb(), 1000)
70
src/serveGuiFiles.ts
+2
-2
@@ -9,7 +9,7 @@ import { getPluginConfigFields, getPluginInfo, mapPlugins, pluginsConfig } from
9
import { refresh_session } from './api.auth'
10
import { ApiError } from './apiMiddleware'
11
import { join, extname } from 'path'
12
-import { FRONTEND_OPTIONS, getOrSet, newObj, onlyTruthy } from './misc'
12
+import { CFG, FRONTEND_OPTIONS, getOrSet, newObj, onlyTruthy } from './misc'
13
import { favicon, title } from './adminApis'
14
import { subscribe } from 'valtio/vanilla'
15
import { customHtmlState, getSection } from './customHtml'
@@ -17,7 +17,7 @@ import _ from 'lodash'
17
import { defineConfig, getConfig } from './config'
18
import { getLangData } from './lang'
19
20
-export const logGui = defineConfig('log_gui', false)
20
+export const logGui = defineConfig(CFG.log_gui, false)
21
_.each(FRONTEND_OPTIONS, (v,k) => defineConfig(k, v)) // define default values
22
23
// in case of dev env we have our static files within the 'dist' folder'