mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
updates
This commit is contained in:
112
site/src/components/charts/container-cpu-chart.tsx
Normal file
112
site/src/components/charts/container-cpu-chart.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
72
site/src/components/charts/cpu-chart.tsx
Normal file
72
site/src/components/charts/cpu-chart.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
84
site/src/components/charts/disk-chart.tsx
Normal file
84
site/src/components/charts/disk-chart.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
72
site/src/components/charts/mem-chart.tsx
Normal file
72
site/src/components/charts/mem-chart.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@@ -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> */}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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))
|
||||||
|
@@ -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
7
site/src/types.d.ts
vendored
@@ -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
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user