This commit is contained in:
Henry Dollman
2024-07-11 00:38:30 -04:00
parent fa9e257864
commit 82888684d9
10 changed files with 742 additions and 437 deletions

View File

@@ -0,0 +1,112 @@
'use client'
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import { useMemo, useState } from 'react'
import { formatShortDate, formatShortTime } from '@/lib/utils'
export default function ({ chartData }: { chartData: Record<string, number | string>[] }) {
const [containerNames, setContainerNames] = useState([] as string[])
const chartConfig = useMemo(() => {
console.log('chartData', chartData)
let config = {} as Record<
string,
{
label: string
color: string
}
>
const lastRecord = chartData.at(-1)
// @ts-ignore
let allKeys = new Set(Object.keys(lastRecord))
allKeys.delete('time')
const keys = Array.from(allKeys)
keys.sort((a, b) => (lastRecord![b] as number) - (lastRecord![a] as number))
setContainerNames(keys)
const length = keys.length
for (let i = 0; i < length; i++) {
const key = keys[i]
const hue = ((i * 360) / length) % 360
config[key] = {
label: key,
color: `hsl(${hue}, 60%, 60%)`,
}
}
console.log('config', config)
return config satisfies ChartConfig
}, [chartData])
if (!containerNames.length) {
return null
}
return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
top: 12,
}}
// reverseStackOrder={true}
>
<CartesianGrid vertical={false} />
{/* <YAxis domain={[0, 250]} tickCount={5} tickLine={false} axisLine={false} tickMargin={8} /> */}
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={formatShortTime}
/>
<ChartTooltip
cursor={false}
labelFormatter={formatShortDate}
// itemSorter={(item) => {
// console.log('itemSorter', item)
// return -item.value
// }}
content={
<ChartTooltipContent
// itemSorter={(item) => {
// console.log('itemSorter', item)
// return -item.value
// }}
indicator="line"
/>
}
/>
{containerNames.map((key) => (
<Area
key={key}
dataKey={key}
type="natural"
fill={chartConfig[key].color}
fillOpacity={0.4}
stroke={chartConfig[key].color}
stackId="a"
/>
))}
{/* <Area
dataKey="other"
type="natural"
fill="var(--color-other)"
fillOpacity={0.4}
stroke="var(--color-other)"
stackId="a"
/> */}
{/* <ChartLegend content={<ChartLegendContent />} className="flex-wrap gap-y-2 mb-2" /> */}
</AreaChart>
</ChartContainer>
)
}

View File

@@ -0,0 +1,72 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import { formatShortDate, formatShortTime } from '@/lib/utils'
// for (const data of chartData) {
// data.month = formatDateShort(data.month)
// }
const chartConfig = {
cpu: {
label: 'CPU Usage',
color: 'hsl(var(--chart-1))',
},
} satisfies ChartConfig
export default function ({ chartData }: { chartData: { time: string; cpu: number }[] }) {
return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 0,
top: 7,
bottom: 7,
}}
>
<CartesianGrid vertical={false} />
<YAxis
domain={[0, 100]}
tickCount={5}
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(v) => `${v}%`}
/>
{/* todo: short time if first date is same day, otherwise short date */}
<XAxis
dataKey="time"
tickLine={true}
axisLine={false}
tickMargin={8}
minTickGap={30}
tickFormatter={formatShortTime}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={formatShortDate}
defaultValue={'%'}
indicator="line"
/>
}
/>
<Area
dataKey="cpu"
type="natural"
fill="var(--color-cpu)"
fillOpacity={0.4}
stroke="var(--color-cpu)"
/>
</AreaChart>
</ChartContainer>
)
}

View File

