@samitouri / QOSami-HFS / commits / 4dec055d

fix: admin/plugins: was not showing the partial list in case of errors

Massimo Melina committed Sep 28, 2023 at 00:16 UTC 4dec055d8e732d109aada8143b7ed0822eef683e
7 files changed +110 -116
admin/src/DataTable.ts
+4 -2
@@ -1,5 +1,5 @@
1 import { DataGrid, DataGridProps, GridColDef, GridValidRowModel, useGridApiRef } from '@mui/x-data-grid'
2 -import { Box, BoxProps, Breakpoint, LinearProgress, useTheme } from '@mui/material'
2 +import { Alert, Box, BoxProps, Breakpoint, LinearProgress, useTheme } from '@mui/material'
3 import { useWindowSize } from 'usehooks-ts'
4 import { createElement as h, Fragment, ReactNode, useEffect, useMemo, useRef, useState } from 'react'
5 import { newDialog, onlyTruthy } from '@hfs/shared'
@@ -17,8 +17,9 @@ interface DataTableProps<R extends GridValidRowModel=any> extends Omit<DataGridP
17 actionsProps?: Partial<GridColDef<R>> & { hideUnder?: Breakpoint | number }
18 initializing?: boolean
19 noRows?: ReactNode
20 + error?: ReactNode
21 }
21 -export function DataTable({ columns, initialState={}, actions, actionsProps, initializing, noRows, ...rest }: DataTableProps) {
22 +export function DataTable({ columns, initialState={}, actions, actionsProps, initializing, noRows, error, ...rest }: DataTableProps) {
23 const { width } = useWindowSize()
24 const theme = useTheme()
25 const apiRef = useGridApiRef()
@@ -80,6 +81,7 @@ export function DataTable({ columns, initialState={}, actions, actionsProps, ini
81 return null
82
83 return h(Fragment, {},
84 + error && h(Alert, { severity: 'error' }, error),
85 initializing && h(Box, { position: 'relative' },
86 h(LinearProgress, { // differently from "loading", this is not blocking user interaction
87 sx: { position: 'absolute', width: 'calc(100% - 2px)', borderRadius: 1, m: '1px 1px' }
admin/src/InstalledPlugins.ts
+2 -10
@@ -2,7 +2,7 @@
2
3 import { apiCall, useApiList } from './api'
4 import { createElement as h, Fragment, ReactNode } from 'react'
5 -import { Alert, Box, Link, Tooltip } from '@mui/material'
5 +import { Box, Link, Tooltip } from '@mui/material'
6 import { DataTable } from './DataTable'
7 import { Delete, Error as ErrorIcon, PlayCircle, Settings, StopCircle, Upgrade } from '@mui/icons-material'
8 import { IconBtn, prefix, with_, xlate } from './misc'
@@ -14,10 +14,9 @@ import FileField from './FileField'
14
15 export default function InstalledPlugins({ updates }: { updates?: true }) {
16 const { list, updateEntry, error, initializing } = useApiList(updates ? 'get_plugin_updates' : 'get_plugins')
17 - if (error)
18 - return showError(error)
17 const size = 'small'
18 return h(DataTable, {
19 + error,
20 rows: list.length ? list : [], // workaround for DataGrid bug causing 'no rows' message to be not displayed after 'loading' was also used
21 initializing,
22 disableColumnSelector: true,
@@ -158,13 +157,6 @@ const type2comp = {
157 real_path: FileField,
158 }
159
161 -export function showError(error: any) {
162 - return h(Alert, { severity: 'error' }, xlate(error, {
163 - github_quota: "Request denied. You may have reached the limit, retry later.",
164 - ENOTFOUND: "Couldn't reach github.com",
165 - }))
166 -}
167 -
160 export async function startPlugin(id: string) {
161 await apiCall('start_plugin', { id })
162 toast("Plugin started", h(PlayCircle, { color: 'success' }))
admin/src/LangPage.ts
+2 -1
@@ -14,7 +14,7 @@ export default function LangPage() {
14 const { list, error, connecting, reload } = useApiList('get_langs')
15 const langs = useMemo(() => ['en', ..._.uniq(list.map(x => x.code))], [list])
16 const large = useBreakpoint('md')
17 - return error || h(Fragment, {},
17 + return h(Fragment, {},
18 large && h(Alert, { severity: 'info' }, "Translation is limited to Front-end, it doesn't apply to Admin-panel"),
19 h(Box, { mt: 1, maxWidth: '40em', flex: 1, display: 'flex', flexDirection: 'column' },
20 h(Box, { mb: 1, display: 'flex' },
@@ -23,6 +23,7 @@ export default function LangPage() {
23 h(ForceLang, { langs }),
24 ),
25 h(DataTable, {
26 + error,
27 loading: connecting,
28 rows: list as any,
29 hideFooter: true,
admin/src/LogsPage.ts
+1 -2
@@ -28,9 +28,8 @@ function LogFile({ file }: { file: string }) {
28 x.notes = notes
29 }
30 })
31 - if (error)
32 - return error
31 return h(DataTable, {
32 + error,
33 loading: connecting,
34 rows: list as any,
35 componentsProps: {
admin/src/MonitorPage.ts
+98 -98
@@ -106,107 +106,107 @@ function Connections() {
106 }
107 }),
108 ),
109 - error ? h(Alert, { severity: 'error' }, error)
110 - : h(DataTable, {
111 - rows,
112 - noRows: filtered && "No downloads at the moment",
113 - columns: [
114 - {
115 - field: 'ip',
116 - headerName: "Address",
117 - flex: 1,
118 - maxWidth: 400,
119 - renderCell: ({ row, value }) => ipForUrl(value) + ' :' + row.port,
120 - mergeRender: { other: 'user', fontSize: 'small' },
121 - },
122 - {
123 - field: 'user',
124 - headerName: "User",
125 - hideUnder: 'md',
126 - },
127 - {
128 - field: 'started',
129 - headerName: "Started",
130 - type: 'dateTime',
131 - width: 100,
132 - hideUnder: 'lg',
133 - valueFormatter: ({ value }) => new Date(value as string).toLocaleTimeString()
134 - },
135 - {
136 - field: 'path',
137 - headerName: "File",
138 - flex: 1.5,
139 - renderCell({ value, row }) {
140 - if (!value) return
141 - if (row.archive)
142 - return h(Fragment, {},
143 - h(FolderZip, { sx: { mr: 1 } }),
144 - row.archive,
145 - h(Box, { ml: 2, color: 'text.secondary' }, value)
146 - )
147 - const i = value?.lastIndexOf('/')
109 + h(DataTable, {
110 + error,
111 + rows,
112 + noRows: filtered && "No downloads at the moment",
113 + columns: [
114 + {
115 + field: 'ip',
116 + headerName: "Address",
117 + flex: 1,
118 + maxWidth: 400,
119 + renderCell: ({ row, value }) => ipForUrl(value) + ' :' + row.port,
120 + mergeRender: { other: 'user', fontSize: 'small' },
121 + },
122 + {
123 + field: 'user',
124 + headerName: "User",
125 + hideUnder: 'md',
126 + },
127 + {
128 + field: 'started',
129 + headerName: "Started",
130 + type: 'dateTime',
131 + width: 100,
132 + hideUnder: 'lg',
133 + valueFormatter: ({ value }) => new Date(value as string).toLocaleTimeString()
134 + },
135 + {
136 + field: 'path',
137 + headerName: "File",
138 + flex: 1.5,
139 + renderCell({ value, row }) {
140 + if (!value) return
141 + if (row.archive)
142 return h(Fragment, {},
149 - row.op && h(IconProgress, {
150 - icon: row.op === 'upload' ? Upload : Download,
151 - progress: row.opProgress ?? row.opOffset,
152 - offset: row.opOffset,
153 - addTitle: row.opTotal && h('div', {}, "Total: " + formatBytes(row.opTotal)),
154 - sx: { mr: 1 }
155 - }),
156 - h(Box, {}, value.slice(i + 1),
157 - i > 0 && h(Box, { ml: 2, fontSize: 'x-small', color: 'text.secondary' }, value.slice(0, i))
158 - ),
143 + h(FolderZip, { sx: { mr: 1 } }),
144 + row.archive,
145 + h(Box, { ml: 2, color: 'text.secondary' }, value)
146 )
160 - }
161 - },
162 - {
163 - field: 'outSpeed',
164 - headerName: "Speed",
165 - width: 110,
166 - hideUnder: 'sm',
167 - type: 'number',
168 - renderCell: ({ value, row }) => formatSpeed(Math.max(value||0, row.inSpeed||0) || undefined),
169 - mergeRender: { other: 'sent', fontSize: 'small', textAlign: 'right' }
170 - },
171 - {
172 - field: 'sent',
173 - headerName: "Sent",
174 - type: 'number',
175 - hideUnder: 'md',
176 - renderCell: ({ value, row}) => formatBytes(Math.max(value||0, row.got||0))
177 - },
178 - {
179 - field: 'v',
180 - headerName: "Protocol",
181 - align: 'center',
182 - hideUnder: Infinity,
183 - renderCell: ({ value, row }) => h(Fragment, {},
184 - "IPv" + value,
185 - iconTooltip(Lock, "HTTPS", { opacity: .5 })
147 + const i = value?.lastIndexOf('/')
148 + return h(Fragment, {},
149 + row.op && h(IconProgress, {
150 + icon: row.op === 'upload' ? Upload : Download,
151 + progress: row.opProgress ?? row.opOffset,
152 + offset: row.opOffset,
153 + addTitle: row.opTotal && h('div', {}, "Total: " + formatBytes(row.opTotal)),
154 + sx: { mr: 1 }
155 + }),
156 + h(Box, {}, value.slice(i + 1),
157 + i > 0 && h(Box, { ml: 2, fontSize: 'x-small', color: 'text.secondary' }, value.slice(0, i))
158 + ),
159 )
187 - },
188 - {
189 - field: 'agent',
190 - headerName: "Agent",
191 - hideUnder: 'lg',
192 - },
193 - ],
194 - actionsProps: { hideUnder: 'sm' },
195 - actions: ({ row }) => [
196 - h(IconBtn, {
197 - icon: LinkOff,
198 - title: "Disconnect",
199 - onClick: () => apiCall('disconnect', _.pick(row, ['ip', 'port']))
200 - .then(() => toast("Disconnection requested")),
201 - }),
202 - h(IconBtn, {
203 - icon: Block,
204 - title: "Block IP",
205 - disabled: row.ip === props?.you,
206 - onClick: () => blockIp(row.ip),
207 - }),
208 - ]
209 - })
160 + }
161 + },
162 + {
163 + field: 'outSpeed',
164 + headerName: "Speed",
165 + width: 110,
166 + hideUnder: 'sm',
167 + type: 'number',
168 + renderCell: ({ value, row }) => formatSpeed(Math.max(value||0, row.inSpeed||0) || undefined),
169 + mergeRender: { other: 'sent', fontSize: 'small', textAlign: 'right' }
170 + },
171 + {
172 + field: 'sent',
173 + headerName: "Sent",
174 + type: 'number',
175 + hideUnder: 'md',
176 + renderCell: ({ value, row}) => formatBytes(Math.max(value||0, row.got||0))
177 + },
178 + {
179 + field: 'v',
180 + headerName: "Protocol",
181 + align: 'center',
182 + hideUnder: Infinity,
183 + renderCell: ({ value, row }) => h(Fragment, {},
184 + "IPv" + value,
185 + iconTooltip(Lock, "HTTPS", { opacity: .5 })
186 + )
187 + },
188 + {
189 + field: 'agent',
190 + headerName: "Agent",
191 + hideUnder: 'lg',
192 + },
193 + ],
194 + actionsProps: { hideUnder: 'sm' },
195 + actions: ({ row }) => [
196 + h(IconBtn, {
197 + icon: LinkOff,
198 + title: "Disconnect",
199 + onClick: () => apiCall('disconnect', _.pick(row, ['ip', 'port']))
200 + .then(() => toast("Disconnection requested")),
201 + }),
202 + h(IconBtn, {
203 + icon: Block,
204 + title: "Block IP",
205 + disabled: row.ip === props?.you,
206 + onClick: () => blockIp(row.ip),
207 + }),
208 + ]
209 + })
210 )
211 }
212
admin/src/OnlinePlugins.ts
+2 -3
@@ -7,7 +7,7 @@ import { IconBtn } from './misc'
7 import { Download, Search } from '@mui/icons-material'
8 import { StringField } from '@hfs/mui-grid-form'
9 import { useDebounce } from 'usehooks-ts'
10 -import { renderName, showError, startPlugin } from './InstalledPlugins'
10 +import { renderName, startPlugin } from './InstalledPlugins'
11 import { state, useSnapState } from './state'
12 import { alertDialog, confirmDialog } from './dialog'
13
@@ -16,8 +16,6 @@ export default function OnlinePlugins() {
16 const debouncedSearch = useDebounce(search, 1000)
17 const { list, error, initializing } = useApiList('get_online_plugins', { text: debouncedSearch })
18 const snap = useSnapState()
19 - if (error)
20 - return showError(error)
19 return h(Fragment, {},
20 h(StringField, {
21 value: search,
@@ -27,6 +25,7 @@ export default function OnlinePlugins() {
25 label: "Search text"
26 }),
27 h(DataTable, {
28 + error,
29 rows: list.length ? list : [], // workaround for DataGrid bug causing 'no rows' message to be not displayed after 'loading' was also used
30 noRows: "No compatible plugins have been found",
31 initializing,
admin/src/misc.ts
+1
@@ -14,6 +14,7 @@ export async function manipulateConfig(k: string, work:(data:any) => any) {
14
15 export function err2msg(code: string) {
16 return {
17 + github_quota: "Request denied. You may have reached the limit, retry later.",
18 ENOENT: "Not found",
19 ENOTDIR: "Not a folder",
20 }[code] || code