mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
add max 1m values for cpu, bandwidth, disk io
* removes unused things from chart.tsx * updates y axis width only if it grows * add generic area chart component and remove individual cpu, bandwidth, disk io charts
This commit is contained in:
@@ -6,38 +6,42 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
Cpu float64 `json:"cpu"`
|
Cpu float64 `json:"cpu"`
|
||||||
PeakCpu float64 `json:"pcpu,omitempty"`
|
MaxCpu float64 `json:"cpum,omitempty"`
|
||||||
Mem float64 `json:"m"`
|
Mem float64 `json:"m"`
|
||||||
MemUsed float64 `json:"mu"`
|
MemUsed float64 `json:"mu"`
|
||||||
MemPct float64 `json:"mp"`
|
MemPct float64 `json:"mp"`
|
||||||
MemBuffCache float64 `json:"mb"`
|
MemBuffCache float64 `json:"mb"`
|
||||||
MemZfsArc float64 `json:"mz,omitempty"` // ZFS ARC memory
|
MemZfsArc float64 `json:"mz,omitempty"` // ZFS ARC memory
|
||||||
Swap float64 `json:"s,omitempty"`
|
Swap float64 `json:"s,omitempty"`
|
||||||
SwapUsed float64 `json:"su,omitempty"`
|
SwapUsed float64 `json:"su,omitempty"`
|
||||||
DiskTotal float64 `json:"d"`
|
DiskTotal float64 `json:"d"`
|
||||||
DiskUsed float64 `json:"du"`
|
DiskUsed float64 `json:"du"`
|
||||||
DiskPct float64 `json:"dp"`
|
DiskPct float64 `json:"dp"`
|
||||||
DiskReadPs float64 `json:"dr"`
|
DiskReadPs float64 `json:"dr"`
|
||||||
DiskWritePs float64 `json:"dw"`
|
DiskWritePs float64 `json:"dw"`
|
||||||
NetworkSent float64 `json:"ns"`
|
MaxDiskReadPs float64 `json:"drm,omitempty"`
|
||||||
PeakNetworkSent float64 `json:"pns,omitempty"`
|
MaxDiskWritePs float64 `json:"dwm,omitempty"`
|
||||||
NetworkRecv float64 `json:"nr"`
|
NetworkSent float64 `json:"ns"`
|
||||||
PeakNetworkRecv float64 `json:"pnr,omitempty"`
|
NetworkRecv float64 `json:"nr"`
|
||||||
Temperatures map[string]float64 `json:"t,omitempty"`
|
MaxNetworkSent float64 `json:"nsm,omitempty"`
|
||||||
ExtraFs map[string]*FsStats `json:"efs,omitempty"`
|
MaxNetworkRecv float64 `json:"nrm,omitempty"`
|
||||||
|
Temperatures map[string]float64 `json:"t,omitempty"`
|
||||||
|
ExtraFs map[string]*FsStats `json:"efs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FsStats struct {
|
type FsStats struct {
|
||||||
Time time.Time `json:"-"`
|
Time time.Time `json:"-"`
|
||||||
Root bool `json:"-"`
|
Root bool `json:"-"`
|
||||||
Mountpoint string `json:"-"`
|
Mountpoint string `json:"-"`
|
||||||
DiskTotal float64 `json:"d"`
|
DiskTotal float64 `json:"d"`
|
||||||
DiskUsed float64 `json:"du"`
|
DiskUsed float64 `json:"du"`
|
||||||
TotalRead uint64 `json:"-"`
|
TotalRead uint64 `json:"-"`
|
||||||
TotalWrite uint64 `json:"-"`
|
TotalWrite uint64 `json:"-"`
|
||||||
DiskWritePs float64 `json:"w"`
|
DiskReadPs float64 `json:"r"`
|
||||||
DiskReadPs float64 `json:"r"`
|
DiskWritePs float64 `json:"w"`
|
||||||
|
MaxDiskReadPS float64 `json:"rm,omitempty"`
|
||||||
|
MaxDiskWritePS float64 `json:"wm,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetIoStats struct {
|
type NetIoStats struct {
|
||||||
|
@@ -151,11 +151,6 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
// use different counter for temps in case some records don't have them
|
// use different counter for temps in case some records don't have them
|
||||||
tempCount := float64(0)
|
tempCount := float64(0)
|
||||||
|
|
||||||
// peak values
|
|
||||||
peakCpu := float64(0)
|
|
||||||
peakNs := float64(0)
|
|
||||||
peakNr := float64(0)
|
|
||||||
|
|
||||||
var stats system.Stats
|
var stats system.Stats
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
record.UnmarshalJSONField("stats", &stats)
|
record.UnmarshalJSONField("stats", &stats)
|
||||||
@@ -175,9 +170,11 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
sum.NetworkSent += stats.NetworkSent
|
sum.NetworkSent += stats.NetworkSent
|
||||||
sum.NetworkRecv += stats.NetworkRecv
|
sum.NetworkRecv += stats.NetworkRecv
|
||||||
// set peak values
|
// set peak values
|
||||||
peakCpu = max(peakCpu, stats.PeakCpu, stats.Cpu)
|
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
|
||||||
peakNs = max(peakNs, stats.PeakNetworkSent, stats.NetworkSent)
|
sum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent)
|
||||||
peakNr = max(peakNr, stats.PeakNetworkRecv, stats.NetworkRecv)
|
sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv)
|
||||||
|
sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs)
|
||||||
|
sum.MaxDiskWritePs = max(sum.MaxDiskWritePs, stats.MaxDiskWritePs, stats.DiskWritePs)
|
||||||
// add temps to sum
|
// add temps to sum
|
||||||
if stats.Temperatures != nil {
|
if stats.Temperatures != nil {
|
||||||
tempCount++
|
tempCount++
|
||||||
@@ -198,29 +195,34 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
sum.ExtraFs[key].DiskUsed += value.DiskUsed
|
sum.ExtraFs[key].DiskUsed += value.DiskUsed
|
||||||
sum.ExtraFs[key].DiskWritePs += value.DiskWritePs
|
sum.ExtraFs[key].DiskWritePs += value.DiskWritePs
|
||||||
sum.ExtraFs[key].DiskReadPs += value.DiskReadPs
|
sum.ExtraFs[key].DiskReadPs += value.DiskReadPs
|
||||||
|
// peak values
|
||||||
|
sum.ExtraFs[key].MaxDiskReadPS = max(sum.ExtraFs[key].MaxDiskReadPS, value.MaxDiskReadPS, value.DiskReadPs)
|
||||||
|
sum.ExtraFs[key].MaxDiskWritePS = max(sum.ExtraFs[key].MaxDiskWritePS, value.MaxDiskWritePS, value.DiskWritePs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stats = system.Stats{
|
stats = system.Stats{
|
||||||
Cpu: twoDecimals(sum.Cpu / count),
|
Cpu: twoDecimals(sum.Cpu / count),
|
||||||
PeakCpu: twoDecimals(peakCpu),
|
Mem: twoDecimals(sum.Mem / count),
|
||||||
Mem: twoDecimals(sum.Mem / count),
|
MemUsed: twoDecimals(sum.MemUsed / count),
|
||||||
MemUsed: twoDecimals(sum.MemUsed / count),
|
MemPct: twoDecimals(sum.MemPct / count),
|
||||||
MemPct: twoDecimals(sum.MemPct / count),
|
MemBuffCache: twoDecimals(sum.MemBuffCache / count),
|
||||||
MemBuffCache: twoDecimals(sum.MemBuffCache / count),
|
MemZfsArc: twoDecimals(sum.MemZfsArc / count),
|
||||||
MemZfsArc: twoDecimals(sum.MemZfsArc / count),
|
Swap: twoDecimals(sum.Swap / count),
|
||||||
Swap: twoDecimals(sum.Swap / count),
|
SwapUsed: twoDecimals(sum.SwapUsed / count),
|
||||||
SwapUsed: twoDecimals(sum.SwapUsed / count),
|
DiskTotal: twoDecimals(sum.DiskTotal / count),
|
||||||
DiskTotal: twoDecimals(sum.DiskTotal / count),
|
DiskUsed: twoDecimals(sum.DiskUsed / count),
|
||||||
DiskUsed: twoDecimals(sum.DiskUsed / count),
|
DiskPct: twoDecimals(sum.DiskPct / count),
|
||||||
DiskPct: twoDecimals(sum.DiskPct / count),
|
DiskReadPs: twoDecimals(sum.DiskReadPs / count),
|
||||||
DiskReadPs: twoDecimals(sum.DiskReadPs / count),
|
DiskWritePs: twoDecimals(sum.DiskWritePs / count),
|
||||||
DiskWritePs: twoDecimals(sum.DiskWritePs / count),
|
NetworkSent: twoDecimals(sum.NetworkSent / count),
|
||||||
NetworkSent: twoDecimals(sum.NetworkSent / count),
|
NetworkRecv: twoDecimals(sum.NetworkRecv / count),
|
||||||
PeakNetworkSent: twoDecimals(peakNs),
|
MaxCpu: sum.MaxCpu,
|
||||||
NetworkRecv: twoDecimals(sum.NetworkRecv / count),
|
MaxDiskReadPs: sum.MaxDiskReadPs,
|
||||||
PeakNetworkRecv: twoDecimals(peakNr),
|
MaxDiskWritePs: sum.MaxDiskWritePs,
|
||||||
|
MaxNetworkSent: sum.MaxNetworkSent,
|
||||||
|
MaxNetworkRecv: sum.MaxNetworkRecv,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sum.Temperatures) != 0 {
|
if len(sum.Temperatures) != 0 {
|
||||||
@@ -234,10 +236,12 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
stats.ExtraFs = make(map[string]*system.FsStats)
|
stats.ExtraFs = make(map[string]*system.FsStats)
|
||||||
for key, value := range sum.ExtraFs {
|
for key, value := range sum.ExtraFs {
|
||||||
stats.ExtraFs[key] = &system.FsStats{
|
stats.ExtraFs[key] = &system.FsStats{
|
||||||
DiskTotal: twoDecimals(value.DiskTotal / count),
|
DiskTotal: twoDecimals(value.DiskTotal / count),
|
||||||
DiskUsed: twoDecimals(value.DiskUsed / count),
|
DiskUsed: twoDecimals(value.DiskUsed / count),
|
||||||
DiskWritePs: twoDecimals(value.DiskWritePs / count),
|
DiskWritePs: twoDecimals(value.DiskWritePs / count),
|
||||||
DiskReadPs: twoDecimals(value.DiskReadPs / count),
|
DiskReadPs: twoDecimals(value.DiskReadPs / count),
|
||||||
|
MaxDiskReadPS: value.MaxDiskReadPS,
|
||||||
|
MaxDiskWritePS: value.MaxDiskWritePS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
131
beszel/site/src/components/charts/area-chart.tsx
Normal file
131
beszel/site/src/components/charts/area-chart.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
|
import {
|
||||||
|
useYAxisWidth,
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
toFixedWithoutTrailingZeros,
|
||||||
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
|
} from '@/lib/utils'
|
||||||
|
// import Spinner from '../spinner'
|
||||||
|
import { ChartTimes, SystemStatsRecord } from '@/types'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
/** [label, key, color, opacity] */
|
||||||
|
type DataKeys = [string, string, number, number]
|
||||||
|
|
||||||
|
const getNestedValue = (path: string, max = false, data: any): number | null => {
|
||||||
|
// fallback value (obj?.stats?.cpum ? 0 : null) should only come into play when viewing
|
||||||
|
// a max value which doesn't exist, or the value was zero and omitted from the stats object.
|
||||||
|
// so we check if cpum is present. if so, return 0 to make sure the zero value is displayed.
|
||||||
|
// if not, return null - there is no max data so do not display anything.
|
||||||
|
return `stats.${path}${max ? 'm' : ''}`
|
||||||
|
.split('.')
|
||||||
|
.reduce((acc: any, key: string) => acc?.[key] ?? (data.stats?.cpum ? 0 : null), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AreaChartDefault({
|
||||||
|
ticks,
|
||||||
|
systemData,
|
||||||
|
showMax = false,
|
||||||
|
unit = ' MB/s',
|
||||||
|
chartName,
|
||||||
|
chartTime,
|
||||||
|
}: {
|
||||||
|
ticks: number[]
|
||||||
|
systemData: SystemStatsRecord[]
|
||||||
|
showMax?: boolean
|
||||||
|
unit?: string
|
||||||
|
chartName: string
|
||||||
|
chartTime: ChartTimes
|
||||||
|
}) {
|
||||||
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|
||||||
|
const dataKeys: DataKeys[] = useMemo(() => {
|
||||||
|
// [label, key, color, opacity]
|
||||||
|
if (chartName === 'CPU Usage') {
|
||||||
|
return [[chartName, 'cpu', 1, 0.4]]
|
||||||
|
} else if (chartName === 'dio') {
|
||||||
|
return [
|
||||||
|
['Write', 'dw', 3, 0.3],
|
||||||
|
['Read', 'dr', 1, 0.3],
|
||||||
|
]
|
||||||
|
} else if (chartName === 'bw') {
|
||||||
|
return [
|
||||||
|
['Sent', 'ns', 5, 0.2],
|
||||||
|
['Received', 'nr', 2, 0.2],
|
||||||
|
]
|
||||||
|
} else if (chartName.startsWith('efs')) {
|
||||||
|
return [
|
||||||
|
['Write', `${chartName}.w`, 3, 0.3],
|
||||||
|
['Read', `${chartName}.r`, 1, 0.3],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ChartContainer
|
||||||
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
|
'opacity-100': yAxisWidth,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis
|
||||||
|
className="tracking-tighter"
|
||||||
|
width={yAxisWidth}
|
||||||
|
tickFormatter={(value) => {
|
||||||
|
const val = toFixedWithoutTrailingZeros(value, 2) + unit
|
||||||
|
return updateYAxisWidth(val)
|
||||||
|
}}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="created"
|
||||||
|
domain={[ticks[0], ticks.at(-1)!]}
|
||||||
|
ticks={ticks}
|
||||||
|
type="number"
|
||||||
|
scale={'time'}
|
||||||
|
minTickGap={35}
|
||||||
|
tickMargin={8}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={chartTimeData[chartTime].format}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + unit}
|
||||||
|
indicator="line"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{dataKeys.map((key, i) => {
|
||||||
|
const color = `hsl(var(--chart-${key[2]}))`
|
||||||
|
return (
|
||||||
|
<Area
|
||||||
|
key={i}
|
||||||
|
dataKey={getNestedValue.bind(null, key[1], showMax)}
|
||||||
|
name={key[0]}
|
||||||
|
type="monotoneX"
|
||||||
|
fill={color}
|
||||||
|
fillOpacity={key[3]}
|
||||||
|
stroke={color}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{/* <ChartLegend content={<ChartLegendContent />} /> */}
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@@ -1,105 +0,0 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
|
||||||
import {
|
|
||||||
useYAxisWidth,
|
|
||||||
chartTimeData,
|
|
||||||
cn,
|
|
||||||
formatShortDate,
|
|
||||||
toFixedWithoutTrailingZeros,
|
|
||||||
twoDecimalString,
|
|
||||||
} from '@/lib/utils'
|
|
||||||
// import Spinner from '../spinner'
|
|
||||||
import { useStore } from '@nanostores/react'
|
|
||||||
import { $chartTime } from '@/lib/stores'
|
|
||||||
import { SystemStatsRecord } from '@/types'
|
|
||||||
|
|
||||||
export default function BandwidthChart({
|
|
||||||
ticks,
|
|
||||||
systemData,
|
|
||||||
}: {
|
|
||||||
ticks: number[]
|
|
||||||
systemData: SystemStatsRecord[]
|
|
||||||
}) {
|
|
||||||
const chartTime = useStore($chartTime)
|
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
|
||||||
<ChartContainer
|
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
|
||||||
'opacity-100': yAxisWidth,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<AreaChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={systemData}
|
|
||||||
margin={{
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 10,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<YAxis
|
|
||||||
className="tracking-tighter"
|
|
||||||
width={yAxisWidth}
|
|
||||||
// domain={[0, (max: number) => (max <= 0.4 ? 0.4 : Math.ceil(max))]}
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
const val = toFixedWithoutTrailingZeros(value, 2) + ' MB/s'
|
|
||||||
return updateYAxisWidth(val)
|
|
||||||
}}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
// unit={' MB/s'}
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="created"
|
|
||||||
domain={[ticks[0], ticks.at(-1)!]}
|
|
||||||
ticks={ticks}
|
|
||||||
type="number"
|
|
||||||
scale={'time'}
|
|
||||||
minTickGap={35}
|
|
||||||
tickMargin={8}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={chartTimeData[chartTime].format}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
|
||||||
animationEasing="ease-out"
|
|
||||||
animationDuration={150}
|
|
||||||
content={
|
|
||||||
<ChartTooltipContent
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
|
||||||
contentFormatter={(item) => twoDecimalString(item.value) + ' MB/s'}
|
|
||||||
indicator="line"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
dataKey="stats.ns"
|
|
||||||
name="Sent"
|
|
||||||
type="monotoneX"
|
|
||||||
fill="hsl(var(--chart-5))"
|
|
||||||
fillOpacity={0.2}
|
|
||||||
stroke="hsl(var(--chart-5))"
|
|
||||||
// animationDuration={1200}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
dataKey="stats.nr"
|
|
||||||
name="Received"
|
|
||||||
type="monotoneX"
|
|
||||||
fill="hsl(var(--chart-2))"
|
|
||||||
fillOpacity={0.2}
|
|
||||||
stroke="hsl(var(--chart-2))"
|
|
||||||
// animationDuration={1200}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@@ -6,7 +6,14 @@ import {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from '@/components/ui/chart'
|
} from '@/components/ui/chart'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useYAxisWidth, chartTimeData, cn, formatShortDate, twoDecimalString } from '@/lib/utils'
|
import {
|
||||||
|
useYAxisWidth,
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime, $containerFilter } from '@/lib/stores'
|
import { $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
@@ -65,7 +72,6 @@ export default function ContainerCpuChart({
|
|||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
@@ -74,9 +80,7 @@ export default function ContainerCpuChart({
|
|||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
// syncId={'cpu'}
|
// syncId={'cpu'}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
margin={{
|
margin={chartMargin}
|
||||||
top: 10,
|
|
||||||
}}
|
|
||||||
reverseStackOrder={true}
|
reverseStackOrder={true}
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
|
@@ -13,6 +13,7 @@ import {
|
|||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
twoDecimalString,
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
@@ -72,7 +73,6 @@ export default function ContainerMemChart({
|
|||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
@@ -81,9 +81,7 @@ export default function ContainerMemChart({
|
|||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={chartData}
|
data={chartData}
|
||||||
reverseStackOrder={true}
|
reverseStackOrder={true}
|
||||||
margin={{
|
margin={chartMargin}
|
||||||
top: 10,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
|
@@ -13,6 +13,7 @@ import {
|
|||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
twoDecimalString,
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
@@ -64,15 +65,10 @@ export default function ContainerCpuChart({
|
|||||||
return config satisfies ChartConfig
|
return config satisfies ChartConfig
|
||||||
}, [chartData])
|
}, [chartData])
|
||||||
|
|
||||||
// if (!chartData.length || !ticks.length) {
|
|
||||||
// return <Spinner />
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
@@ -80,9 +76,7 @@ export default function ContainerCpuChart({
|
|||||||
<AreaChart
|
<AreaChart
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={chartData}
|
data={chartData}
|
||||||
margin={{
|
margin={chartMargin}
|
||||||
top: 10,
|
|
||||||
}}
|
|
||||||
reverseStackOrder={true}
|
reverseStackOrder={true}
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
|
@@ -1,11 +1,19 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
import { useYAxisWidth, chartTimeData, cn, formatShortDate, twoDecimalString } from '@/lib/utils'
|
import {
|
||||||
|
useYAxisWidth,
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime, $cpuMax } from '@/lib/stores'
|
||||||
import { SystemStatsRecord } from '@/types'
|
import { SystemStatsRecord } from '@/types'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
export default function CpuChart({
|
export default function CpuChart({
|
||||||
ticks,
|
ticks,
|
||||||
@@ -16,11 +24,16 @@ export default function CpuChart({
|
|||||||
}) {
|
}) {
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
const showMax = useStore($cpuMax)
|
||||||
|
|
||||||
|
const dataKey = useMemo(
|
||||||
|
() => `stats.cpu${showMax && chartTime !== '1h' ? 'm' : ''}`,
|
||||||
|
[showMax, systemData]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
@@ -28,7 +41,7 @@ export default function CpuChart({
|
|||||||
<AreaChart
|
<AreaChart
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={systemData}
|
data={systemData}
|
||||||
margin={{ top: 10 }}
|
margin={chartMargin}
|
||||||
// syncId={'cpu'}
|
// syncId={'cpu'}
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
@@ -63,16 +76,13 @@ export default function CpuChart({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="stats.cpu"
|
dataKey={dataKey}
|
||||||
name="CPU Usage"
|
name="CPU Usage"
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill="hsl(var(--chart-1))"
|
fill="hsl(var(--chart-1))"
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.4}
|
||||||
stroke="hsl(var(--chart-1))"
|
stroke="hsl(var(--chart-1))"
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
// animationEasing="ease-out"
|
|
||||||
// animationDuration={1200}
|
|
||||||
// animateNewValues={true}
|
|
||||||
/>
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
|
@@ -10,6 +10,7 @@ import {
|
|||||||
toFixedFloat,
|
toFixedFloat,
|
||||||
getSizeVal,
|
getSizeVal,
|
||||||
getSizeUnit,
|
getSizeUnit,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import { useMemo } from 'react'
|
// import { useMemo } from 'react'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
@@ -35,21 +36,11 @@ export default function DiskChart({
|
|||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AreaChart
|
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
|
||||||
accessibilityLayer
|
|
||||||
data={systemData}
|
|
||||||
margin={{
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 10,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
|
@@ -1,102 +0,0 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
|
||||||
import {
|
|
||||||
useYAxisWidth,
|
|
||||||
chartTimeData,
|
|
||||||
cn,
|
|
||||||
formatShortDate,
|
|
||||||
toFixedWithoutTrailingZeros,
|
|
||||||
twoDecimalString,
|
|
||||||
} from '@/lib/utils'
|
|
||||||
// import Spinner from '../spinner'
|
|
||||||
import { useStore } from '@nanostores/react'
|
|
||||||
import { $chartTime } from '@/lib/stores'
|
|
||||||
import { SystemStatsRecord } from '@/types'
|
|
||||||
|
|
||||||
export default function DiskIoChart({
|
|
||||||
ticks,
|
|
||||||
systemData,
|
|
||||||
dataKeys,
|
|
||||||
}: {
|
|
||||||
ticks: number[]
|
|
||||||
systemData: SystemStatsRecord[]
|
|
||||||
dataKeys: string[]
|
|
||||||
}) {
|
|
||||||
const chartTime = useStore($chartTime)
|
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
|
||||||
<ChartContainer
|
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
|
||||||
'opacity-100': yAxisWidth,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<AreaChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={systemData}
|
|
||||||
margin={{
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 10,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<YAxis
|
|
||||||
className="tracking-tighter"
|
|
||||||
width={yAxisWidth}
|
|
||||||
// domain={[0, (max: number) => (max <= 0.4 ? 0.4 : Math.ceil(max))]}
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
const val = toFixedWithoutTrailingZeros(value, 2) + ' MB/s'
|
|
||||||
return updateYAxisWidth(val)
|
|
||||||
}}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="created"
|
|
||||||
domain={[ticks[0], ticks.at(-1)!]}
|
|
||||||
ticks={ticks}
|
|
||||||
type="number"
|
|
||||||
scale={'time'}
|
|
||||||
minTickGap={35}
|
|
||||||
tickMargin={8}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={chartTimeData[chartTime].format}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
|
||||||
animationEasing="ease-out"
|
|
||||||
animationDuration={150}
|
|
||||||
content={
|
|
||||||
<ChartTooltipContent
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
|
||||||
contentFormatter={(item) => twoDecimalString(item.value) + ' MB/s'}
|
|
||||||
indicator="line"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{dataKeys.map((dataKey, i) => {
|
|
||||||
const action = i ? 'Read' : 'Write'
|
|
||||||
const color = i ? 'hsl(var(--chart-1))' : 'hsl(var(--chart-3))'
|
|
||||||
return (
|
|
||||||
<Area
|
|
||||||
key={i}
|
|
||||||
dataKey={dataKey}
|
|
||||||
name={action}
|
|
||||||
type="monotoneX"
|
|
||||||
fill={color}
|
|
||||||
fillOpacity={0.3}
|
|
||||||
stroke={color}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@@ -8,6 +8,7 @@ import {
|
|||||||
toFixedFloat,
|
toFixedFloat,
|
||||||
twoDecimalString,
|
twoDecimalString,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
@@ -32,18 +33,11 @@ export default function MemChart({
|
|||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AreaChart
|
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
|
||||||
accessibilityLayer
|
|
||||||
data={systemData}
|
|
||||||
margin={{
|
|
||||||
top: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
{totalMem && (
|
{totalMem && (
|
||||||
<YAxis
|
<YAxis
|
||||||
|
@@ -8,8 +8,8 @@ import {
|
|||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
twoDecimalString,
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime } from '@/lib/stores'
|
||||||
import { SystemStatsRecord } from '@/types'
|
import { SystemStatsRecord } from '@/types'
|
||||||
@@ -27,12 +27,11 @@ export default function SwapChart({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AreaChart accessibilityLayer data={systemData} margin={{ top: 10 }}>
|
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
|
@@ -14,6 +14,7 @@ import {
|
|||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
twoDecimalString,
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime } from '@/lib/stores'
|
||||||
@@ -60,21 +61,11 @@ export default function TemperatureChart({
|
|||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<LineChart
|
<LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}>
|
||||||
accessibilityLayer
|
|
||||||
data={newChartData.data}
|
|
||||||
margin={{
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 10,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from '@/lib/stores'
|
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from '@/lib/stores'
|
||||||
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
||||||
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
import { Card, CardHeader, CardTitle, CardDescription } from '../ui/card'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
|
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
|
||||||
@@ -12,16 +12,15 @@ import { scaleTime } from 'd3-scale'
|
|||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||||
import { Button, buttonVariants } from '../ui/button'
|
import { Button, buttonVariants } from '../ui/button'
|
||||||
import { Input } from '../ui/input'
|
import { Input } from '../ui/input'
|
||||||
import { Rows, TuxIcon } from '../ui/icons'
|
import { ChartAverage, ChartMax, Rows, TuxIcon } from '../ui/icons'
|
||||||
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
|
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||||
|
|
||||||
const CpuChart = lazy(() => import('../charts/cpu-chart'))
|
|
||||||
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
|
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
|
||||||
const MemChart = lazy(() => import('../charts/mem-chart'))
|
const MemChart = lazy(() => import('../charts/mem-chart'))
|
||||||
const ContainerMemChart = lazy(() => import('../charts/container-mem-chart'))
|
const ContainerMemChart = lazy(() => import('../charts/container-mem-chart'))
|
||||||
const DiskChart = lazy(() => import('../charts/disk-chart'))
|
const DiskChart = lazy(() => import('../charts/disk-chart'))
|
||||||
const DiskIoChart = lazy(() => import('../charts/disk-io-chart'))
|
const AreaChartDefault = lazy(() => import('../charts/area-chart'))
|
||||||
const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
|
|
||||||
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
|
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
|
||||||
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
||||||
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
||||||
@@ -29,11 +28,16 @@ const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
|||||||
export default function SystemDetail({ name }: { name: string }) {
|
export default function SystemDetail({ name }: { name: string }) {
|
||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
|
/** Max CPU toggle value */
|
||||||
|
const cpuMaxStore = useState(false)
|
||||||
|
const bandwidthMaxStore = useState(false)
|
||||||
|
const diskIoMaxStore = useState(false)
|
||||||
const [grid, setGrid] = useLocalStorage('grid', true)
|
const [grid, setGrid] = useLocalStorage('grid', true)
|
||||||
const [ticks, setTicks] = useState([] as number[])
|
const [ticks, setTicks] = useState([] as number[])
|
||||||
const [system, setSystem] = useState({} as SystemRecord)
|
const [system, setSystem] = useState({} as SystemRecord)
|
||||||
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
||||||
const netCardRef = useRef<HTMLDivElement>(null)
|
const netCardRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [containerFilterBar, setContainerFilterBar] = useState(null as null | JSX.Element)
|
||||||
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>(
|
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -43,15 +47,18 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
const [dockerNetChartData, setDockerNetChartData] = useState<Record<string, number | number[]>[]>(
|
const [dockerNetChartData, setDockerNetChartData] = useState<Record<string, number | number[]>[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const hasDockerStats = dockerCpuChartData.length > 0
|
const isLongerChart = chartTime !== '1h'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${name} / Beszel`
|
document.title = `${name} / Beszel`
|
||||||
return () => {
|
return () => {
|
||||||
resetCharts()
|
resetCharts()
|
||||||
$chartTime.set($userSettings.get().chartTime)
|
$chartTime.set($userSettings.get().chartTime)
|
||||||
|
setContainerFilterBar(null)
|
||||||
$containerFilter.set('')
|
$containerFilter.set('')
|
||||||
// setHasDocker(false)
|
cpuMaxStore[1](false)
|
||||||
|
bandwidthMaxStore[1](false)
|
||||||
|
diskIoMaxStore[1](false)
|
||||||
}
|
}
|
||||||
}, [name])
|
}, [name])
|
||||||
|
|
||||||
@@ -133,12 +140,15 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
getStats<ContainerStatsRecord>('container_stats'),
|
getStats<ContainerStatsRecord>('container_stats'),
|
||||||
]).then(([systemStats, containerStats]) => {
|
]).then(([systemStats, containerStats]) => {
|
||||||
const expectedInterval = chartTimeData[chartTime].expectedInterval
|
const expectedInterval = chartTimeData[chartTime].expectedInterval
|
||||||
if (containerStats.status === 'fulfilled' && containerStats.value.length) {
|
|
||||||
makeContainerData(addEmptyValues(containerStats.value, expectedInterval))
|
|
||||||
}
|
|
||||||
if (systemStats.status === 'fulfilled') {
|
if (systemStats.status === 'fulfilled') {
|
||||||
setSystemStats(addEmptyValues(systemStats.value, expectedInterval))
|
setSystemStats(addEmptyValues(systemStats.value, expectedInterval))
|
||||||
}
|
}
|
||||||
|
if (containerStats.status === 'fulfilled' && containerStats.value.length) {
|
||||||
|
!containerFilterBar && setContainerFilterBar(<ContainerFilterBar />)
|
||||||
|
makeContainerData(addEmptyValues(containerStats.value, expectedInterval))
|
||||||
|
} else {
|
||||||
|
setContainerFilterBar(null)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}, [system, chartTime])
|
}, [system, chartTime])
|
||||||
|
|
||||||
@@ -149,7 +159,10 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const startTime = chartTimeData[chartTime].getOffset(now)
|
const startTime = chartTimeData[chartTime].getOffset(now)
|
||||||
const scale = scaleTime([startTime.getTime(), now], [0, systemStats.length])
|
const scale = scaleTime([startTime.getTime(), now], [0, systemStats.length])
|
||||||
setTicks(scale.ticks(chartTimeData[chartTime].ticks).map((d) => d.getTime()))
|
const newTicks = scale.ticks(chartTimeData[chartTime].ticks).map((d) => d.getTime())
|
||||||
|
if (newTicks[0] !== ticks[0]) {
|
||||||
|
setTicks(newTicks)
|
||||||
|
}
|
||||||
}, [chartTime, systemStats])
|
}, [chartTime, systemStats])
|
||||||
|
|
||||||
// make container stats for charts
|
// make container stats for charts
|
||||||
@@ -192,7 +205,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
let uptime: number | string = system.info.u
|
let uptime: number | string = system.info.u
|
||||||
if (system.info.u < 172800) {
|
if (system.info.u < 172800) {
|
||||||
const hours = Math.trunc(uptime / 3600)
|
const hours = Math.trunc(uptime / 3600)
|
||||||
uptime = `${hours} hour${hours > 1 ? 's' : ''}`
|
uptime = `${hours} hour${hours == 1 ? '' : 's'}`
|
||||||
} else {
|
} else {
|
||||||
uptime = `${Math.trunc(system.info?.u / 86400)} days`
|
uptime = `${Math.trunc(system.info?.u / 86400)} days`
|
||||||
}
|
}
|
||||||
@@ -239,7 +252,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="chartwrap" className="grid gap-4 mb-10">
|
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
|
||||||
{/* system info */}
|
{/* system info */}
|
||||||
<Card>
|
<Card>
|
||||||
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
||||||
@@ -324,17 +337,27 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<ChartCard
|
<ChartCard
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title="Total CPU Usage"
|
title="Total CPU Usage"
|
||||||
description="Average system-wide CPU utilization"
|
description={`${
|
||||||
|
cpuMaxStore[0] && isLongerChart ? 'Max 1 min ' : 'Average'
|
||||||
|
} system-wide CPU utilization`}
|
||||||
|
cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null}
|
||||||
>
|
>
|
||||||
<CpuChart ticks={ticks} systemData={systemStats} />
|
<AreaChartDefault
|
||||||
|
ticks={ticks}
|
||||||
|
systemData={systemStats}
|
||||||
|
chartName="CPU Usage"
|
||||||
|
showMax={isLongerChart && cpuMaxStore[0]}
|
||||||
|
unit="%"
|
||||||
|
chartTime={chartTime}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && (
|
{containerFilterBar && (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title="Docker CPU Usage"
|
title="Docker CPU Usage"
|
||||||
description="CPU utilization of docker containers"
|
description="Average CPU utilization of containers"
|
||||||
isContainerChart={true}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
@@ -348,12 +371,12 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<MemChart ticks={ticks} systemData={systemStats} />
|
<MemChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && (
|
{containerFilterBar && (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title="Docker Memory Usage"
|
title="Docker Memory Usage"
|
||||||
description="Memory usage of docker containers"
|
description="Memory usage of docker containers"
|
||||||
isContainerChart={true}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
@@ -368,23 +391,37 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
<ChartCard grid={grid} title="Disk I/O" description="Throughput of root filesystem">
|
<ChartCard
|
||||||
<DiskIoChart
|
grid={grid}
|
||||||
|
title="Disk I/O"
|
||||||
|
description="Throughput of root filesystem"
|
||||||
|
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
|
||||||
|
>
|
||||||
|
<AreaChartDefault
|
||||||
ticks={ticks}
|
ticks={ticks}
|
||||||
systemData={systemStats}
|
systemData={systemStats}
|
||||||
dataKeys={['stats.dw', 'stats.dr']}
|
showMax={isLongerChart && diskIoMaxStore[0]}
|
||||||
|
chartName="dio"
|
||||||
|
chartTime={chartTime}
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
<ChartCard
|
<ChartCard
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title="Bandwidth"
|
title="Bandwidth"
|
||||||
|
cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null}
|
||||||
description="Network traffic of public interfaces"
|
description="Network traffic of public interfaces"
|
||||||
>
|
>
|
||||||
<BandwidthChart ticks={ticks} systemData={systemStats} />
|
<AreaChartDefault
|
||||||
|
ticks={ticks}
|
||||||
|
systemData={systemStats}
|
||||||
|
showMax={isLongerChart && bandwidthMaxStore[0]}
|
||||||
|
chartName="bw"
|
||||||
|
chartTime={chartTime}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && dockerNetChartData.length > 0 && (
|
{containerFilterBar && dockerNetChartData.length > 0 && (
|
||||||
<div
|
<div
|
||||||
ref={netCardRef}
|
ref={netCardRef}
|
||||||
className={cn({
|
className={cn({
|
||||||
@@ -394,7 +431,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<ChartCard
|
<ChartCard
|
||||||
title="Docker Network I/O"
|
title="Docker Network I/O"
|
||||||
description="Includes traffic between internal services"
|
description="Includes traffic between internal services"
|
||||||
isContainerChart={true}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
|
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
@@ -436,11 +473,14 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
grid={grid}
|
grid={grid}
|
||||||
title={`${extraFsName} I/O`}
|
title={`${extraFsName} I/O`}
|
||||||
description={`Throughput of ${extraFsName}`}
|
description={`Throughput of ${extraFsName}`}
|
||||||
|
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
|
||||||
>
|
>
|
||||||
<DiskIoChart
|
<AreaChartDefault
|
||||||
ticks={ticks}
|
ticks={ticks}
|
||||||
systemData={systemStats}
|
systemData={systemStats}
|
||||||
dataKeys={[`stats.efs.${extraFsName}.w`, `stats.efs.${extraFsName}.r`]}
|
showMax={isLongerChart && diskIoMaxStore[0]}
|
||||||
|
chartName={`efs.${extraFsName}`}
|
||||||
|
chartTime={chartTime}
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -461,10 +501,10 @@ function ContainerFilterBar() {
|
|||||||
|
|
||||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
$containerFilter.set(e.target.value)
|
$containerFilter.set(e.target.value)
|
||||||
}, []) // Use an empty dependency array to prevent re-creation
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
|
<>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter..."
|
placeholder="Filter..."
|
||||||
className="pl-4 pr-8"
|
className="pl-4 pr-8"
|
||||||
@@ -483,7 +523,33 @@ function ContainerFilterBar() {
|
|||||||
<XIcon className="h-4 w-4" />
|
<XIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectAvgMax({
|
||||||
|
store,
|
||||||
|
}: {
|
||||||
|
store: [boolean, React.Dispatch<React.SetStateAction<boolean>>]
|
||||||
|
}) {
|
||||||
|
const [max, setMax] = store
|
||||||
|
const Icon = max ? ChartMax : ChartAverage
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select value={max ? 'max' : 'avg'} onValueChange={(e) => setMax(e === 'max')}>
|
||||||
|
<SelectTrigger className="relative pl-10 pr-5">
|
||||||
|
<Icon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" />
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem key="avg" value="avg">
|
||||||
|
Average
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem key="max" value="max">
|
||||||
|
Max 1 min
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,13 +558,13 @@ function ChartCard({
|
|||||||
description,
|
description,
|
||||||
children,
|
children,
|
||||||
grid,
|
grid,
|
||||||
isContainerChart,
|
cornerEl,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
grid?: boolean
|
grid?: boolean
|
||||||
isContainerChart?: boolean
|
cornerEl?: JSX.Element | null
|
||||||
}) {
|
}) {
|
||||||
const { isIntersecting, ref } = useIntersectionObserver()
|
const { isIntersecting, ref } = useIntersectionObserver()
|
||||||
|
|
||||||
@@ -510,12 +576,16 @@ function ChartCard({
|
|||||||
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
|
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
|
||||||
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
||||||
<CardDescription>{description}</CardDescription>
|
<CardDescription>{description}</CardDescription>
|
||||||
{isContainerChart && <ContainerFilterBar />}
|
{cornerEl && (
|
||||||
|
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
|
||||||
|
{cornerEl}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
<div className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
||||||
{<Spinner />}
|
{<Spinner />}
|
||||||
{isIntersecting && <Suspense>{children}</Suspense>}
|
{isIntersecting && <Suspense>{children}</Suspense>}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -16,77 +16,77 @@ export type ChartConfig = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartContextProps = {
|
// type ChartContextProps = {
|
||||||
config: ChartConfig
|
// config: ChartConfig
|
||||||
}
|
// }
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
// const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||||
|
|
||||||
function useChart() {
|
// 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
|
||||||
}
|
// }
|
||||||
|
|
||||||
const ChartContainer = React.forwardRef<
|
const ChartContainer = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<'div'> & {
|
React.ComponentProps<'div'> & {
|
||||||
config: ChartConfig
|
// config: ChartConfig
|
||||||
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children']
|
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children']
|
||||||
}
|
}
|
||||||
>(({ id, className, children, config, ...props }, ref) => {
|
>(({ id, className, children, ...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 }}>
|
||||||
<div
|
<div
|
||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
"text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChartStyle id={chartId} config={config} />
|
{/* <ChartStyle id={chartId} config={config} /> */}
|
||||||
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
<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(([_, config]) => config.theme || config.color)
|
// const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
// if (!colorConfig.length) {
|
||||||
return null
|
// return null
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<style
|
// <style
|
||||||
dangerouslySetInnerHTML={{
|
// dangerouslySetInnerHTML={{
|
||||||
__html: Object.entries(THEMES).map(
|
// __html: Object.entries(THEMES).map(
|
||||||
([theme, prefix]) => `
|
// ([theme, prefix]) => `
|
||||||
${prefix} [data-chart=${id}] {
|
// ${prefix} [data-chart=${id}] {
|
||||||
${colorConfig
|
// ${colorConfig
|
||||||
.map(([key, itemConfig]) => {
|
// .map(([key, itemConfig]) => {
|
||||||
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
|
// const 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')}
|
||||||
}
|
// }
|
||||||
`
|
// `
|
||||||
),
|
// ),
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||||
|
|
||||||
@@ -126,7 +126,8 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { config } = useChart()
|
// const { config } = useChart()
|
||||||
|
const config = {}
|
||||||
|
|
||||||
React.useMemo(() => {
|
React.useMemo(() => {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
@@ -146,10 +147,7 @@ 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' ? label : itemConfig?.label
|
||||||
!labelKey && typeof label === 'string'
|
|
||||||
? config[label as keyof typeof config]?.label || label
|
|
||||||
: itemConfig?.label
|
|
||||||
|
|
||||||
if (labelFormatter) {
|
if (labelFormatter) {
|
||||||
return (
|
return (
|
||||||
@@ -262,7 +260,7 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
hideIcon?: boolean
|
hideIcon?: boolean
|
||||||
nameKey?: string
|
nameKey?: string
|
||||||
}
|
}
|
||||||
>(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
|
>(({ className, payload, verticalAlign = 'bottom' }, ref) => {
|
||||||
// const { config } = useChart()
|
// const { config } = useChart()
|
||||||
|
|
||||||
if (!payload?.length) {
|
if (!payload?.length) {
|
||||||
@@ -342,5 +340,5 @@ export {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
ChartLegend,
|
ChartLegend,
|
||||||
ChartLegendContent,
|
ChartLegendContent,
|
||||||
ChartStyle,
|
// ChartStyle,
|
||||||
}
|
}
|
||||||
|
@@ -23,3 +23,25 @@ export function Rows(props: SVGProps<SVGSVGElement>) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IconPark Apache License 2.0 https://github.com/bytedance/IconPark
|
||||||
|
export function ChartAverage(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" stroke="currentColor" {...props}>
|
||||||
|
<path strokeWidth="3" d="M4 4v40h40" />
|
||||||
|
<path strokeWidth="3" d="M10 38S15.3 4 27 4s17 34 17 34" />
|
||||||
|
<path strokeWidth="4" d="M10 24h34" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IconPark Apache License 2.0 https://github.com/bytedance/IconPark
|
||||||
|
export function ChartMax(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" stroke="currentColor" {...props}>
|
||||||
|
<path strokeWidth="3" d="M4 4v40h40" />
|
||||||
|
<path strokeWidth="3" d="M10 38S15.3 4 27 4s17 34 17 34" />
|
||||||
|
<path strokeWidth="4" d="M10 4h34" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -204,7 +204,10 @@ export function useYAxisWidth() {
|
|||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
document.body.appendChild(div)
|
document.body.appendChild(div)
|
||||||
setYAxisWidth(div.offsetWidth + 24)
|
const width = div.offsetWidth + 24
|
||||||
|
if (width > yAxisWidth) {
|
||||||
|
setYAxisWidth(div.offsetWidth + 24)
|
||||||
|
}
|
||||||
document.body.removeChild(div)
|
document.body.removeChild(div)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -285,3 +288,5 @@ export const getSizeUnit = (n: number) => (n >= 1_000 ? ' TB' : ' GB')
|
|||||||
* @returns value in GB if less than 1000, otherwise value in TB
|
* @returns value in GB if less than 1000, otherwise value in TB
|
||||||
*/
|
*/
|
||||||
export const getSizeVal = (n: number) => (n >= 1_000 ? n / 1_000 : n)
|
export const getSizeVal = (n: number) => (n >= 1_000 ? n / 1_000 : n)
|
||||||
|
|
||||||
|
export const chartMargin = { top: 12 }
|
||||||
|
18
beszel/site/src/types.d.ts
vendored
18
beszel/site/src/types.d.ts
vendored
@@ -36,7 +36,7 @@ export interface SystemStats {
|
|||||||
/** cpu percent */
|
/** cpu percent */
|
||||||
cpu: number
|
cpu: number
|
||||||
/** peak cpu */
|
/** peak cpu */
|
||||||
pcpu?: number
|
cpum?: number
|
||||||
/** total memory (gb) */
|
/** total memory (gb) */
|
||||||
m: number
|
m: number
|
||||||
/** memory used (gb) */
|
/** memory used (gb) */
|
||||||
@@ -61,14 +61,18 @@ export interface SystemStats {
|
|||||||
dr: number
|
dr: number
|
||||||
/** disk write (mb) */
|
/** disk write (mb) */
|
||||||
dw: number
|
dw: number
|
||||||
|
/** max disk read (mb) */
|
||||||
|
drm?: number
|
||||||
|
/** max disk write (mb) */
|
||||||
|
dwm?: number
|
||||||
/** network sent (mb) */
|
/** network sent (mb) */
|
||||||
ns: number
|
ns: number
|
||||||
/** peak network sent (mb) */
|
|
||||||
pns?: number
|
|
||||||
/** network received (mb) */
|
/** network received (mb) */
|
||||||
nr: number
|
nr: number
|
||||||
/** peak network received (mb) */
|
/** max network sent (mb) */
|
||||||
pnr?: number
|
nsm?: number
|
||||||
|
/** max network received (mb) */
|
||||||
|
nrm?: number
|
||||||
/** temperatures */
|
/** temperatures */
|
||||||
t?: Record<string, number>
|
t?: Record<string, number>
|
||||||
/** extra filesystems */
|
/** extra filesystems */
|
||||||
@@ -84,6 +88,10 @@ export interface ExtraFsStats {
|
|||||||
r: number
|
r: number
|
||||||
/** total write (mb) */
|
/** total write (mb) */
|
||||||
w: number
|
w: number
|
||||||
|
/** max read (mb) */
|
||||||
|
rm: number
|
||||||
|
/** max write (mb) */
|
||||||
|
wm: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerStatsRecord extends RecordModel {
|
export interface ContainerStatsRecord extends RecordModel {
|
||||||
|
Reference in New Issue
Block a user