admin/plugins/installed: new order, first on top
Massimo Melina committed
Apr 4, 2025 at 00:44 UTC
078180b25ae909e56308b6c3783ec4c8358d9917
2 files changed
+30
-5
admin/src/InstalledPlugins.ts
+8
-4
@@ -1,7 +1,7 @@
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
3
import { apiCall, useApiEx, useApiList } from './api'
4
-import { createElement as h, Fragment, useEffect } from 'react'
4
+import { createElement as h, Fragment, useEffect, useRef, useState } from 'react'
5
import { Box, Breakpoint, Link, Paper, Table, TableCell, TableRow, useTheme } from '@mui/material'
6
import { DataTable, DataTableColumn } from './DataTable'
7
import {
@@ -9,7 +9,7 @@ import {
9
} from '@mui/icons-material'
10
import {
11
CFG, Html, HTTP_FAILED_DEPENDENCY, md, newObj, prefix, with_, xlate, formatTime, formatDate, replaceStringToReact,
12
- callable, tryJson
12
+ callable, tryJson, useAutoScroll
13
} from './misc'
14
import { alertDialog, confirmDialog, formDialog, toast } from './dialog'
15
import _ from 'lodash'
@@ -153,12 +153,16 @@ export default function InstalledPlugins({ updates }: { updates?: true }) {
153
const { list } = useApiList('get_plugin_log', { id }, {
154
map(x) { x.ts = new Date(x.ts) }
155
})
156
+ const autoScroll = useAutoScroll(list)
157
let lastDate: any
158
return h(Flex, { alignItems: 'stretch', justifyContent: 'center', flexWrap: 'wrap', flexDirection: showOptions ? undefined : 'column' },
159
h(Box, { maxWidth, minWidth: 'min-content' /*in case content requires more space (eg: reverse-proxy's table)*/ }, children),
160
list.length > 0 ? h(Paper, { elevation: 1, sx: { position: 'relative', fontFamily: 'monospace', flex: 1, minWidth: 'min(40em, 90vw)', minHeight: '20em', px: .5 } },
160
- h(Box, { my: .5, pb: .5, borderBottom: '1px solid' }, "Output (last on top)"),
161
- h(Box, { position: 'absolute', bottom: 0, top: '1.8em', left: 0, right: 0, sx: { overflowY: 'auto' } },
161
+ h(Box, { my: .5, pb: .5, borderBottom: '1px solid' }, "Output"),
162
+ h(Box, {
163
+ position: 'absolute', bottom: 0, top: '1.8em', left: 0, right: 0, sx: { overflowY: 'auto' },
164
+ ref: autoScroll,
165
+ },
166
h(Box, {
167
sx: {
168
textIndent: '-1em', pl: '1em',
shared/react.ts
+22
-1
@@ -5,7 +5,7 @@ import {
5
useCallback, useEffect, useMemo, useRef, useState
6
} from 'react'
7
import { useIsMounted, useWindowSize, useMediaQuery } from 'usehooks-ts'
8
-import { Callback, Falsy } from '.'
8
+import { Callback, domOn, Falsy } from '.'
9
import _ from 'lodash'
10
11
export function useStateMounted<T>(init: T) {
@@ -192,3 +192,24 @@ export const isMac = navigator.platform.match('Mac')
192
export function isCtrlKey(ev: KeyboardEvent) {
193
return (ev.ctrlKey || isMac && ev.metaKey) && ev.key
194
}
195
+
196
+export function useAutoScroll(dependency: any) {
197
+ const ref = useRef<any>()
198
+ const lastScrollListenerRef = useRef<any>()
199
+ const [goBottom, setGoBottom] = useState(true)
200
+ useEffect(() => {
201
+ const { current: el } = ref
202
+ if (goBottom)
203
+ el?.scrollTo(0, el.scrollHeight)
204
+ }, [goBottom, dependency])
205
+ return useCallback((el: any) => {
206
+ ref.current = el
207
+ // reinstall listener
208
+ lastScrollListenerRef.current?.()
209
+ lastScrollListenerRef.current = domOn('scroll', ev => {
210
+ const el = ev.target as HTMLDivElement
211
+ if (!el) return
212
+ setGoBottom(el.scrollTop + el.clientHeight >= el.scrollHeight - 3)
213
+ }, { target: el })
214
+ }, [])
215
+}
\ No newline at end of file