From 8b09b5092ec682df7e31f83ceebdb6f5add45028 Mon Sep 17 00:00:00 2001 From: Henry Dollman Date: Tue, 16 Jul 2024 23:31:38 -0400 Subject: [PATCH] updates --- main.go | 16 ++- site/bun.lockb | Bin 141686 -> 141750 bytes site/package.json | 2 + .../components/charts/chart-time-select.tsx | 21 ++-- .../components/charts/container-cpu-chart.tsx | 21 ++-- .../components/charts/container-mem-chart.tsx | 24 +++-- site/src/components/charts/cpu-chart.tsx | 26 +++-- site/src/components/charts/disk-chart.tsx | 23 ++--- site/src/components/charts/disk-io-chart.tsx | 97 ++++++++++++++++++ site/src/components/charts/mem-chart.tsx | 18 ++-- site/src/components/routes/server.tsx | 84 +++++++++------ site/src/lib/stores.ts | 6 +- site/src/lib/utils.ts | 87 +++++++++------- site/src/types.d.ts | 2 + 14 files changed, 296 insertions(+), 131 deletions(-) create mode 100644 site/src/components/charts/disk-io-chart.tsx diff --git a/main.go b/main.go index 145f4f3..d80c696 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "crypto/ed25519" "encoding/json" "encoding/pem" + "errors" "fmt" "log" _ "monitor-site/migrations" @@ -151,6 +152,12 @@ func main() { if newStatus == "down" || newStatus == "paused" { deleteServerConnection(newRecord) } + + // if server is set to pending, try to connect + if newStatus == "pending" { + go updateServer(newRecord) + } + // alerts handleStatusAlerts(newStatus, oldRecord) return nil @@ -218,6 +225,13 @@ func updateServer(record *models.Record) { // get server stats from agent systemData, err := requestJson(&server) if err != nil { + if err.Error() == "retry" { + // if previous connection was closed, try again + app.Logger().Error("Existing SSH connection closed. Retrying...", "host", server.Host, "port", server.Port) + deleteServerConnection(record) + updateServer(record) + return + } app.Logger().Error("Failed to get server stats: ", "err", err.Error()) updateServerStatus(record, "down") return @@ -307,7 +321,7 @@ func getServerConnection(server *Server) (*ssh.Client, error) { func requestJson(server *Server) (SystemData, error) { session, err := server.Client.NewSession() if err != nil { - return SystemData{}, err + return SystemData{}, errors.New("retry") } defer session.Close() diff --git a/site/bun.lockb b/site/bun.lockb index 1f21930ef45038ef0e7b2758e768eba75444c9d2..cd799766af1cd51b4533d050daa5672b9c83b1d6 100755 GIT binary patch delta 6853 zcmeI0dyG}p6^GBcIxm!mf{dmCQW07zQZUt25y`~H2lAB13@9Kfh~c56#wX3?5frTt zVA?q#Dhx6{(mz_XFd$Y9Xj7q(DQ{aVHMSzOkWh`S){uVRon;x535mhjf9}ft?r*QP z_St9ObIv|@@BPV+@^w4Q7Y)tz?6qcir)l}~k|YsLO`!*LhW?O)0idWm7Btm( zU7SCuGm5EJ%tDuqZT$E4JoiFn}QHO0Gv<$r{;y(=S zk9tC1S>~@sZGUhYVaeN)$x&^5IQmHsh)5$dK!8MdL}wA z^2e3?uN@D3bABbKT9%Ilr+@BM4UJ<+DfpGd8;IqJ34QvW#LB4KVz=vLH@Y(stO_eOkQ#Q%tT z;y;JKAGJg88?W8=R_t14~O4U4IPQNnyc+{XJz3Z`s0YUruOv{ z^7iS|sHb+ouOR6~Y!})QwO(hGPjYtn-wMAA>cHt9evce?$iVpod<$QKYN`!g7CI0$ zRmGPlyfWg~MEqKlm}(yfM}8PufnO8;^{A%$1E%E|p&l@ufVr7bLG_(;7wQn7i<+9x zmmOG$o`cq-zH^=r{UypLSrht_4yJnCdertdqn**Wz5Z<}e1Q56`8Vogk>lsvu~Se& zNr{&u)DxZ_eoM7}+o<0j)$b7XRDJEdz;a>wX`%<%qi_G0NPB1L$h6~t3yfd`s9+sO-X#XG%OmCr!B@Q#q=CdvNA1v| zY=ljGPSGkZ?`^^fe~yB&ArW&*eQLypM$CD5Sypco<&hc@DQDzIOv7~%bA9qQXd@%$ zN^)Ly)FvJ-B$_u9?C2TrNW`k6x)c0IBUTeJcSIh`sz%X7Ek#!)H$>_z%5L#l<%_)t zr|63!HX-V{)L)z}rirvJYZ#So9)B*8?%?XcrOgcRHkNuTp&Tlp4OCI@3UC3?6TSoA zh3~=n&>Or$z$uI?`!Za8QuSHPWxXthvHlqN$ zo`U~ysx7bsHo#uk2R@X0vVJvbw{LHz_*?iLxB_$+V+iGE;W>Cdn_H81%+IAb5AK0` z;XW7xGhrg!2y9&>!#Ug`;f|V1Zg&$_z)Glx?XVMe!EV?C?m)Z?ZiKu6jj#b;h6A+u z7k6dc4`-5OVjctN@Vyoa8cnBPXKLrP#1MM+* z0BYbC@E`53=|DC^DKrpR4Nt+-@N-xW?yYq~C9`qvZag|J{nKd`87+SozSmOzdn7lU(HE7WiL)m_hsAfOS_zP z=WR14C-+|5VdIhoD@W(Jm|Ek)+tOo$D>`_$u%_(sb!n9wNlDhUE`7aCvc5UdwO8X4 Jjp;kH{tI**YPbLZ delta 6788 zcmeI0du*508OPtZP|DQ{DvjF`hd8B7iF1htoFf=E1u1fEfl)x2Y>YZ2Gj6)MUmI?2 z>MSer4i&ixW;4bZwG}oKz^OQlL-0ZzkZcNNgKT0pbdFsZ={cumSNEekyEly~J23F^v1iULoSP(xXle=rpdSo@GPoSHQte847<7Rv zK|2vO?}#KRBqq8VOmq!srD}&pgIW!mYI%$WtRpqx#MK!P<)EmREnrI$Wz%8Ja zY8T44g6G{9`dw6OPrKo7@dA*hQYPPIczqO6+mZm=UwVEj_hRQ<~= zVE%y;EE<6;D_S6#}j(XL^ zKaKKf_i`omC>TB>8c(e9{rh5JX3s?u5x8G4f zo~Vr^4jzZV29HGfFQ^@QAI^YYv})X`sQ$jFPvcYJ|1`>9(w`se&r50FFY-bh73k-q z;?mF|s0|K_`Y)q4FbdU5)r8gfy;I{+LncI=>Up)HUqP*!=78iuoj;*AIE!*G^qXj3 zv?1zGwiZ50!b&=nMTh=JEu5@GA1P0{1L6Z*i5IhyAD`jsxU%-tsx|!hwy%pg)$)4u zRP;&IcVJ`Gm#Xi;Gx%+1v-Q6J`N4;%F~y=av^`fXIajq;c5_vb$}ZGQAyl|zg^6t$(Q>u~s0yYa`+cl0;+xw$yjycm(FWyP_MgW{NTGwLT^ zh=4}a4lG8`MpvPZNgF~RMfpn}3w_)Irg~lrYQ8OKKlBZ>5BfIh5OUOpd@sCgdzPZO zk}lqcP#f$SQfI~w@ zN4V2?bLn#h&tHBaW#JnWK4;hE*&0)Hi>h%^F~qw)ke7U_B+EjIecz(+?5TVNQ$K#y4IK$RcCSRoXL>7@HzATQTV2Z z&jtKF*-}!Z%d+(o(`W0P>$_5!3$6#cLOFDU3g`|_&xgYRI1fGp=fgm#fM#9Hp7};C=W2+TbAk9{vD_;4u6N-hm^q30xc90e317Sqo`dJXb=)(YehtSD!Ft#LZXbUSZinxKbEpQm1-=d6fr(HLbub;O!O!Gc zaFfhcv>OB~;lY#?t2lTGUV)vk3tojb*aL2EY=x(w6`qFuB;5zQz#YzQumiTk%kUzM zhKFG#JP3;DCCuQ!HT~7Fm*ksa6~5K529|)IHjb$g%@91wmCO|Tps4E*@}-@}j2WpF&;UVT%^fW4I6I5l0; zdTeUCyH}6oYenbc*1faSt!X#z7?u?_q-SNDzMk%GeSS`QY;=zuEyb{=YbkF>M0T3!$OaK4? diff --git a/site/package.json b/site/package.json index 0874a10..d81f089 100644 --- a/site/package.json +++ b/site/package.json @@ -26,6 +26,8 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "d3-scale": "^4.0.2", + "d3-time": "^3.1.0", "lucide-react": "^0.407.0", "nanostores": "^0.10.3", "pocketbase": "^0.21.3", diff --git a/site/src/components/charts/chart-time-select.tsx b/site/src/components/charts/chart-time-select.tsx index 0cfe125..10686b9 100644 --- a/site/src/components/charts/chart-time-select.tsx +++ b/site/src/components/charts/chart-time-select.tsx @@ -6,7 +6,8 @@ import { SelectValue, } from '@/components/ui/select' import { $chartTime } from '@/lib/stores' -import { cn } from '@/lib/utils' +import { chartTimeData, cn } from '@/lib/utils' +import { ChartTimes } from '@/types' import { useStore } from '@nanostores/react' import { useEffect } from 'react' @@ -19,16 +20,20 @@ export default function ChartTimeSelect({ className }: { className?: string }) { }, []) return ( - $chartTime.set(value)} + > - + - 1 hour - 12 hours - 24 hours - 1 week - 30 days + {Object.entries(chartTimeData).map(([value, { label }]) => ( + + {label} + + ))} ) diff --git a/site/src/components/charts/container-cpu-chart.tsx b/site/src/components/charts/container-cpu-chart.tsx index cc082a6..7c1006a 100644 --- a/site/src/components/charts/container-cpu-chart.tsx +++ b/site/src/components/charts/container-cpu-chart.tsx @@ -8,14 +8,20 @@ import { ChartTooltipContent, } from '@/components/ui/chart' import { useMemo } from 'react' -import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils' +import { formatShortDate, hourWithMinutes } from '@/lib/utils' import Spinner from '../spinner' export default function ContainerCpuChart({ chartData, + ticks, }: { chartData: Record[] + ticks: number[] }) { + if (!chartData.length || !ticks.length) { + return + } + const chartConfig = useMemo(() => { let config = {} as Record< string, @@ -45,18 +51,12 @@ export default function ContainerCpuChart({ const hue = ((i * 360) / length) % 360 config[key] = { label: key, - color: `hsl(${hue}, 60%, 60%)`, + color: `hsl(${hue}, 60%, 55%)`, } } return config satisfies ChartConfig }, [chartData]) - const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData]) - - if (!chartData.length) { - return - } - return ( ( [] }) { - if (!chartData.length) { +export default function ({ + chartData, + ticks, +}: { + chartData: Record[] + ticks: number[] +}) { + if (!chartData.length || !ticks.length) { return } @@ -45,14 +51,12 @@ export default function ({ chartData }: { chartData: Record calculateXaxisTicks(chartData), [chartData]) - return ( { x = x / 1024 return x % 1 === 0 ? x : x.toFixed(1) @@ -88,7 +92,7 @@ export default function ({ chartData }: { chartData: Record ( } - - const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData]) + const chartTime = useStore($chartTime) return ( @@ -42,10 +48,10 @@ export default function CpuChart({ chartData }: { chartData: { time: number; cpu ticks={ticks} type="number" scale={'time'} - axisLine={false} - tickMargin={8} minTickGap={35} - tickFormatter={formatShortTime} + tickMargin={8} + axisLine={false} + tickFormatter={chartTimeData[chartTime].format} /> } @@ -33,8 +32,6 @@ export default function DiskChart({ return Math.round(chartData[0]?.disk) }, [chartData]) - const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData]) - // const ticks = useMemo(() => { // let ticks = [0] // for (let i = 1; i < diskSize; i += diskSize / 5) { @@ -65,7 +62,7 @@ export default function DiskChart({ minTickGap={8} tickLine={false} axisLine={false} - unit={' GiB'} + unit={' GB'} /> {/* todo: short time if first date is same day, otherwise short date */} formatShortDate(data[0].payload.time)} indicator="line" /> @@ -92,7 +89,7 @@ export default function DiskChart({ /> + } + + return ( + + + + + {/* todo: short time if first date is same day, otherwise short date */} + + formatShortDate(data[0].payload.time)} + indicator="line" + /> + } + /> + + + + + ) +} diff --git a/site/src/components/charts/mem-chart.tsx b/site/src/components/charts/mem-chart.tsx index ca51abd..7cf2d3b 100644 --- a/site/src/components/charts/mem-chart.tsx +++ b/site/src/components/charts/mem-chart.tsx @@ -6,21 +6,21 @@ import { ChartTooltip, ChartTooltipContent, } from '@/components/ui/chart' -import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils' +import { formatShortDate, hourWithMinutes } from '@/lib/utils' import { useMemo } from 'react' import Spinner from '../spinner' export default function MemChart({ chartData, + ticks, }: { chartData: { time: number; mem: number; memUsed: number; memCache: number }[] + ticks: number[] }) { - if (!chartData.length) { + if (!chartData.length || !ticks.length) { return } - const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData]) - const totalMem = useMemo(() => { return Math.ceil(chartData[0]?.mem) }, [chartData]) @@ -56,7 +56,7 @@ export default function MemChart({ tickLine={false} allowDecimals={false} axisLine={false} - tickFormatter={(v) => `${v} GiB`} + tickFormatter={(v) => `${v} GB`} /> {/* todo: short time if first date is same day, otherwise short date */} a.name.localeCompare(b.name)} labelFormatter={(_, data) => formatShortDate(data[0].payload.time)} @@ -87,7 +87,7 @@ export default function MemChart({ /> import('../charts/cpu-chart')) const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart')) @@ -23,6 +25,8 @@ function timestampToBrowserTime(timestamp: string) { export default function ServerDetail({ name }: { name: string }) { const servers = useStore($systems) const updatedSystem = useStore($updatedSystem) + const chartTime = useStore($chartTime) + const [ticks, setTicks] = useState([] as number[]) const [server, setServer] = useState({} as SystemRecord) const [containers, setContainers] = useState([] as ContainerStatsRecord[]) @@ -34,6 +38,9 @@ export default function ServerDetail({ name }: { name: string }) { const [diskChartData, setDiskChartData] = useState( [] as { time: number; disk: number; diskUsed: number }[] ) + const [diskIoChartData, setDiskIoChartData] = useState( + [] as { time: number; read: number; write: number }[] + ) const [dockerCpuChartData, setDockerCpuChartData] = useState( [] as Record[] ) @@ -44,7 +51,6 @@ export default function ServerDetail({ name }: { name: string }) { useEffect(() => { document.title = `${name} / Beszel` return () => { - console.log('unmounting') setServer({} as SystemRecord) setCpuChartData([]) setMemChartData([]) @@ -55,14 +61,14 @@ export default function ServerDetail({ name }: { name: string }) { }, [name]) useEffect(() => { - if (server?.id && server.name === name) { + if (server.id && server.name === name) { return } const matchingServer = servers.find((s) => s.name === name) as SystemRecord if (matchingServer) { setServer(matchingServer) } - }, [name, server]) + }, [name, server, servers]) // if visiting directly, make sure server gets set when servers are loaded // useEffect(() => { @@ -77,17 +83,14 @@ export default function ServerDetail({ name }: { name: string }) { // get stats useEffect(() => { - if (!('id' in server)) { - console.log('no id in server') + if (!server.id) { return - } else { - console.log('id in server') } pb.collection('system_stats') .getFullList({ filter: `system="${server.id}" && created > "${getPbTimestamp('1h')}"`, fields: 'created,stats', - sort: '-created', + sort: 'created', }) .then((records) => { // console.log('sctats', records) @@ -101,17 +104,15 @@ export default function ServerDetail({ name }: { name: string }) { } }, [updatedSystem]) - // get cpu data + // create cpu / mem / disk data for charts useEffect(() => { if (!serverStats.length) { return } - - console.log('stats', serverStats) - // let maxCpu = 0 const cpuData = [] as typeof cpuChartData const memData = [] as typeof memChartData const diskData = [] as typeof diskChartData + const diskIoData = [] as typeof diskIoChartData for (let { created, stats } of serverStats) { const time = new Date(created).getTime() cpuData.push({ time, cpu: stats.cpu }) @@ -122,18 +123,33 @@ export default function ServerDetail({ name }: { name: string }) { memCache: stats.mb, }) diskData.push({ time, disk: stats.d, diskUsed: stats.du }) + diskIoData.push({ time, read: stats.dr, write: stats.dw }) } - setCpuChartData(cpuData.reverse()) - setMemChartData(memData.reverse()) - setDiskChartData(diskData.reverse()) + setCpuChartData(cpuData) + setMemChartData(memData) + setDiskChartData(diskData) + setDiskIoChartData(diskIoData) }, [serverStats]) useEffect(() => { + if (!serverStats.length) { + return + } + const now = new Date() + const startTime = chartTimeData[chartTime].getOffset(now) + const scale = scaleTime([startTime.getTime(), now], [0, cpuChartData.length]) + setTicks(scale.ticks().map((d) => d.getTime())) + }, [chartTime, serverStats]) + + useEffect(() => { + if (!server.id) { + return + } pb.collection('container_stats') .getFullList({ filter: `system="${server.id}" && created > "${getPbTimestamp('1h')}"`, fields: 'created,stats', - sort: '-created', + sort: 'created', }) .then((records) => { setContainers(records) @@ -158,19 +174,18 @@ export default function ServerDetail({ name }: { name: string }) { dockerMemData.push(memData) } // console.log('containerMemData', containerMemData) - setDockerCpuChartData(dockerCpuData.reverse()) - setDockerMemChartData(dockerMemData.reverse()) + setDockerCpuChartData(dockerCpuData) + setDockerMemChartData(dockerMemData) }, [containers]) const uptime = useMemo(() => { - console.log('making uptime') let uptime = server.info?.u || 0 if (uptime < 172800) { - return `${Math.floor(uptime / 3600)} hours` + return `${Math.trunc(uptime / 3600)} hours` } - return `${Math.floor(server.info?.u / 86400)} days` + return `${Math.trunc(server.info?.u / 86400)} days` }, [server.info?.u]) - if (!('id' in server)) { + if (!server.id) { return null } @@ -220,25 +235,34 @@ export default function ServerDetail({ name }: { name: string }) { title="Total CPU Usage" description="Average system-wide CPU utilization as a percentage" > - + {dockerCpuChartData.length > 0 && ( - + )} - + {dockerMemChartData.length > 0 && ( - + )} - - + + + + + diff --git a/site/src/lib/stores.ts b/site/src/lib/stores.ts index 161e5e7..a40ca9f 100644 --- a/site/src/lib/stores.ts +++ b/site/src/lib/stores.ts @@ -1,6 +1,6 @@ import PocketBase from 'pocketbase' -import { atom } from 'nanostores' -import { AlertRecord, SystemRecord } from '@/types' +import { atom, WritableAtom } from 'nanostores' +import { AlertRecord, ChartTimes, SystemRecord } from '@/types' import { createRouter } from '@nanostores/router' /** PocketBase JS Client */ @@ -35,4 +35,4 @@ export const $alerts = atom([] as AlertRecord[]) export const $publicKey = atom('') /** Chart time period */ -export const $chartTime = atom('1h') +export const $chartTime = atom('1h') as WritableAtom diff --git a/site/src/lib/utils.ts b/site/src/lib/utils.ts index 37f1a7d..9b688b4 100644 --- a/site/src/lib/utils.ts +++ b/site/src/lib/utils.ts @@ -2,9 +2,10 @@ import { toast } from '@/components/ui/use-toast' import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' import { $alerts, $systems, pb } from './stores' -import { AlertRecord, SystemRecord } from '@/types' +import { AlertRecord, ChartTimes, SystemRecord } from '@/types' import { RecordModel, RecordSubscription } from 'pocketbase' import { WritableAtom } from 'nanostores' +import { timeDay, timeHour } from 'd3-time' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -42,32 +43,35 @@ export const updateAlerts = () => { }) } -const shortTimeFormatter = new Intl.DateTimeFormat(undefined, { - // day: 'numeric', - // month: 'numeric', - // year: '2-digit', - // hour12: false, +const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: 'numeric', }) -export const formatShortTime = (timestamp: string) => { - // console.log('ts', timestamp) - return shortTimeFormatter.format(new Date(timestamp)) +export const hourWithMinutes = (timestamp: string) => { + return hourWithMinutesFormatter.format(new Date(timestamp)) } const shortDateFormatter = new Intl.DateTimeFormat(undefined, { day: 'numeric', month: 'short', - // year: '2-digit', - // hour12: false, hour: 'numeric', minute: 'numeric', }) export const formatShortDate = (timestamp: string) => { - console.log('ts', timestamp) + // console.log('ts', timestamp) return shortDateFormatter.format(new Date(timestamp)) } +const dayFormatter = new Intl.DateTimeFormat(undefined, { + day: 'numeric', + month: 'long', + // dateStyle: 'medium', +}) +export const formatDay = (timestamp: string) => { + // console.log('ts', timestamp) + return dayFormatter.format(new Date(timestamp)) +} + export const updateFavicon = (newIconUrl: string) => ((document.querySelector("link[rel='icon']") as HTMLLinkElement).href = newIconUrl) @@ -103,33 +107,42 @@ export function updateRecordList( $store.set(newRecords) } -export function getPbTimestamp(timeString: string) { - const now = new Date() - let timeValue = parseInt(timeString.slice(0, -1)) - let unit = timeString.slice(-1) - - if (unit === 'h') { - now.setUTCHours(now.getUTCHours() - timeValue) - } else { - // d - now.setUTCDate(now.getUTCDate() - timeValue) - } - - const year = now.getUTCFullYear() - const month = String(now.getUTCMonth() + 1).padStart(2, '0') - const day = String(now.getUTCDate()).padStart(2, '0') - const hours = String(now.getUTCHours()).padStart(2, '0') - const minutes = String(now.getUTCMinutes()).padStart(2, '0') - const seconds = String(now.getUTCSeconds()).padStart(2, '0') +export function getPbTimestamp(timeString: ChartTimes) { + const d = chartTimeData[timeString].getOffset(new Date()) + const year = d.getUTCFullYear() + const month = String(d.getUTCMonth() + 1).padStart(2, '0') + const day = String(d.getUTCDate()).padStart(2, '0') + const hours = String(d.getUTCHours()).padStart(2, '0') + const minutes = String(d.getUTCMinutes()).padStart(2, '0') + const seconds = String(d.getUTCSeconds()).padStart(2, '0') return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } -export const calculateXaxisTicks = (chartData: any[]) => { - const ticks: number[] = [] - const lastDate = chartData.at(-1)!.time - for (let i = 60; i >= 0; i--) { - ticks.push(lastDate - i * 60 * 1000) - } - return ticks +export const chartTimeData = { + '1h': { + label: '1 hour', + format: (timestamp: string) => hourWithMinutes(timestamp), + getOffset: (endTime: Date) => timeHour.offset(endTime, -1), + }, + '12h': { + label: '12 hours', + format: (timestamp: string) => hourWithMinutes(timestamp), + getOffset: (endTime: Date) => timeHour.offset(endTime, -12), + }, + '24h': { + label: '24 hours', + format: (timestamp: string) => hourWithMinutes(timestamp), + getOffset: (endTime: Date) => timeHour.offset(endTime, -24), + }, + '1w': { + label: '1 week', + format: (timestamp: string) => formatDay(timestamp), + getOffset: (endTime: Date) => timeDay.offset(endTime, -7), + }, + '30d': { + label: '30 days', + format: (timestamp: string) => formatDay(timestamp), + getOffset: (endTime: Date) => timeDay.offset(endTime, -30), + }, } diff --git a/site/src/types.d.ts b/site/src/types.d.ts index 7966e77..1b58a70 100644 --- a/site/src/types.d.ts +++ b/site/src/types.d.ts @@ -75,3 +75,5 @@ export interface AlertRecord extends RecordModel { name: string // user: string } + +export type ChartTimes = '1h' | '12h' | '24h' | '1w' | '30d'