admin/monitor: option to show downloading only
Massimo Melina committed
Mar 12, 2022 at 20:04 UTC
5eccd9ceeb527a24deb960736e90753409286ac1
4 files changed
+94
-79
admin/src/Form.ts
+2
-2
@@ -185,8 +185,8 @@ interface SelectPair<T> { label: string, value:T }
185
export function SelectField<T>(props: FieldProps<T> & { options:SelectOptions<T> }) {
186
const { value, onChange, options, ...rest } = props
187
return h(TextField, { // using TextField because Select is not displaying label correctly
188
- ...rest,
188
...commonSelectProps(props),
189
+ ...rest,
190
onChange(event) {
191
try {
192
let newVal: any = event.target.value
@@ -201,8 +201,8 @@ export function SelectField<T>(props: FieldProps<T> & { options:SelectOptions<T>
201
export function MultiSelectField<T>(props: FieldProps<T[]> & { options:SelectOptions<T> }) {
202
const { value, options, ...rest } = props
203
return h(TextField, {
204
- ...rest,
204
...commonSelectProps({ ...props, value:undefined }),
205
+ ...rest,
206
SelectProps: { multiple: true },
207
value: !Array.isArray(value) ? [] : value.map(x => JSON.stringify(x)),
208
onChange(event) {
admin/src/MonitorPage.ts
+85
-73
@@ -1,18 +1,18 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import _ from "lodash"
4
-import { isValidElement, createElement as h, useMemo, Fragment } from "react"
4
+import { isValidElement, createElement as h, useMemo, Fragment, useState } from "react"
5
import { apiCall, useApiComp, useApiList } from "./api"
6
import { Delete, Lock, Block } from '@mui/icons-material'
7
import { Box, Chip, Typography } from '@mui/material'
8
import { DataGrid } from "@mui/x-data-grid"
9
import { Alert } from '@mui/material'
10
import { prefix, formatBytes, IconBtn, iconTooltip, manipulateConfig } from "./misc"
11
+import { Field, SelectField } from './Form'
12
13
export default function MonitorPage() {
14
return h(Fragment, {},
15
h(MoreInfo),
15
- h(SectionTitle, {}, "Active connections"),
16
h(Connections),
17
)
18
}
@@ -27,7 +27,7 @@ function MoreInfo() {
27
const [res] = useApiComp('get_status')
28
return h(Fragment, {},
29
isValidElement(res) ? res :
30
- h(Box, { display: 'flex', flexWrap: 'wrap', gap: '1em', mb: 1 },
30
+ h(Box, { display: 'flex', flexWrap: 'wrap', gap: '1em', mb: 2 },
31
pair('started'),
32
pair('version'),
33
pair('build'),
@@ -55,76 +55,88 @@ function MoreInfo() {
55
56
function Connections() {
57
const { list, error } = useApiList('get_connections')
58
- const rows = useMemo(()=> list?.map((x:any,id:number) => ({ id, ...x })), [list])
59
- if (error)
60
- return h(Alert, { severity: 'error' }, error)
61
- return h(DataGrid, {
62
- pageSize: 25,
63
- rows,
64
- columns: [
65
- {
66
- field: 'ip',
67
- headerName: 'Address',
68
- flex: 1,
69
- valueGetter: ({ row, value }) => (row.v === 6 ? `[${value}]` : value) + ' :' + row.port
70
- },
71
- {
72
- field: 'v',
73
- headerName: 'Protocol',
74
- align: 'center',
75
- hide: true,
76
- renderCell: ({ value, row }) => h(Fragment, {},
77
- 'IPv' + value,
78
- row.secure && iconTooltip(Lock, "HTTPS", { opacity: .5 })
79
- )
80
- },
81
- {
82
- field: 'outSpeed',
83
- headerName: 'Speed',
84
- valueGetter: ({ value }) => value ? formatBytes(value * 1000, 'B/s', 1000) : ''
85
- },
86
- {
87
- field: 'sent',
88
- headerName: 'Total',
89
- valueGetter: ({ value }) => formatBytes(value)
90
- },
91
- {
92
- field: 'started',
93
- headerName: 'Started',
94
- valueGetter: ({ value }) => new Date(value).toLocaleTimeString()
95
- },
96
- {
97
- field: 'path',
98
- headerName: 'Path',
99
- flex: 1,
100
- renderCell: ({ value }) => {
101
- if (!value) return
102
- const i = value?.lastIndexOf('/')
103
- return h(Fragment, {}, value.slice(i + 1),
104
- i > 0 && h(Box, { ml: 2, color: 'text.secondary' }, value.slice(0, i)))
105
- }
106
- },
107
- {
108
- field: 'Actions ',
109
- width: 80,
110
- align: 'center',
111
- renderCell({ row }) {
112
- return h('div', {},
113
- h(IconBtn, {
114
- icon: Delete,
115
- title: 'Disconnect',
116
- onClick: () => apiCall('disconnect', _.pick(row, ['ip', 'port'])),
117
- }),
118
- h(IconBtn, {
119
- icon: Block,
120
- title: 'Block IP',
121
- onClick: () => blockIp(row.ip),
122
- }),
123
- )
124
- }
125
- }
126
- ]
127
- })
58
+ const [filtered, setFiltered] = useState(true)
59
+ const rows = useMemo(()=> list?.filter((x:any) => !filtered || x.path).map((x:any,id:number) => ({ id, ...x })), [list, filtered])
60
+ return h(Fragment, {},
61
+ h(Box, { display: 'flex', justifyContent: 'space-between' },
62
+ h(SectionTitle, {}, "Active connections"),
63
+ h(SelectField as Field<boolean>, {
64
+ fullWidth: false,
65
+ value: filtered,
66
+ onChange: setFiltered,
67
+ options: { "only downloads": true, "all connections": false }
68
+ }),
69
+ ),
70
+ error ? h(Alert, { severity: 'error' }, error) :
71
+ h(DataGrid, {
72
+ pageSize: 25,
73
+ rows,
74
+ columns: [
75
+ {
76
+ field: 'ip',
77
+ headerName: "Address",
78
+ flex: 1,
79
+ maxWidth: 400,
80
+ valueGetter: ({ row, value }) => (row.v === 6 ? `[${value}]` : value) + ' :' + row.port
81
+ },
82
+ {
83
+ field: 'started',
84
+ headerName: "Started",
85
+ valueGetter: ({ value }) => new Date(value).toLocaleTimeString()
86
+ },
87
+ {
88
+ field: 'path',
89
+ headerName: "File",
90
+ flex: 1,
91
+ renderCell: ({ value }) => {
92
+ if (!value) return
93
+ const i = value?.lastIndexOf('/')
94
+ return h(Fragment, {}, value.slice(i + 1),
95
+ i > 0 && h(Box, { ml: 2, color: 'text.secondary' }, value.slice(0, i)))
96
+ }
97
+ },
98
+ {
99
+ field: 'v',
100
+ headerName: "Protocol",
101
+ align: 'center',
102
+ hide: true,
103
+ renderCell: ({ value, row }) => h(Fragment, {},
104
+ "IPv" + value,
105
+ row.secure && iconTooltip(Lock, "HTTPS", { opacity: .5 })
106
+ )
107
+ },
108
+ {
109
+ field: 'outSpeed',
110
+ headerName: "Speed",
111
+ valueGetter: ({ value }) => value ? formatBytes(value * 1000, "B/s", 1000) : ''
112
+ },
113
+ {
114
+ field: 'sent',
115
+ headerName: "Total",
116
+ valueGetter: ({ value }) => formatBytes(value)
117
+ },
118
+ {
119
+ field: "Actions ",
120
+ width: 80,
121
+ align: 'center',
122
+ renderCell({ row }) {
123
+ return h('div', {},
124
+ h(IconBtn, {
125
+ icon: Delete,
126
+ title: "Disconnect",
127
+ onClick: () => apiCall('disconnect', _.pick(row, ['ip', 'port'])),
128
+ }),
129
+ h(IconBtn, {
130
+ icon: Block,
131
+ title: "Block IP",
132
+ onClick: () => blockIp(row.ip),
133
+ }),
134
+ )
135
+ }
136
+ }
137
+ ]
138
+ })
139
+ )
140
}
141
142
function blockIp(ip: string) {
dev-guidelines.md
+3
@@ -12,3 +12,6 @@
12
13
# Syntax
14
- For strings, I'm trying to use double-quotes or backticks for text that's read by the user, and single-quotes elsewhere.
15
+
16
+# Known problems
17
+- react-scripts server doesn't seem to play nicely with SSE, like sockets are left open
server/src/config.ts
+4
-4
@@ -17,8 +17,8 @@ const configProps:Record<string, ConfigProps<any>> = {}
17
18
let started = false // this will tell the difference for subscribeConfig()s that are called before or after config is loaded
19
let state: Record<string, any> = {}
20
-const emitter = new EventEmitter()
21
-emitter.setMaxListeners(10_000)
20
+const cfgEvents = new EventEmitter()
21
+cfgEvents.setMaxListeners(10_000)
22
const path = argv.config || process.env.HFS_CONFIG || PATH
23
const { save } = watchLoad(path, values => setConfig(values||{}, false), {
24
failedOnFirstAttempt(){
@@ -56,7 +56,7 @@ export function subscribeConfig<T>({ k, ...definition }:{ k:string } & ConfigPro
56
if (v !== undefined)
57
cb(v)
58
}
59
- return onOffMap(emitter, { [eventName]: cb })
59
+ return onOffMap(cfgEvents, { [eventName]: cb })
60
}
61
62
export function getConfig(k:string) {
@@ -106,7 +106,7 @@ export function setConfig(newCfg: Record<string,any>, save?: boolean) {
106
delete state[k]
107
else
108
state[k] = v
109
- emitter.emit('new.'+k, v, oldV)
109
+ cfgEvents.emit('new.'+k, v, oldV)
110
if (save === undefined)
111
saveConfigAsap().then()
112
}