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'