@samitouri / QOSami-HFS / commits / 66ae8fd0

admin/options: nicer "block" dialog

Massimo Melina committed Mar 22, 2025 at 11:42 UTC 66ae8fd0d2f509d19351bb3959bbd6d10da68c31
7 files changed +49 -33
admin/src/ArrayField.ts
+28 -8
@@ -3,24 +3,43 @@
3 import { createElement as h, Fragment, useMemo, useState } from 'react'
4 import { callable, DialogOptions, Dict, Functionable, isOrderedEqual, setHidden, swap } from './misc'
5 import { Add, Edit, Delete, ArrowUpward, ArrowDownward, Undo, Check } from '@mui/icons-material'
6 -import { formDialog } from './dialog'
6 +import { FormDialog, formDialog } from './dialog'
7 import { GridActionsCellItem, GridAlignment, GridColDef } from '@mui/x-data-grid'
8 import { BoolField, FieldDescriptor, FieldProps, labelFromKey } from '@hfs/mui-grid-form'
9 import { Box, FormHelperText, FormLabel } from '@mui/material'
10 import { DateTimeField } from './DateTimeField'
11 import _ from 'lodash'
12 import { Center, Flex, IconBtn, useBreakpoint } from './mui'
13 -import { DataTable } from './DataTable'
13 +import { DataTable, DataTableColumn } from './DataTable'
14
15 -type ArrayFieldProps<T> = FieldProps<T[]> & { fields: Functionable<FieldDescriptor[]>, height?: number, reorder?: boolean, prepend?: boolean, autoRowHeight?: boolean, dialog?: Omit<DialogOptions, 'Content'> }
16 -export function ArrayField<T extends object>({ label, helperText, fields, value, onChange, onError, setApi, reorder, prepend, noRows, valuesForAdd, autoRowHeight, dialog, ...rest }: ArrayFieldProps<T>) {
15 +type ArrayFieldProps<T> = FieldProps<T[]> & {
16 + fields: Functionable<FieldDescriptor[] & {
17 + $width?: number,
18 + $column?: Partial<DataTableColumn>,
19 + $type?: string
20 + $hideUnder?: DataTableColumn['hideUnder'],
21 + showIf: (values: any) => unknown, // truthy
22 + $render?: GridColDef['renderCell'],
23 + $mergeRender: DataTableColumn['mergeRender'],
24 + }>,
25 + height?: number,
26 + reorder?: boolean,
27 + prepend?: boolean,
28 + autoRowHeight?: boolean,
29 + form?: Partial<FormDialog<any>>,
30 + dialog?: Partial<DialogOptions>,
31 + details?: boolean
32 +}
33 +export function ArrayField<T extends object>({ label, helperText, fields, value, onChange, onError, setApi, reorder, prepend, noRows, valuesForAdd, autoRowHeight, dialog, form, details, ...rest }: ArrayFieldProps<T>) {
34 if (!Array.isArray(value)) // avoid crash if non-array values are passed, especially developing plugins
35 value = []
36 const rows = useMemo(() => value!.map((x,$idx) =>
37 setHidden({ ...x } as any, x.hasOwnProperty('id') ? { $idx } : { id: $idx })),
38 [JSON.stringify(value)]) //eslint-disable-line
22 - const form = (values: any) => ({
23 - fields: callable(fields, values).map(({ $width, $column, $type, $hideUnder, showIf, $render, $mergeRender, ...rest }) => (!showIf || showIf(values)) && _.defaults(rest, byType[$type]?.field))
39 + const formProp = (values: any) => ({
40 + fields: callable(fields, values).map(({ $width, $column, $type, $hideUnder, showIf, $render, $mergeRender, ...rest }) =>
41 + (!showIf || showIf(values)) && _.defaults(rest, byType[$type]?.field)),
42 + ...form,
43 })
44 setApi?.({ isEqual: isOrderedEqual }) // don't rely on stringify, as it wouldn't work with non-json values
45 const [undo, setUndo] = useState<typeof value>()
@@ -32,6 +51,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
51 h(Box, { ...rest },
52 h(DataTable, {
53 rows,
54 + details,
55 ...autoRowHeight && { getRowHeight: () => 'auto' as const },
56 ...!useBreakpoint('sm') && { compact: true },
57 sx: {
@@ -79,7 +99,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
99 title,
100 size: 'small',
101 onClick: ev =>
82 - formDialog<T>({ form, title, values: valuesForAdd, dialogProps: dialog }).then(x => {
102 + formDialog<T>({ form: formProp, title, values: valuesForAdd, dialogProps: dialog }).then(x => {
103 if (!x) return
104 const newValue = value?.slice() || []
105 if (prepend) newValue.unshift(x)
@@ -107,7 +127,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
127 ev.stopPropagation()
128 const res = await formDialog<T>({
129 values: row as any,
110 - form,
130 + form: formProp,
131 title: h(Fragment, {}, title, label && ' - ', label),
132 dialogProps: dialog
133 })
admin/src/DataTable.ts
+6 -6
@@ -12,8 +12,7 @@ import { useDebounce } from 'usehooks-ts'
12 const ACTIONS = 'Actions'
13
14 export type DataTableColumn<R extends GridValidRowModel=any> = GridColDef<R> & {
15 - hidden?: boolean
16 - hideUnder?: Breakpoint | number
15 + hideUnder?: Breakpoint | number | boolean
16 dialogHidden?: boolean
17 sx?: SxProps | Callback<GridRenderCellParams, SxProps>
18 mergeRender?: { [other: string]: false | { override?: Partial<GridColDef<R>> } & BoxProps }
@@ -30,8 +29,9 @@ export interface DataTableProps<R extends GridValidRowModel=any> extends Omit<Da
29 footerSide?: (width: number) => ReactNode
30 fillFlex?: boolean
31 persist?: string
32 + details?: boolean
33 }
34 -export function DataTable({ columns, initialState={}, actions, actionsProps, initializing, noRows, error, compact, footerSide, fillFlex, persist, ...rest }: DataTableProps) {
34 +export function DataTable({ columns, initialState={}, actions, actionsProps, initializing, noRows, error, compact, footerSide, fillFlex, persist, details, ...rest }: DataTableProps) {
35 const theme = useTheme()
36 const apiRef = useGridApiRef()
37 const [actionsLength, setActionsLength] = useState(0)
@@ -98,8 +98,8 @@ export function DataTable({ columns, initialState={}, actions, actionsProps, ini
98 const sizeGrid = useGetSize()
99 const width = useDebounce(sizeGrid.w || 0, 500) // stabilize width
100 const hideCols = useMemo(() => {
101 - const fields = onlyTruthy(manipulatedColumns.map(({ field, hideUnder, hidden }) =>
102 - (hidden || hideUnder && width < (typeof hideUnder === 'number' ? hideUnder : theme.breakpoints.values[hideUnder]))
101 + const fields = onlyTruthy(manipulatedColumns.map(({ field, hideUnder }) =>
102 + (hideUnder === true || hideUnder && width < (typeof hideUnder === 'number' ? hideUnder : theme.breakpoints.values[hideUnder]))
103 && field))
104 const o = Object.fromEntries(fields.map(x => [x, false]))
105 _.merge(initialState, { columns: { columnVisibilityModel: o } })
@@ -163,7 +163,7 @@ export function DataTable({ columns, initialState={}, actions, actionsProps, ini
163 },
164 },
165 onCellClick({ field, row }) {
166 - if (field === ACTIONS) return
166 + if (field === ACTIONS || details === false) return
167 if (window.getSelection()?.type === 'Range') return // not a click but a drag
168 const visibleInList = merged + apiRef.current.getVisibleColumns().length
169 const showInDialog = manipulatedColumns.filter(x =>
admin/src/InstalledPlugins.ts
+4 -4
@@ -68,7 +68,7 @@ export default function InstalledPlugins({ updates }: { updates?: true }) {
68 },
69 {
70 field: 'installedVersion',
71 - hidden: true,
71 + hideUnder: true,
72 dialogHidden: true,
73 renderCell: ({ value }) => value && `Yours ${value}`
74 },
@@ -76,8 +76,7 @@ export default function InstalledPlugins({ updates }: { updates?: true }) {
76 field: 'changelog',
77 headerName: "Change log",
78 flex: 2,
79 - hideUnder: 'sm',
80 - hidden: !updates,
79 + hideUnder: !updates || 'sm',
80 sx: { flexDirection: 'column', alignItems: 'flex-start' },
81 renderCell({ value, row }) {
82 if (!Array.isArray(value)) return null
@@ -262,6 +261,7 @@ function makeFields(config: any, values: any) {
261 rest.valuesForAdd = newObj(callable(fields, false), x => x.defaultValue)
262 if (typeof fields === 'string')
263 fields = eval(fields)
264 + rest.details ??= false
265 rest.fields = (values: unknown) => _.map(makeFields(callable(fields, values), values), (v,k) => v && ({ k, ...v, defaultValue: undefined })).filter(Boolean)
266 }
267 if (defaultValue !== undefined && type === 'boolean')
@@ -341,7 +341,7 @@ export const descriptionField: DataTableColumn = {
341 export const themeField: DataTableColumn = {
342 field: 'isTheme',
343 headerName: "is theme",
344 - hidden: true,
344 + hideUnder: true,
345 dialogHidden: true,
346 type: 'boolean',
347 renderCell({ value }) {
admin/src/LogsPage.ts
+5 -9
@@ -10,7 +10,7 @@ import {
10 _dbg, mapFilter, safeDecodeURIComponent, stringAfter, onlyTruthy, formatTimestamp, formatSpeed, copyTextToClipboard
11 } from '@hfs/shared'
12 import {
13 - NetmaskField, Flex, IconBtn, useBreakpoint, usePauseButton, useToggleButton, WildcardsSupported, Country,
13 + NetmaskField, Flex, IconBtn, useBreakpoint, usePauseButton, useToggleButton, Country,
14 hTooltip, Btn, wikiLink
15 } from './mui';
16 import _ from 'lodash'
@@ -235,14 +235,12 @@ export function LogFile({ file, footerSide, hidden, limit, filter, ...rest }: Lo
235 headerName: "Country",
236 field: 'country',
237 flex: 1,
238 - hidden: !showCountry,
239 - hideUnder: 'md',
238 + hideUnder: !showCountry || 'md',
239 valueGetter: ({ value }) => _.find(COUNTRIES, { code: value })?.name || value,
240 renderCell: ({ row }) => h(Country, { code: row.country, long: true, def: '-' }),
241 },
242 {
244 - hidden: isIps,
245 - field: 'msg',
243 + field: 'isIps || msg',
244 headerName: "Message",
245 flex: 4,
246 }
@@ -252,8 +250,7 @@ export function LogFile({ file, footerSide, hidden, limit, filter, ...rest }: Lo
250 headerName: "Country",
251 field: 'country',
252 valueGetter: ({ row }) => row.extra?.country,
255 - hidden: !showCountry,
256 - hideUnder: 'xl',
253 + hideUnder: !showCountry || 'xl',
254 renderCell: ({ value }) => h(Country, { code: value, def: '-' }),
255 },
256 {
@@ -288,10 +285,9 @@ export function LogFile({ file, footerSide, hidden, limit, filter, ...rest }: Lo
285 },
286 {
287 headerName: "Agent",
291 - hideUnder: 'md',
288 field: 'ua',
289 width: 60,
294 - hidden: !showAgent,
290 + hideUnder: !showAgent || 'md',
291 valueGetter: ({ row }) => row.extra?.ua,
292 renderCell: ({ value }) => agentIcons(value),
293 },
admin/src/MonitorPage.ts
+1 -2
@@ -172,9 +172,8 @@ function Connections() {
172 },
173 {
174 field: 'country',
175 - hidden: config.data?.[CFG.geo_enable] !== true,
175 headerName: "Country",
177 - hideUnder: 'md',
176 + hideUnder: config.data?.[CFG.geo_enable] !== true || 'md',
177 renderCell: ({ value, row }) => h(Country, { code: value, ip: row.ip }),
178 },
179 {
admin/src/OptionsPage.ts
+4 -3
@@ -9,7 +9,7 @@ import { CardMembership, EditNote, Refresh, Warning } from '@mui/icons-material'
9 import { adminApis } from '../../src/adminApis'
10 import {
11 MAX_TILE_SIZE, REPO_URL, SORT_BY_OPTIONS, THEME_OPTIONS, CFG, IMAGE_FILEMASK,
12 - Dict, md, wait, with_, try_, ipForUrl,
12 + Dict, md, with_, try_, ipForUrl,
13 } from './misc'
14 import {
15 iconTooltip, InLink, LinkBtn, propsForModifiedValues, wikiLink, useBreakpoint, NetmaskField, WildcardsSupported, Flex
@@ -17,7 +17,7 @@ import {
17 import { Form, BoolField, NumberField, SelectField, FieldProps, Field, StringField } from '@hfs/mui-grid-form';
18 import { ArrayField } from './ArrayField'
19 import FileField from './FileField'
20 -import { alertDialog, confirmDialog, newDialog, toast, waitDialog } from './dialog'
20 +import { alertDialog, confirmDialog, newDialog, toast } from './dialog'
21 import { proxyWarning } from './HomePage'
22 import _ from 'lodash';
23 import { proxy, subscribe, useSnapshot } from 'valtio'
@@ -165,8 +165,9 @@ export default function OptionsPage() {
165 helperText: "In case another website is linking your files" },
166
167 { k: 'block', label: false, comp: ArrayField, xs: 12, prepend: true, sm: true, autoRowHeight: true,
168 + form: { maxWidth: '30em' },
169 fields: [
169 - { k: 'ip', label: "Blocked IP", sm: 12, required: true, wrap: true, $width: 2,
170 + { k: 'ip', label: "Blocked IP", sm: 12, required: true, wrap: true, $width: 2, comp: NetmaskField,
171 $column: { mergeRender: { comment: {}, expire: {} } },
172 helperText: h(Flex, { component: 'span' }, h(WildcardsSupported), "Be careful to not kick yourself out, by blocking also your IP."),
173 },
admin/src/dialog.ts
+1 -1
@@ -140,7 +140,7 @@ export function confirmDialog(msg: ReactNode, { href, trueText="Go", falseText="
140 }
141 }
142
143 -type FormDialog<T> = Omit<FormProps<T>, 'values' | 'save' | 'set'>
143 +export type FormDialog<T> = Omit<FormProps<T>, 'values' | 'save' | 'set'>
144 & Partial<Pick<FormProps<T>, 'save'>>
145 & {
146 onChange?: (values:Partial<T>, extra: { setValues: Dispatch<SetStateAction<Partial<T>>> }) => void,