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()