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,