admin/logs: inverted + pause button
Massimo Melina committed
Nov 11, 2023 at 16:08 UTC
68cd5a718b179c5fc5b7f3ec4e8b44cb4515245b
4 files changed
+50
-24
admin/src/LogsPage.ts
+12
-5
@@ -6,20 +6,27 @@ import { API_URL, useApiList } from './api'
6
import { DataTable } from './DataTable'
7
import { formatBytes, tryJson } from '@hfs/shared'
8
import { logLabels } from './OptionsPage'
9
-import { typedKeys, xlate } from './misc';
9
+import { Flex, typedKeys, usePauseButton } from './misc';
10
11
export default function LogsPage() {
12
const [tab, setTab] = useState(0)
13
const files = typedKeys(logLabels)
14
+ const { pause, pauseButton } = usePauseButton()
15
return h(Fragment, {},
15
- h(Tabs, { value: tab, onChange(ev,i){ setTab(i) } },
16
- files.map(f => h(Tab, { label: logLabels[f], key: f })) ),
17
- h(LogFile, { key: tab, file: files[tab] }), // without key, some state is accidentally preserved across files
16
+ h(Flex, { gap: 0 },
17
+ h(Tabs, { value: tab, onChange(ev,i){ setTab(i) } },
18
+ files.map(f => h(Tab, { label: logLabels[f], key: f })) ),
19
+ h(Box, { flex: 1 }),
20
+ pauseButton,
21
+ ),
22
+ h(LogFile, { key: tab, pause, file: files[tab] }), // without key, some state is accidentally preserved across files
23
)
24
}
25
21
-function LogFile({ file }: { file: string }) {
26
+function LogFile({ file, pause }: { file: string, pause?: boolean }) {
27
const { list, error, connecting } = useApiList('get_log', { file }, {
28
+ invert: true,
29
+ pause,
30
map(x) {
31
const { extra } = x
32
if (!extra) return
admin/src/MonitorPage.ts
+15
-11
@@ -6,7 +6,18 @@ import { apiCall, useApiEvents, useApiEx, useApiList } from "./api"
6
import { PauseCircle, PlayCircle, LinkOff, Lock, Block, FolderZip, Upload, Download } from '@mui/icons-material'
7
import { Box, Chip, ChipProps, Tooltip } from '@mui/material'
8
import { DataTable } from './DataTable'
9
-import { formatBytes, IconBtn, IconProgress, iconTooltip, ipForUrl, manipulateConfig, useBreakpoint, useBatch, CFG } from "./misc"
9
+import {
10
+ formatBytes,
11
+ IconBtn,
12
+ IconProgress,
13
+ iconTooltip,
14
+ ipForUrl,
15
+ manipulateConfig,
16
+ useBreakpoint,
17
+ useBatch,
18
+ CFG,
19
+ usePauseButton
20
+} from "./misc"
21
import { Field, SelectField } from '@hfs/mui-grid-form'
22
import { StandardCSSProperties } from '@mui/system/styleFunctionSx/StandardCssProperties'
23
import { toast } from "./dialog"
@@ -98,10 +109,10 @@ function Connections() {
109
const { list, error, props } = useApiList('get_connections')
110
const config = useApiEx('get_config', { only: [CFG.geo_enable] })
111
const [filtered, setFiltered] = useState(true)
101
- const [paused, setPaused] = useState(false)
112
+ const { pause, pauseButton } = usePauseButton()
113
const rows = useMemo(() =>
114
list?.filter((x: any) => !filtered || x.op).map((x: any, id: number) => ({ id, ...x })),
104
- [!paused && list, filtered]) //eslint-disable-line
115
+ [!pause && list, filtered]) //eslint-disable-line
116
return h(Fragment, {},
117
h(Box, { display: 'flex', alignItems: 'center' },
118
h(SelectField as Field<boolean>, {
@@ -112,14 +123,7 @@ function Connections() {
123
}),
124
125
h(Box, { flex: 1 }),
115
- h(IconBtn, {
116
- title: paused ? "Resume" : "Pause",
117
- icon: paused ? PlayCircle : PauseCircle,
118
- sx: { mr: 1 },
119
- onClick() {
120
- setPaused(!paused)
121
- }
122
- }),
126
+ pauseButton,
127
),
128
h(DataTable, {
129
error,
admin/src/api.ts
+7
-4
@@ -35,7 +35,7 @@ export function useApiEx<T=any>(...args: Parameters<typeof useApi>) {
35
}
36
}
37
38
-export function useApiList<T=any, S=T>(cmd:string|Falsy, params: Dict={}, { map }: { map?: (rec: S) => T }={}) {
38
+export function useApiList<T=any, S=T>(cmd:string|Falsy, params: Dict={}, { map, invert, pause }: { pause?: boolean, invert?: boolean, map?: (rec: S) => T }={}) {
39
const [list, setList] = useStateMounted<T[]>([])
40
const [props, setProps] = useStateMounted<any>(undefined)
41
const [error, setError] = useStateMounted<any>(undefined)
@@ -44,13 +44,16 @@ export function useApiList<T=any, S=T>(cmd:string|Falsy, params: Dict={}, { map
44
const [initializing, setInitializing] = useStateMounted(true)
45
const [reloader, setReloader] = useState(0)
46
const idGenerator = useRef(0)
47
+ const [pausedList, setPausedList] = useState<typeof list | undefined>()
48
+ useEffect(() => setPausedList(pause ? list : undefined), [pause])
49
useEffect(() => {
50
if (!cmd) return
51
const bufferAdd: T[] = []
52
const apply = _.debounce(() => {
53
const chunk = bufferAdd.splice(0, Infinity)
52
- if (chunk.length)
53
- setList(list => [ ...list, ...chunk ])
54
+ if (!chunk.length) return
55
+ if (invert) chunk.reverse() // setList callback can be called twice (and will, in dev)
56
+ setList(list => invert ? [ ...chunk, ...list ] : [ ...list, ...chunk ])
57
}, 1000, { maxWait: 1000 })
58
setError(undefined)
59
setLoading(true)
@@ -145,7 +148,7 @@ export function useApiList<T=any, S=T>(cmd:string|Falsy, params: Dict={}, { map
148
apply.flush()
149
}
150
}, [reloader, cmd, JSON.stringify(params)]) //eslint-disable-line
148
- return { list, props, loading, error, initializing, connecting, setList, updateList, updateEntry, reload }
151
+ return { list: pausedList ?? list, props, loading, error, initializing, connecting, setList, updateList, updateEntry, reload }
152
153
function reload() {
154
setReloader(x => x + 1)
admin/src/mui.ts
+16
-4
@@ -1,9 +1,9 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
// all content here is shared between client and server
3
4
-import { Refresh, SvgIconComponent } from '@mui/icons-material'
4
+import { PauseCircle, PlayCircle, Refresh, SvgIconComponent } from '@mui/icons-material'
5
import { SxProps } from '@mui/system'
6
-import { createElement as h, FC, forwardRef, Fragment, ReactNode } from 'react'
6
+import { createElement as h, FC, forwardRef, Fragment, ReactNode, useState } from 'react'
7
import { Box, BoxProps, Breakpoint, ButtonProps, CircularProgress, IconButton, IconButtonProps, Link, LinkProps,
8
Tooltip, TooltipProps, useMediaQuery } from '@mui/material'
9
import { formatPerc, WIKI_URL } from '../../src/cross'
@@ -51,11 +51,12 @@ export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgr
51
)
52
}
53
54
-export function Flex({ gap='.8em', vert=false, center=false, children=null, props={}, ...rest }) {
54
+type FlexProps = SxProps & { vert?: boolean, center?: boolean, children?: ReactNode, props?: BoxProps }
55
+export function Flex({ vert=false, center=false, children=null, props={}, ...rest }: FlexProps) {
56
return h(Box, {
57
sx: {
58
display: 'flex',
58
- gap,
59
+ gap: '.8em',
60
flexDirection: vert ? 'column' : undefined,
61
alignItems: vert ? undefined : 'center',
62
...center && { justifyContent: 'center' },
@@ -197,3 +198,14 @@ export const Center = forwardRef((props: BoxProps, ref) =>
198
export function LinkBtn({ ...rest }: LinkProps) {
199
return h(Link, { ...rest, sx: { cursor: 'pointer', ...rest.sx } })
200
}
201
+
202
+export function usePauseButton() {
203
+ const [pause, setPause] = useState(false)
204
+ const el = h(IconBtn, {
205
+ title: pause ? "Resume" : "Pause",
206
+ icon: pause ? PlayCircle : PauseCircle,
207
+ size: 'small',
208
+ onClick() { setPause(x => !x) }
209
+ })
210
+ return { pause, pauseButton: el }
211
+}
\ No newline at end of file