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