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 }