@@ -0,0 +1,84 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import { formatShortDate, formatShortTime } from '@/lib/utils'
import { useMemo } from 'react'
// for (const data of chartData) {
// data.month = formatDateShort(data.month)
// }
const chartConfig = {
diskUsed: {
label: 'Disk Use',
color: 'hsl(var(--chart-3))',
},
} satisfies ChartConfig
export default function ({
chartData,
}: {
chartData: { time: string; disk: number; diskUsed: number }[]
}) {
const diskSize = useMemo(() => {
return Math.round(chartData[0]?.disk)
}, [chartData])
// const ticks = useMemo(() => {
// let ticks = [0]
// for (let i = 1; i < diskSize; i += diskSize / 5) {
// ticks.push(Math.trunc(i))
// }
// ticks.push(diskSize)
// return ticks
// }, [diskSize])
return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 0,
top: 7,
bottom: 7,
}}
>
<CartesianGrid vertical={false} />
<YAxis
domain={[0, diskSize]}
tickCount={10}
// ticks={ticks}
tickLine={false}
axisLine={false}
tickFormatter={(v) => `${v} GiB`}
/>
{/* todo: short time if first date is same day, otherwise short date */}
<XAxis
dataKey="time"
tickLine={true}
axisLine={false}
tickMargin={8}
minTickGap={30}
tickFormatter={formatShortTime}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent labelFormatter={formatShortDate} indicator="line" />}
/>
<Area
dataKey="diskUsed"
type="natural"
fill="var(--color-diskUsed)"
fillOpacity={0.4}
stroke="var(--color-diskUsed)"
/>
</AreaChart>
</ChartContainer>
)
}

View File

@@ -0,0 +1,72 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import { formatShortDate, formatShortTime } from '@/lib/utils'
import { useMemo } from 'react'
const chartConfig = {
memUsed: {
label: 'Memory Use',
color: 'hsl(var(--chart-2))',
},
} satisfies ChartConfig
export default function ({
chartData,
}: {
chartData: { time: string; mem: number; memUsed: number }[]
}) {
const totalMem = useMemo(() => {
return Math.ceil(chartData[0]?.mem)
}, [chartData])
return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 0,
top: 7,
bottom: 7,
}}
>
<CartesianGrid vertical={false} />
<YAxis
// use "ticks" instead of domain / tickcount if need more control
domain={[0, totalMem]}
tickCount={9}
tickLine={false}
axisLine={false}
tickFormatter={(v) => `${v} GiB`}
/>
{/* todo: short time if first date is same day, otherwise short date */}
<XAxis
dataKey="time"
tickLine={true}
axisLine={false}
tickMargin={8}
minTickGap={30}
tickFormatter={formatShortTime}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent labelFormatter={formatShortDate} indicator="line" />}
/>
<Area
dataKey="memUsed"
type="natural"
fill="var(--color-memUsed)"
fillOpacity={0.4}
stroke="var(--color-memUsed)"
/>
</AreaChart>
</ChartContainer>
)
}

View File

@@ -1,117 +0,0 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import { formatDateShort } from '@/lib/utils'
const chartData = [
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
]
// for (const data of chartData) {
// data.month = formatDateShort(data.month)
// }
const chartConfig = {
cpu: {
label: 'cpu',
color: 'hsl(var(--chart-1))',
},
} satisfies ChartConfig
export default function () {
return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 0,
top: 7,
bottom: 7,
}}
>
<CartesianGrid vertical={false} />
<YAxis domain={[0, 100]} tickCount={5} tickLine={false} axisLine={false} tickMargin={8} />
<XAxis
dataKey="month"
tickLine={true}
axisLine={false}
tickMargin={8}
minTickGap={30}
tickFormatter={(value) => formatDateShort(value)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
<Area
dataKey="cpu"
type="natural"
fill="var(--color-cpu)"
fillOpacity={0.4}
stroke="var(--color-cpu)"
/>
</AreaChart>
</ChartContainer>
)
}

View File

