update charts to use fixed time for x axis

This commit is contained in:
Henry Dollman
2024-07-16 16:40:37 -04:00
parent cdb069e633
commit 4f3796e9bc
7 changed files with 141 additions and 59 deletions

View File

@@ -8,10 +8,14 @@ import {
ChartTooltipContent, ChartTooltipContent,
} from '@/components/ui/chart' } from '@/components/ui/chart'
import { useMemo } from 'react' import { useMemo } from 'react'
import { formatShortDate, formatShortTime } from '@/lib/utils' import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils'
import Spinner from '../spinner' import Spinner from '../spinner'
export default function ({ chartData }: { chartData: Record<string, number | string>[] }) { export default function ContainerCpuChart({
chartData,
}: {
chartData: Record<string, number | string>[]
}) {
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
let config = {} as Record< let config = {} as Record<
string, string,
@@ -47,6 +51,8 @@ export default function ({ chartData }: { chartData: Record<string, number | str
return config satisfies ChartConfig return config satisfies ChartConfig
}, [chartData]) }, [chartData])
const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData])
if (!chartData.length) { if (!chartData.length) {
return <Spinner /> return <Spinner />
} }
@@ -64,7 +70,7 @@ export default function ({ chartData }: { chartData: Record<string, number | str
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<YAxis <YAxis
domain={[0, (max: number) => Math.max(Math.ceil(max), 0.4)]} domain={[0, (max: number) => Math.max(Math.ceil(max), 0.4)]}
// tickCount={5} width={47}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
unit={'%'} unit={'%'}
@@ -72,6 +78,10 @@ export default function ({ chartData }: { chartData: Record<string, number | str
/> />
<XAxis <XAxis
dataKey="time" dataKey="time"
domain={[ticks[0], ticks.at(-1)!]}
ticks={ticks}
type="number"
scale={'time'}
tickLine={true} tickLine={true}
axisLine={false} axisLine={false}
tickMargin={8} tickMargin={8}
@@ -79,8 +89,10 @@ export default function ({ chartData }: { chartData: Record<string, number | str
tickFormatter={formatShortTime} tickFormatter={formatShortTime}
/> />
<ChartTooltip <ChartTooltip
cursor={false} // cursor={false}
labelFormatter={formatShortDate} animationEasing="ease-out"
animationDuration={150}
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
// @ts-ignore // @ts-ignore
itemSorter={(a, b) => b.value - a.value} itemSorter={(a, b) => b.value - a.value}
content={<ChartTooltipContent unit="%" indicator="line" />} content={<ChartTooltipContent unit="%" indicator="line" />}

View File

@@ -8,10 +8,14 @@ import {
ChartTooltipContent, ChartTooltipContent,
} from '@/components/ui/chart' } from '@/components/ui/chart'
import { useMemo } from 'react' import { useMemo } from 'react'
import { formatShortDate, formatShortTime } from '@/lib/utils' import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils'
import Spinner from '../spinner' import Spinner from '../spinner'
export default function ({ chartData }: { chartData: Record<string, number | string>[] }) { export default function ({ chartData }: { chartData: Record<string, number | string>[] }) {
if (!chartData.length) {
return <Spinner />
}
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
let config = {} as Record< let config = {} as Record<
string, string,
@@ -47,9 +51,7 @@ export default function ({ chartData }: { chartData: Record<string, number | str
return config satisfies ChartConfig return config satisfies ChartConfig
}, [chartData]) }, [chartData])
if (!chartData.length) { const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData])
return <Spinner />
}
return ( return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto"> <ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
@@ -78,6 +80,10 @@ export default function ({ chartData }: { chartData: Record<string, number | str
/> />
<XAxis <XAxis
dataKey="time" dataKey="time"
domain={[ticks[0], ticks.at(-1)!]}
ticks={ticks}
type="number"
scale={'time'}
tickLine={true} tickLine={true}
axisLine={false} axisLine={false}
tickMargin={8} tickMargin={8}
@@ -85,8 +91,10 @@ export default function ({ chartData }: { chartData: Record<string, number | str
tickFormatter={formatShortTime} tickFormatter={formatShortTime}
/> />
<ChartTooltip <ChartTooltip
cursor={false} // cursor={false}
labelFormatter={formatShortDate} animationEasing="ease-out"
animationDuration={150}
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
// @ts-ignore // @ts-ignore
itemSorter={(a, b) => b.value - a.value} itemSorter={(a, b) => b.value - a.value}
content={<ChartTooltipContent unit=" MiB" indicator="line" />} content={<ChartTooltipContent unit=" MiB" indicator="line" />}

View File

@@ -6,8 +6,9 @@ import {
ChartTooltip, ChartTooltip,
ChartTooltipContent, ChartTooltipContent,
} from '@/components/ui/chart' } from '@/components/ui/chart'
import { formatShortDate, formatShortTime } from '@/lib/utils' import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils'
import Spinner from '../spinner' import Spinner from '../spinner'
import { useMemo } from 'react'
const chartConfig = { const chartConfig = {
cpu: { cpu: {
@@ -16,11 +17,13 @@ const chartConfig = {
}, },
} satisfies ChartConfig } satisfies ChartConfig
export default function CpuChart({ chartData }: { chartData: { time: string; cpu: number }[] }) { export default function CpuChart({ chartData }: { chartData: { time: number; cpu: number }[] }) {
if (!chartData?.length) { if (!chartData?.length) {
return <Spinner /> return <Spinner />
} }
const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData])
return ( return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto"> <ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart accessibilityLayer data={chartData} margin={{ top: 10 }}> <AreaChart accessibilityLayer data={chartData} margin={{ top: 10 }}>
@@ -35,15 +38,24 @@ export default function CpuChart({ chartData }: { chartData: { time: string; cpu
{/* todo: short time if first date is same day, otherwise short date */} {/* todo: short time if first date is same day, otherwise short date */}
<XAxis <XAxis
dataKey="time" dataKey="time"
domain={[ticks[0], ticks.at(-1)!]}
ticks={ticks}
type="number"
scale={'time'}
axisLine={false} axisLine={false}
tickMargin={8} tickMargin={8}
minTickGap={30} minTickGap={35}
tickFormatter={formatShortTime} tickFormatter={formatShortTime}
/> />
<ChartTooltip <ChartTooltip
cursor={false} animationEasing="ease-out"
animationDuration={150}
content={ content={
<ChartTooltipContent unit="%" labelFormatter={formatShortDate} indicator="line" /> <ChartTooltipContent
unit="%"
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
indicator="line"
/>
} }
/> />
<Area <Area

View File

@@ -6,7 +6,7 @@ import {
ChartTooltip, ChartTooltip,
ChartTooltipContent, ChartTooltipContent,
} from '@/components/ui/chart' } from '@/components/ui/chart'
import { formatShortDate, formatShortTime } from '@/lib/utils' import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils'
import { useMemo } from 'react' import { useMemo } from 'react'
import Spinner from '../spinner' import Spinner from '../spinner'
// for (const data of chartData) { // for (const data of chartData) {
@@ -23,12 +23,18 @@ const chartConfig = {
export default function DiskChart({ export default function DiskChart({
chartData, chartData,
}: { }: {
chartData: { time: string; disk: number; diskUsed: number }[] chartData: { time: number; disk: number; diskUsed: number }[]
}) { }) {
if (!chartData.length) {
return <Spinner />
}
const diskSize = useMemo(() => { const diskSize = useMemo(() => {
return Math.round(chartData[0]?.disk) return Math.round(chartData[0]?.disk)
}, [chartData]) }, [chartData])
const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData])
// const ticks = useMemo(() => { // const ticks = useMemo(() => {
// let ticks = [0] // let ticks = [0]
// for (let i = 1; i < diskSize; i += diskSize / 5) { // for (let i = 1; i < diskSize; i += diskSize / 5) {
@@ -38,10 +44,6 @@ export default function DiskChart({
// return ticks // return ticks
// }, [diskSize]) // }, [diskSize])
if (!chartData.length) {
return <Spinner />
}
return ( return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto"> <ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart <AreaChart
@@ -68,6 +70,10 @@ export default function DiskChart({
{/* todo: short time if first date is same day, otherwise short date */} {/* todo: short time if first date is same day, otherwise short date */}
<XAxis <XAxis
dataKey="time" dataKey="time"
domain={[ticks[0], ticks.at(-1)!]}
ticks={ticks}
type="number"
scale={'time'}
tickLine={true} tickLine={true}
axisLine={false} axisLine={false}
tickMargin={8} tickMargin={8}
@@ -77,7 +83,11 @@ export default function DiskChart({
<ChartTooltip <ChartTooltip
cursor={false} cursor={false}
content={ content={
<ChartTooltipContent unit=" GiB" labelFormatter={formatShortDate} indicator="line" /> <ChartTooltipContent
unit=" GiB"
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
indicator="line"
/>
} }
/> />
<Area <Area

View File

@@ -6,15 +6,21 @@ import {
ChartTooltip, ChartTooltip,
ChartTooltipContent, ChartTooltipContent,
} from '@/components/ui/chart' } from '@/components/ui/chart'
import { formatShortDate, formatShortTime } from '@/lib/utils' import { calculateXaxisTicks, formatShortDate, formatShortTime } from '@/lib/utils'
import { useMemo } from 'react' import { useMemo } from 'react'
import Spinner from '../spinner' import Spinner from '../spinner'
export default function ({ export default function MemChart({
chartData, chartData,
}: { }: {
chartData: { time: string; mem: number; memUsed: number; memCache: number }[] chartData: { time: number; mem: number; memUsed: number; memCache: number }[]
}) { }) {
if (!chartData.length) {
return <Spinner />
}
const ticks = useMemo(() => calculateXaxisTicks(chartData), [chartData])
const totalMem = useMemo(() => { const totalMem = useMemo(() => {
return Math.ceil(chartData[0]?.mem) return Math.ceil(chartData[0]?.mem)
}, [chartData]) }, [chartData])
@@ -33,10 +39,6 @@ export default function ({
[] []
) satisfies ChartConfig ) satisfies ChartConfig
if (!chartData.length) {
return <Spinner />
}
return ( return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto"> <ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart <AreaChart
@@ -59,6 +61,10 @@ export default function ({
{/* todo: short time if first date is same day, otherwise short date */} {/* todo: short time if first date is same day, otherwise short date */}
<XAxis <XAxis
dataKey="time" dataKey="time"
domain={[ticks[0], ticks.at(-1)!]}
ticks={ticks}
type="number"
scale={'time'}
tickLine={true} tickLine={true}
axisLine={false} axisLine={false}
tickMargin={8} tickMargin={8}
@@ -66,13 +72,15 @@ export default function ({
tickFormatter={formatShortTime} tickFormatter={formatShortTime}
/> />
<ChartTooltip <ChartTooltip
cursor={false} // cursor={false}
animationEasing="ease-out"
animationDuration={150}
content={ content={
<ChartTooltipContent <ChartTooltipContent
unit="GiB" unit="GiB"
// @ts-ignore // @ts-ignore
itemSorter={(a, b) => a.name.localeCompare(b.name)} itemSorter={(a, b) => a.name.localeCompare(b.name)}
labelFormatter={formatShortDate} labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
indicator="line" indicator="line"
/> />
} }

View File

@@ -27,22 +27,34 @@ export default function ServerDetail({ name }: { name: string }) {
const [containers, setContainers] = useState([] as ContainerStatsRecord[]) const [containers, setContainers] = useState([] as ContainerStatsRecord[])
const [serverStats, setServerStats] = useState([] as SystemStatsRecord[]) const [serverStats, setServerStats] = useState([] as SystemStatsRecord[])
const [cpuChartData, setCpuChartData] = useState([] as { time: string; cpu: number }[]) const [cpuChartData, setCpuChartData] = useState([] as { time: number; cpu: number }[])
const [memChartData, setMemChartData] = useState( const [memChartData, setMemChartData] = useState(
[] as { time: string; mem: number; memUsed: number; memCache: number }[] [] as { time: number; mem: number; memUsed: number; memCache: number }[]
) )
const [diskChartData, setDiskChartData] = useState( const [diskChartData, setDiskChartData] = useState(
[] as { time: string; disk: number; diskUsed: number }[] [] as { time: number; disk: number; diskUsed: number }[]
) )
const [containerCpuChartData, setContainerCpuChartData] = useState( const [dockerCpuChartData, setDockerCpuChartData] = useState(
[] as Record<string, number | string>[] [] as Record<string, number | string>[]
) )
const [containerMemChartData, setContainerMemChartData] = useState( const [dockerMemChartData, setDockerMemChartData] = useState(
[] as Record<string, number | string>[] [] as Record<string, number | string>[]
) )
useEffect(() => { useEffect(() => {
document.title = `${name} / Beszel` document.title = `${name} / Beszel`
return () => {
console.log('unmounting')
setServer({} as SystemRecord)
setCpuChartData([])
setMemChartData([])
setDiskChartData([])
setDockerCpuChartData([])
setDockerMemChartData([])
}
}, [name])
useEffect(() => {
if (server?.id && server.name === name) { if (server?.id && server.name === name) {
return return
} }
@@ -97,14 +109,19 @@ export default function ServerDetail({ name }: { name: string }) {
console.log('stats', serverStats) console.log('stats', serverStats)
// let maxCpu = 0 // let maxCpu = 0
const cpuData = [] as { time: string; cpu: number }[] const cpuData = [] as typeof cpuChartData
const memData = [] as { time: string; mem: number; memUsed: number; memCache: number }[] const memData = [] as typeof memChartData
const diskData = [] as { time: string; disk: number; diskUsed: number }[] const diskData = [] as typeof diskChartData
for (let { created, stats } of serverStats) { for (let { created, stats } of serverStats) {
cpuData.push({ time: created, cpu: stats.cpu }) const time = new Date(created).getTime()
// maxCpu = Math.max(maxCpu, stats.c) cpuData.push({ time, cpu: stats.cpu })
memData.push({ time: created, mem: stats.m, memUsed: stats.mu, memCache: stats.mb }) memData.push({
diskData.push({ time: created, disk: stats.d, diskUsed: stats.du }) time,
mem: stats.m,
memUsed: stats.mu,
memCache: stats.mb,
})
diskData.push({ time, disk: stats.d, diskUsed: stats.du })
} }
setCpuChartData(cpuData.reverse()) setCpuChartData(cpuData.reverse())
setMemChartData(memData.reverse()) setMemChartData(memData.reverse())
@@ -119,7 +136,6 @@ export default function ServerDetail({ name }: { name: string }) {
sort: '-created', sort: '-created',
}) })
.then((records) => { .then((records) => {
// console.log('containers', records)
setContainers(records) setContainers(records)
}) })
}, [server]) }, [server])
@@ -127,22 +143,23 @@ export default function ServerDetail({ name }: { name: string }) {
// container stats for charts // container stats for charts
useEffect(() => { useEffect(() => {
// console.log('containers', containers) // console.log('containers', containers)
const containerCpuData = [] as Record<string, number | string>[] const dockerCpuData = [] as Record<string, number | string>[]
const containerMemData = [] as Record<string, number | string>[] const dockerMemData = [] as Record<string, number | string>[]
for (let { created, stats } of containers) { for (let { created, stats } of containers) {
let cpuData = { time: created } as Record<string, number | string> const time = new Date(created).getTime()
let memData = { time: created } as Record<string, number | string> let cpuData = { time } as (typeof dockerCpuChartData)[0]
let memData = { time } as (typeof dockerMemChartData)[0]
for (let container of stats) { for (let container of stats) {
cpuData[container.n] = container.c cpuData[container.n] = container.c
memData[container.n] = container.m memData[container.n] = container.m
} }
containerCpuData.push(cpuData) dockerCpuData.push(cpuData)
containerMemData.push(memData) dockerMemData.push(memData)
} }
// console.log('containerMemData', containerMemData) // console.log('containerMemData', containerMemData)
setContainerCpuChartData(containerCpuData.reverse()) setDockerCpuChartData(dockerCpuData.reverse())
setContainerMemChartData(containerMemData.reverse()) setDockerMemChartData(dockerMemData.reverse())
}, [containers]) }, [containers])
const uptime = useMemo(() => { const uptime = useMemo(() => {
console.log('making uptime') console.log('making uptime')
@@ -206,18 +223,18 @@ export default function ServerDetail({ name }: { name: string }) {
<CpuChart chartData={cpuChartData} /> <CpuChart chartData={cpuChartData} />
</ChartCard> </ChartCard>
{containerCpuChartData.length > 0 && ( {dockerCpuChartData.length > 0 && (
<ChartCard title="Docker CPU Usage" description="CPU utilization of docker containers"> <ChartCard title="Docker CPU Usage" description="CPU utilization of docker containers">
<ContainerCpuChart chartData={containerCpuChartData} /> <ContainerCpuChart chartData={dockerCpuChartData} />
</ChartCard> </ChartCard>
)} )}
<ChartCard title="Total Memory Usage" description="Precise utilization at the recorded time"> <ChartCard title="Total Memory Usage" description="Precise utilization at the recorded time">
<MemChart chartData={memChartData} /> <MemChart chartData={memChartData} />
</ChartCard> </ChartCard>
{containerMemChartData.length > 0 && ( {dockerMemChartData.length > 0 && (
<ChartCard title="Docker Memory Usage" description="Memory usage of docker containers"> <ChartCard title="Docker Memory Usage" description="Memory usage of docker containers">
<ContainerMemChart chartData={containerMemChartData} /> <ContainerMemChart chartData={dockerMemChartData} />
</ChartCard> </ChartCard>
)} )}
<ChartCard title="Disk Usage" description="Precise usage at the recorded time"> <ChartCard title="Disk Usage" description="Precise usage at the recorded time">

View File

@@ -50,7 +50,10 @@ const shortTimeFormatter = new Intl.DateTimeFormat(undefined, {
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
}) })
export const formatShortTime = (timestamp: string) => shortTimeFormatter.format(new Date(timestamp)) export const formatShortTime = (timestamp: string) => {
// console.log('ts', timestamp)
return shortTimeFormatter.format(new Date(timestamp))
}
const shortDateFormatter = new Intl.DateTimeFormat(undefined, { const shortDateFormatter = new Intl.DateTimeFormat(undefined, {
day: 'numeric', day: 'numeric',
@@ -60,7 +63,10 @@ const shortDateFormatter = new Intl.DateTimeFormat(undefined, {
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
}) })
export const formatShortDate = (timestamp: string) => shortDateFormatter.format(new Date(timestamp)) export const formatShortDate = (timestamp: string) => {
console.log('ts', timestamp)
return shortDateFormatter.format(new Date(timestamp))
}
export const updateFavicon = (newIconUrl: string) => export const updateFavicon = (newIconUrl: string) =>
((document.querySelector("link[rel='icon']") as HTMLLinkElement).href = newIconUrl) ((document.querySelector("link[rel='icon']") as HTMLLinkElement).href = newIconUrl)
@@ -118,3 +124,12 @@ export function getPbTimestamp(timeString: string) {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` 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
}