@samitouri / QOSami-HFS / commits / 68cd5a71

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