admin/logs: new "notes" column #334

Massimo Melina committed Sep 7, 2023 at 19:06 UTC c5501b17567aaadbfdbe764523036e6c2bf35256
8 files changed +38 -13
admin/src/LogsPage.ts
+21 -4
@@ -6,7 +6,7 @@ import { API_URL, useApiList } from './api'
6 import { DataTable } from './DataTable'
7 import { formatBytes, tryJson } from '@hfs/shared'
8 import { logLabels } from './OptionsPage'
9 -import { typedKeys } from './misc';
9 +import { typedKeys, xlate } from './misc';
10
11 export default function LogsPage() {
12 const [tab, setTab] = useState(0)
@@ -19,7 +19,15 @@ export default function LogsPage() {
19 }
20
21 function LogFile({ file }: { file: string }) {
22 - const { list, error, connecting } = useApiList('get_log', { file })
22 + const { list, error, connecting } = useApiList('get_log', { file }, {
23 + map(x) {
24 + const { extra } = x
25 + if (!extra) return
26 + const notes = extra.dl ? "fully downloaded" : extra.ul ? "uploaded " + formatBytes(extra.size) : ''
27 + if (notes)
28 + x.notes = notes
29 + }
30 + })
31 if (error)
32 return error
33 return h(DataTable, {
@@ -63,7 +71,7 @@ function LogFile({ file }: { file: string }) {
71 {
72 field: 'user',
73 headerName: "Username",
66 - flex: .4,
74 + flex: .3,
75 maxWidth: 200,
76 hideUnder: 'lg',
77 },
@@ -95,13 +103,22 @@ function LogFile({ file }: { file: string }) {
103 hideUnder: 'md',
104 valueFormatter: ({ value }) => formatBytes(value as number)
105 },
106 + {
107 + field: 'notes',
108 + headerName: "Notes",
109 + width: 100,
110 + hideUnder: 'sm',
111 + cellClassName: 'wrap',
112 + },
113 {
114 field: 'uri',
115 headerName: "URI",
116 flex: 2,
117 minWidth: 100,
118 mergeRender: { other: 'method', fontSize: 'small' },
104 - valueFormatter: ({ value }) => {
119 + renderCell: ({ value, row }) => {
120 + if (row.extra?.ul)
121 + return row.extra?.ul
122 value = decodeURIComponent(value)
123 if (!value.startsWith(API_URL))
124 return value
admin/src/api.ts
+2 -2
@@ -31,7 +31,7 @@ export function useApiEx<T=any>(...args: Parameters<typeof useApi>) {
31 return { data, error, reload, loading, element }
32 }
33
34 -export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { map=((x:any)=>x) }={}) {
34 +export function useApiList<T=any, S=T>(cmd:string|Falsy, params: Dict={}, { map }: { map?: (rec: S) => T }={}) {
35 const [list, setList] = useStateMounted<T[]>([])
36 const [props, setProps] = useStateMounted<any>(undefined)
37 const [error, setError] = useStateMounted<any>(undefined)
@@ -86,7 +86,7 @@ export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { map=((x:a
86 if (op === 'props')
87 return setProps(par)
88 if (op === 'add') {
89 - const mappedPar = map(par)
89 + const mappedPar = map?.(par) ?? par
90 mappedPar.id ??= idGenerator.current = Math.max(idGenerator.current, Date.now()) + .001
91 bufferAdd.push(mappedPar)
92 apply()
admin/src/index.css
+2
@@ -25,6 +25,8 @@ code {
25 background-color: #8882;
26 }
27
28 +.wrap .MuiDataGrid-cellContent { white-space: normal }
29 +
30 ol, ul { margin-top: .2em; padding-left: 1.5em; }
31
32 .dialog-alert .MuiDialogContent-root {
src/adminApis.ts
+4 -3
@@ -19,7 +19,7 @@ import monitorApis from './api.monitor'
19 import langApis from './api.lang'
20 import netApis from './api.net'
21 import { getConnections } from './connections'
22 -import { debounceAsync, isLocalHost, makeNetMatcher, onOff, wait, waitFor } from './misc'
22 +import { debounceAsync, isLocalHost, makeNetMatcher, onOff, tryJson, wait, waitFor } from './misc'
23 import events from './events'
24 import { accountCanLoginAdmin, accountsConfig, getFromAccount } from './perm'
25 import Koa from 'koa'
@@ -158,9 +158,9 @@ export const adminApis: ApiHandlers = {
158 })
159
160 function parse(line: string) {
161 - const m = /^(.+?) (.+?) (.+?) \[(.{11}):(.{14})] "(\w+) ([^"]+) HTTP\/\d.\d" (\d+) (-|\d+)/.exec(line)
161 + const m = /^(.+?) (.+?) (.+?) \[(.{11}):(.{14})] "(\w+) ([^"]+) HTTP\/\d.\d" (\d+) (-|\d+) ?(.*)/.exec(line)
162 if (!m) return
163 - const [, ip, , user, date, time, method, uri, status, length] = m
163 + const [, ip, , user, date, time, method, uri, status, length, extra] = m
164 return { // keep object format same as events emitted by the log module
165 ip,
166 user: user === '-' ? undefined : user,
@@ -169,6 +169,7 @@ export const adminApis: ApiHandlers = {
169 uri,
170 status: Number(status),
171 length: length === '-' ? undefined : Number(length),
172 + extra: tryJson(tryJson(extra)) || undefined,
173 }
174 }
175 },
src/connections.ts
-1
@@ -15,7 +15,6 @@ export class Connection {
15 opTotal?: number
16 opProgress?: number
17 opOffset?: number
18 - uploadPath?: string
18 ctx?: Koa.Context
19 private _cachedIp?: string
20 [rest:symbol]: any // let other modules add extra data, but using symbols to avoid name collision
src/log.ts
+6 -2
@@ -98,13 +98,16 @@ export const logMw: Koa.Middleware = async (ctx, next) => {
98 if (!stream) return
99 }
100 }
101 - const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s\n' // Apache's Common Log Format
101 + const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s %s\n' // Apache's Common Log Format
102 const a = now.toString().split(' ')
103 const date = a[2]+'/'+a[1]+'/'+a[3]+':'+a[4]+' '+a[5]?.slice(3)
104 const user = getCurrentUsername(ctx)
105 const length = ctx.state.length ?? ctx.length
106 const uri = ctx.originalUrl
107 - events.emit(logger.name, Object.assign(_.pick(ctx, ['ip', 'method','status']), { length, user, ts: now, uri }))
107 + const extra = ctx.state.includesLastByte && ctx.vfsNode && { dl: 1 }
108 + || ctx.state.uploadPath && { ul: ctx.state.uploadPath, size: ctx.state.uploadSize }
109 + || undefined
110 + events.emit(logger.name, Object.assign(_.pick(ctx, ['ip', 'method','status']), { length, user, ts: now, uri, extra }))
111 debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
112 stat(logger.path).catch(() => logger.reopen())) // async = smoother but we may lose some entries
113 stream!.write(util.format( format,
@@ -116,6 +119,7 @@ export const logMw: Koa.Middleware = async (ctx, next) => {
119 ctx.req.httpVersion,
120 ctx.status,
121 length?.toString() ?? '-',
122 + extra ? JSON.stringify(JSON.stringify(extra)) : '',
123 ))
124 })
125 }
src/serveFile.ts
+1
@@ -95,6 +95,7 @@ export function getRange(ctx: Koa.Context, totalSize: number) {
95 ctx.set('Accept-Ranges', 'bytes')
96 const { range } = ctx.request.header
97 if (!range) {
98 + ctx.state.includesLastByte = true
99 ctx.response.length = totalSize
100 return
101 }
src/upload.ts
+2 -1
@@ -121,8 +121,9 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
121 let lastGotTime = 0
122 const conn = socket2connection(ctx.socket)
123 if (!conn) return ()=>{}
124 - ctx.state.uploadPath = ctx.path + path
124 const opTotal = reqSize + resume
125 + ctx.state.uploadPath = ctx.path + path
126 + ctx.state.uploadSize = opTotal
127 updateConnection(conn, { ctx, op: 'upload', opTotal, opOffset: resume / opTotal })
128 const h = setInterval(() => {
129 const now = Date.now()