admin/monitoring: uptime
Massimo Melina committed
Feb 2, 2024 at 00:09 UTC
d1cff1317d0d1ba9dde53096b20064dcf973006e
2 files changed
+42
-7
admin/src/MonitorPage.ts
+14
-7
@@ -4,9 +4,9 @@ import _ from "lodash"
4
import { createElement as h, useMemo, Fragment } from "react"
5
import { apiCall, useApiEvents, useApiEx, useApiList } from "./api"
6
import { LinkOff, Lock, Block, FolderZip, Upload, Download } from '@mui/icons-material'
7
-import { Box, Chip, ChipProps } from '@mui/material'
7
+import { Box, Chip, ChipProps, Tooltip } from '@mui/material'
8
import { DataTable } from './DataTable'
9
-import { formatBytes, ipForUrl, manipulateConfig, CFG, formatSpeed, with_ } from "./misc"
9
+import { formatBytes, ipForUrl, manipulateConfig, CFG, formatSpeed, with_, createDurationFormatter } from "./misc"
10
import { IconBtn, IconProgress, iconTooltip, usePauseButton, useBreakpoint, Country } from './mui'
11
import { Field, SelectField } from '@hfs/mui-grid-form'
12
import { StandardCSSProperties } from '@mui/system/styleFunctionSx/StandardCssProperties'
@@ -30,10 +30,15 @@ function MoreInfo() {
30
const xl = useBreakpoint('xl')
31
const md = useBreakpoint('md')
32
const sm = useBreakpoint('sm')
33
+ const formatDuration = createDurationFormatter({ maxTokens: 2, skipZeroes: true })
34
return element || h(Box, { display: 'flex', flexWrap: 'wrap', gap: '1em', mb: 2 },
34
- xl && pair('started'),
35
+ md && pair('started', {
36
+ label: "Uptime",
37
+ render: x => formatDuration(Date.now() - +new Date(x)),
38
+ title: x => x
39
+ }),
40
xl && pair('http', { label: "HTTP", render: port }),
36
- md && pair('https', { label: "HTTPS", render: port }),
41
+ xl && pair('https', { label: "HTTPS", render: port }),
42
sm && pair('connections'),
43
pair('sent', { render: formatBytes, minWidth: '4em' }),
44
sm && pair('got', { render: formatBytes, minWidth: '4em' }),
@@ -47,15 +52,17 @@ function MoreInfo() {
52
label?: string
53
render?: Render
54
minWidth?: StandardCSSProperties['minWidth']
55
+ title?: (v: any) => string
56
}
57
52
- function pair(k: string, { label, minWidth, render }: PairOptions={}) {
58
+ function pair(k: string, { label, minWidth, render, title }: PairOptions={}) {
59
let v = _.get(status, k)
60
if (v === undefined)
61
return null
62
if (typeof v === 'string' && isoDateRe.test(v))
63
v = new Date(v).toLocaleString()
64
let color: Color = undefined
65
+ const renderedTitle = title?.(v)
66
if (render) {
67
v = render(v)
68
if (Array.isArray(v))
@@ -63,7 +70,7 @@ function MoreInfo() {
70
}
71
if (!label)
72
label = _.capitalize(k.replaceAll('_', ' '))
66
- return h(Chip, {
73
+ return h(Tooltip, { title: renderedTitle, children: h(Chip, {
74
variant: 'filled',
75
color,
76
label: h(Fragment, {},
@@ -71,7 +78,7 @@ function MoreInfo() {
78
': ',
79
h('span', { style:{ display: 'inline-block', minWidth } }, v),
80
),
74
- })
81
+ }) })
82
}
83
84
function port(v: any): ReturnType<Render> {
shared/index.ts
+28
@@ -2,11 +2,14 @@
2
3
import _ from 'lodash'
4
import { apiCall } from './api'
5
+import { DAY, HOUR, MINUTE, objSameKeys, typedEntries } from '../src/cross'
6
export * from './react'
7
export * from './dialogs'
8
export * from '../src/srp'
9
export * from '../src/cross'
10
11
+// code in this file is shared among frontends, but not backend
12
+
13
(window as any)._ = _
14
15
const HFS = getHFS()
@@ -99,4 +102,29 @@ export function focusSelector(selector: string, root=document) {
102
export function disableConsoleDebug() {
103
const was = console.debug
104
console.debug = (...args) => (window as any).DEV && was(...args)
105
+}
106
+
107
+type DurationUnit = 'day' | 'hour' | 'minute' | 'second'
108
+export function createDurationFormatter({ locale=undefined, unitDisplay='narrow', largest='day', smallest='second', maxTokens, skipZeroes }:
109
+ { skipZeroes?: boolean, largest?: DurationUnit, smallest?: DurationUnit, locale?: string, unitDisplay?: 'long' | 'short' | 'narrow', maxTokens?: 1 | 2 | 3 }={}) {
110
+ const multipliers: Record<DurationUnit, number> = { day: DAY, hour: HOUR, minute: MINUTE, second: 1000 }
111
+ const fmt = objSameKeys(multipliers, (v,k) => Intl.NumberFormat(locale, { style: 'unit', unit: k, unitDisplay }).format)
112
+ const fmtList = new Intl.ListFormat(locale, { style: 'narrow', type: 'unit' })
113
+ return (ms: number) => {
114
+ const a = []
115
+ let on = false
116
+ for (const [unit, mul] of typedEntries(multipliers)) {
117
+ if (unit === smallest)
118
+ break
119
+ if (unit === largest)
120
+ on = true
121
+ if (!on) continue
122
+ const v = Math.floor(ms / mul)
123
+ if (!v && skipZeroes) continue
124
+ a.push( fmt[unit]?.(v) ?? String(v) )
125
+ if (a.length === maxTokens) break
126
+ ms %= mul
127
+ }
128
+ return fmtList.format(a)
129
+ }
130
}
\ No newline at end of file