@samitouri / QOSami-HFS / commits / 078180b2

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