@@ -1,12 +1,15 @@
import { $servers, pb } from '@/lib/stores' import { $servers, pb } from '@/lib/stores'
import { ContainerStatsRecord, SystemRecord } from '@/types' import { ContainerStatsRecord, SystemRecord, SystemStats, SystemStatsRecord } from '@/types'
import { Suspense, lazy, useEffect, useState } from 'react' import { Suspense, lazy, useEffect, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import Spinner from '../spinner' import Spinner from '../spinner'
// import { CpuChart } from '../cpu-chart' import CpuChart from '../charts/cpu-chart'
import MemChart from '../charts/mem-chart'
import DiskChart from '../charts/disk-chart'
import ContainerCpuChart from '../charts/container-cpu-chart'
const CpuChart = lazy(() => import('../cpu-chart')) // const CpuChart = lazy(() => import('../cpu-chart'))
function timestampToBrowserTime(timestamp: string) { function timestampToBrowserTime(timestamp: string) {
const date = new Date(timestamp) const date = new Date(timestamp)
@@ -25,9 +28,57 @@ export default function ServerDetail({ name }: { name: string }) {
const [server, setServer] = useState({} as SystemRecord) const [server, setServer] = useState({} as SystemRecord)
const [containers, setContainers] = useState([] as ContainerStatsRecord[]) const [containers, setContainers] = useState([] as ContainerStatsRecord[])
const [serverStats, setServerStats] = useState([] as SystemStatsRecord[])
const [cpuChartData, setCpuChartData] = useState({} as { time: string; cpu: number }[])
const [memChartData, setMemChartData] = useState(
{} as { time: string; mem: number; memUsed: number }[]
)
const [diskChartData, setDiskChartData] = useState(
{} as { time: string; disk: number; diskUsed: number }[]
)
const [containerCpuChartData, setContainerCpuChartData] = useState(
[] as Record<string, number | string>[]
)
// get stats
useEffect(() => {
if (!('name' in server)) {
return
}
pb.collection<SystemStatsRecord>('system_stats')
.getList(1, 60, {
filter: `system="${server.id}"`,
fields: 'created,stats',
sort: '-created',
})
.then((records) => {
console.log('stats', records)
setServerStats(records.items)
})
}, [server])
// get cpu data
useEffect(() => {
if (!serverStats.length) {
return
}
const cpuData = [] as { time: string; cpu: number }[]
const memData = [] as { time: string; mem: number; memUsed: number }[]
const diskData = [] as { time: string; disk: number; diskUsed: number }[]
for (let { created, stats } of serverStats) {
cpuData.push({ time: created, cpu: stats.cpu })
memData.push({ time: created, mem: stats.mem, memUsed: stats.memUsed })
diskData.push({ time: created, disk: stats.disk, diskUsed: stats.diskUsed })
}
setCpuChartData(cpuData.reverse())
setMemChartData(memData.reverse())
setDiskChartData(diskData.reverse())
}, [serverStats])
useEffect(() => { useEffect(() => {
document.title = name document.title = name
}, []) }, [name])
useEffect(() => { useEffect(() => {
if ($servers.get().length === 0) { if ($servers.get().length === 0) {
@@ -47,29 +98,84 @@ export default function ServerDetail({ name }: { name: string }) {
// }) // })
pb.collection<ContainerStatsRecord>('container_stats') pb.collection<ContainerStatsRecord>('container_stats')
.getList(1, 2, { .getList(1, 60, {
filter: `system="${matchingServer.id}"`, filter: `system="${matchingServer.id}"`,
fields: 'created,stats', fields: 'created,stats',
sort: '-created', sort: '-created',
}) })
.then((records) => { .then((records) => {
console.log('records', records) // console.log('records', records)
setContainers(records.items) setContainers(records.items)
}) })
}, [servers]) }, [servers, name])
// container stats for charts
useEffect(() => {
console.log('containers', containers)
const containerCpuData = [] as Record<string, number | string>[]
for (let { created, stats } of containers) {
let obj = { time: created } as Record<string, number | string>
for (let { name, cpu } of stats) {
obj[name] = cpu * 10
}
containerCpuData.push(obj)
}
setContainerCpuChartData(containerCpuData.reverse())
console.log('containerCpuData', containerCpuData)
}, [containers])
return ( return (
<> <>
<div className="grid grid-cols-1 gap-10"> <div className="grid grid-cols-2 gap-6 mb-10">
<Card> <Card className="pb-2 col-span-2">
<CardHeader> <CardHeader>
<CardTitle>CPU Usage</CardTitle> <CardTitle>CPU Usage</CardTitle>
<CardDescription>Showing total visitors for the last 30 minutes</CardDescription> <CardDescription>
Average usage of the one minute preceding the recorded time
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="pl-0 w-[calc(100%-2em)] h-72 relative"> <CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
<Suspense fallback={<Spinner />}> {/* <Suspense fallback={<Spinner />}> */}
<CpuChart /> <CpuChart chartData={cpuChartData} />
</Suspense> {/* </Suspense> */}
</CardContent>
</Card>
<Card className="pb-2">
<CardHeader>
<CardTitle>Memory Usage</CardTitle>
<CardDescription>Precise usage at the recorded time</CardDescription>
</CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
{/* <Suspense fallback={<Spinner />}> */}
<MemChart chartData={memChartData} />
{/* </Suspense> */}
</CardContent>
</Card>
<Card className="pb-2">
<CardHeader>
<CardTitle>Disk Usage</CardTitle>
<CardDescription>Precise usage at the recorded time</CardDescription>
</CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
{/* <Suspense fallback={<Spinner />}> */}
<DiskChart chartData={diskChartData} />
{/* </Suspense> */}
</CardContent>
</Card>
<Card className="pb-2 col-span-2">
<CardHeader>
<CardTitle>Container CPU Usage</CardTitle>
<CardDescription>
Average usage of the one minute preceding the recorded time
</CardDescription>
</CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-64 relative'}>
{/* <Suspense fallback={<Spinner />}> */}
{containerCpuChartData.length > 0 && (
<ContainerCpuChart chartData={containerCpuChartData} />
)}
{/* </Suspense> */}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -86,14 +192,14 @@ export default function ServerDetail({ name }: { name: string }) {
</CardContent> </CardContent>
</Card> </Card>
<Card> {/* <Card>
<CardHeader> <CardHeader>
<CardTitle className={'mb-3'}>Containers</CardTitle> <CardTitle className={'mb-3'}>Containers</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<pre>{JSON.stringify(containers, null, 2)}</pre> <pre>{JSON.stringify(containers, null, 2)}</pre>
</CardContent> </CardContent>
</Card> </Card> */}
</> </>
) )
} }

View File

@@ -1,10 +1,10 @@
import * as React from "react" import * as React from 'react'
import * as RechartsPrimitive from "recharts" import * as RechartsPrimitive from 'recharts'
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const const THEMES = { light: '', dark: '.dark' } as const
export type ChartConfig = { export type ChartConfig = {
[k in string]: { [k in string]: {
@@ -26,7 +26,7 @@ function useChart() {
const context = React.useContext(ChartContext) const context = React.useContext(ChartContext)
if (!context) { if (!context) {
throw new Error("useChart must be used within a <ChartContainer />") throw new Error('useChart must be used within a <ChartContainer />')
} }
return context return context
@@ -34,15 +34,13 @@ function useChart() {
const ChartContainer = React.forwardRef< const ChartContainer = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & { React.ComponentProps<'div'> & {
config: ChartConfig config: ChartConfig
children: React.ComponentProps< children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children']
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
} }
>(({ id, className, children, config, ...props }, ref) => { >(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId() const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
return ( return (
<ChartContext.Provider value={{ config }}> <ChartContext.Provider value={{ config }}>
@@ -56,19 +54,15 @@ const ChartContainer = React.forwardRef<
{...props} {...props}
> >
<ChartStyle id={chartId} config={config} /> <ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer> <RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div> </div>
</ChartContext.Provider> </ChartContext.Provider>
) )
}) })
ChartContainer.displayName = "Chart" ChartContainer.displayName = 'Chart'
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter( const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) { if (!colorConfig.length) {
return null return null
@@ -82,12 +76,10 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
${prefix} [data-chart=${id}] { ${prefix} [data-chart=${id}] {
${colorConfig ${colorConfig
.map(([key, itemConfig]) => { .map(([key, itemConfig]) => {
const color = const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null return color ? ` --color-${key}: ${color};` : null
}) })
.join("\n")} .join('\n')}
} }
` `
), ),
@@ -101,10 +93,10 @@ const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef< const ChartTooltipContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> & React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & { React.ComponentProps<'div'> & {
hideLabel?: boolean hideLabel?: boolean
hideIndicator?: boolean hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed" indicator?: 'line' | 'dot' | 'dashed'
nameKey?: string nameKey?: string
labelKey?: string labelKey?: string
} }
@@ -114,7 +106,7 @@ const ChartTooltipContent = React.forwardRef<
active, active,
payload, payload,
className, className,
indicator = "dot", indicator = 'dot',
hideLabel = false, hideLabel = false,
hideIndicator = false, hideIndicator = false,
label, label,
@@ -135,18 +127,16 @@ const ChartTooltipContent = React.forwardRef<
} }
const [item] = payload const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}` const key = `${labelKey || item.dataKey || item.name || 'value'}`
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value = const value =
!labelKey && typeof label === "string" !labelKey && typeof label === 'string'
? config[label as keyof typeof config]?.label || label ? config[label as keyof typeof config]?.label || label
: itemConfig?.label : itemConfig?.label
if (labelFormatter) { if (labelFormatter) {
return ( return (
<div className={cn("font-medium", labelClassName)}> <div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>
{labelFormatter(value, payload)}
</div>
) )
} }
@@ -154,35 +144,27 @@ const ChartTooltipContent = React.forwardRef<
return null return null
} }
return <div className={cn("font-medium", labelClassName)}>{value}</div> return <div className={cn('font-medium', labelClassName)}>{value}</div>
}, [ }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) { if (!active || !payload?.length) {
return null return null
} }
const nestLabel = payload.length === 1 && indicator !== "dot" const nestLabel = payload.length === 1 && indicator !== 'dot'
return ( return (
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", 'grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl',
className className
)} )}
> >
{!nestLabel ? tooltipLabel : null} {!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5"> <div className="grid gap-1.5">
{payload.map((item, index) => { {payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}` const key = `${nameKey || item.name || item.dataKey || 'value'}`
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color const indicatorColor = color || item.payload.fill || item.color
@@ -190,11 +172,11 @@ const ChartTooltipContent = React.forwardRef<
<div <div
key={item.dataKey} key={item.dataKey}
className={cn( className={cn(
"flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", 'flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',
indicator === "dot" && "items-center" indicator === 'dot' && 'items-center'
)} )}
> >
{formatter && (item?.value !== undefined) && item.name ? ( {formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload) formatter(item.value, item.name, item, index, item.payload)
) : ( ) : (
<> <>
@@ -204,19 +186,19 @@ const ChartTooltipContent = React.forwardRef<
!hideIndicator && ( !hideIndicator && (
<div <div
className={cn( className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", 'shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]',
{ {
"h-2.5 w-2.5": indicator === "dot", 'h-2.5 w-2.5': indicator === 'dot',
"w-1": indicator === "line", 'w-1': indicator === 'line',
"w-0 border-[1.5px] border-dashed bg-transparent": 'w-0 border-[1.5px] border-dashed bg-transparent':
indicator === "dashed", indicator === 'dashed',
"my-0.5": nestLabel && indicator === "dashed", 'my-0.5': nestLabel && indicator === 'dashed',
} }
)} )}
style={ style={
{ {
"--color-bg": indicatorColor, '--color-bg': indicatorColor,
"--color-border": indicatorColor, '--color-border': indicatorColor,
} as React.CSSProperties } as React.CSSProperties
} }
/> />
@@ -224,8 +206,8 @@ const ChartTooltipContent = React.forwardRef<
)} )}
<div <div
className={cn( className={cn(
"flex flex-1 justify-between leading-none", 'flex flex-1 justify-between leading-none',
nestLabel ? "items-end" : "items-center" nestLabel ? 'items-end' : 'items-center'
)} )}
> >
<div className="grid gap-1.5"> <div className="grid gap-1.5">
@@ -250,22 +232,18 @@ const ChartTooltipContent = React.forwardRef<
) )
} }
) )
ChartTooltipContent.displayName = "ChartTooltip" ChartTooltipContent.displayName = 'ChartTooltip'
const ChartLegend = RechartsPrimitive.Legend const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef< const ChartLegendContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & React.ComponentProps<'div'> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
hideIcon?: boolean hideIcon?: boolean
nameKey?: string nameKey?: string
} }
>( >(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart() const { config } = useChart()
if (!payload?.length) { if (!payload?.length) {
@@ -276,20 +254,20 @@ const ChartLegendContent = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"flex items-center justify-center gap-4", 'flex items-center justify-center gap-4',
verticalAlign === "top" ? "pb-3" : "pt-3", verticalAlign === 'top' ? 'pb-3' : 'pt-3',
className className
)} )}
> >
{payload.map((item) => { {payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}` const key = `${nameKey || item.dataKey || 'value'}`
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key)
return ( return (
<div <div
key={item.value} key={item.value}
className={cn( className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground" 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground'
)} )}
> >
{itemConfig?.icon && !hideIcon ? ( {itemConfig?.icon && !hideIcon ? (
@@ -308,47 +286,33 @@ const ChartLegendContent = React.forwardRef<
})} })}
</div> </div>
) )
} })
) ChartLegendContent.displayName = 'ChartLegend'
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload. // Helper to extract item config from a payload.
function getPayloadConfigFromPayload( function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
config: ChartConfig, if (typeof payload !== 'object' || payload === null) {
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined return undefined
} }
const payloadPayload = const payloadPayload =
"payload" in payload && 'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload ? payload.payload
: undefined : undefined
let configLabelKey: string = key let configLabelKey: string = key
if ( if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string configLabelKey = payload[key as keyof typeof payload] as string
} else if ( } else if (
payloadPayload && payloadPayload &&
key in payloadPayload && key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string" typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
) { ) {
configLabelKey = payloadPayload[ configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
key as keyof typeof payloadPayload
] as string
} }
return configLabelKey in config return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
? config[configLabelKey]
: config[key as keyof typeof config]
} }
export { export {

View File

@@ -32,7 +32,7 @@ export const updateServerList = () => {
}) })
} }
export const shortDateFormatter = new Intl.DateTimeFormat('en-US', { const shortTimeFormatter = new Intl.DateTimeFormat(undefined, {
// day: 'numeric', // day: 'numeric',
// month: 'numeric', // month: 'numeric',
// year: '2-digit', // year: '2-digit',
@@ -40,5 +40,14 @@ export const shortDateFormatter = new Intl.DateTimeFormat('en-US', {
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
}) })
export const formatShortTime = (timestamp: string) => shortTimeFormatter.format(new Date(timestamp))
export const formatDateShort = (timestamp: string) => shortDateFormatter.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) => shortDateFormatter.format(new Date(timestamp))

View File

@@ -3,10 +3,8 @@ import React, { Suspense, lazy, useEffect } from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import Home from './components/routes/home.tsx' import Home from './components/routes/home.tsx'
import { ThemeProvider } from './components/theme-provider.tsx' import { ThemeProvider } from './components/theme-provider.tsx'
// import LoginPage from './components/login.tsx'
import { $authenticated, $router } from './lib/stores.ts' import { $authenticated, $router } from './lib/stores.ts'
import { ModeToggle } from './components/mode-toggle.tsx' import { ModeToggle } from './components/mode-toggle.tsx'
// import { CommandPalette } from './components/command-palette.tsx'
import { cn, updateServerList } from './lib/utils.ts' import { cn, updateServerList } from './lib/utils.ts'
import { buttonVariants } from './components/ui/button.tsx' import { buttonVariants } from './components/ui/button.tsx'
import { Github } from 'lucide-react' import { Github } from 'lucide-react'

7
site/src/types.d.ts vendored
View File

@@ -20,7 +20,7 @@ export interface SystemStats {
export interface ContainerStatsRecord extends RecordModel { export interface ContainerStatsRecord extends RecordModel {
system: string system: string
stats: ContainerStats stats: ContainerStats[]
} }
interface ContainerStats { interface ContainerStats {
@@ -29,3 +29,8 @@ interface ContainerStats {
mem: number mem: number
mempct: number mempct: number
} }
export interface SystemStatsRecord extends RecordModel {
system: string
stats: SystemStats
}