mirror of
https://github.com/fankes/beszel.git
synced 2025-10-20 02:09:28 +08:00
add temperature chart
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
|
||||
sshServer "github.com/gliderlabs/ssh"
|
||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||
@@ -140,6 +142,21 @@ func (a *Agent) getSystemStats() (*system.Info, *system.Stats) {
|
||||
a.netIoStats.Time = time.Now()
|
||||
}
|
||||
|
||||
// temperatures
|
||||
if temps, err := sensors.SensorsTemperatures(); err == nil {
|
||||
systemStats.Temperatures = make(map[string]float64)
|
||||
// log.Printf("Temperatures: %+v\n", temps)
|
||||
for i, temp := range temps {
|
||||
if _, ok := systemStats.Temperatures[temp.SensorKey]; ok {
|
||||
// if key already exists, append int to key
|
||||
systemStats.Temperatures[temp.SensorKey+"_"+strconv.Itoa(i)] = twoDecimals(temp.Temperature)
|
||||
} else {
|
||||
systemStats.Temperatures[temp.SensorKey] = twoDecimals(temp.Temperature)
|
||||
}
|
||||
}
|
||||
// log.Printf("Temperature map: %+v\n", systemStats.Temperatures)
|
||||
}
|
||||
|
||||
systemInfo := &system.Info{
|
||||
Cpu: systemStats.Cpu,
|
||||
MemPct: systemStats.MemPct,
|
||||
|
@@ -20,6 +20,7 @@ type Stats struct {
|
||||
DiskWrite float64 `json:"dw"`
|
||||
NetworkSent float64 `json:"ns"`
|
||||
NetworkRecv float64 `json:"nr"`
|
||||
Temperatures map[string]float64 `json:"t,omitempty"`
|
||||
}
|
||||
|
||||
type DiskIoStats struct {
|
||||
|
@@ -166,7 +166,11 @@ func (rm *RecordManager) CreateLongerRecords() {
|
||||
// Calculate the average stats of a list of system_stats records without reflect
|
||||
func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Stats {
|
||||
var sum system.Stats
|
||||
sum.Temperatures = make(map[string]float64)
|
||||
|
||||
count := float64(len(records))
|
||||
// use different counter for temps in case some records don't have them
|
||||
tempCount := float64(0)
|
||||
|
||||
var stats system.Stats
|
||||
for _, record := range records {
|
||||
@@ -185,9 +189,18 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
||||
sum.DiskWrite += stats.DiskWrite
|
||||
sum.NetworkSent += stats.NetworkSent
|
||||
sum.NetworkRecv += stats.NetworkRecv
|
||||
if stats.Temperatures != nil {
|
||||
tempCount++
|
||||
for key, value := range stats.Temperatures {
|
||||
if _, ok := sum.Temperatures[key]; !ok {
|
||||
sum.Temperatures[key] = 0
|
||||
}
|
||||
sum.Temperatures[key] += value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return system.Stats{
|
||||
stats = system.Stats{
|
||||
Cpu: twoDecimals(sum.Cpu / count),
|
||||
Mem: twoDecimals(sum.Mem / count),
|
||||
MemUsed: twoDecimals(sum.MemUsed / count),
|
||||
@@ -203,6 +216,15 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
||||
NetworkSent: twoDecimals(sum.NetworkSent / count),
|
||||
NetworkRecv: twoDecimals(sum.NetworkRecv / count),
|
||||
}
|
||||
|
||||
if len(sum.Temperatures) != 0 {
|
||||
stats.Temperatures = make(map[string]float64)
|
||||
for key, value := range sum.Temperatures {
|
||||
stats.Temperatures[key] = twoDecimals(value / tempCount)
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// Calculate the average stats of a list of container_stats records
|
||||
|
@@ -62,7 +62,7 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
className={cn('flex gap-1 max-xs:h-[2.4rem]', className, isReadOnlyUser() && 'hidden')}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 -ml-1" />
|
||||
Add <span className="hidden sm:inline">System</span>
|
||||
Add <span className="hidden xs:inline">System</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-[90%] sm:max-w-[425px] rounded-lg">
|
||||
|
129
beszel/site/src/components/charts/temperature-chart.tsx
Normal file
129
beszel/site/src/components/charts/temperature-chart.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts'
|
||||
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from '@/components/ui/chart'
|
||||
import {
|
||||
chartTimeData,
|
||||
cn,
|
||||
formatShortDate,
|
||||
toFixedWithoutTrailingZeros,
|
||||
useYaxisWidth,
|
||||
} from '@/lib/utils'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { $chartTime } from '@/lib/stores'
|
||||
import { SystemStatsRecord } from '@/types'
|
||||
import { useMemo, useRef } from 'react'
|
||||
|
||||
export default function TemperatureChart({
|
||||
ticks,
|
||||
systemData,
|
||||
}: {
|
||||
ticks: number[]
|
||||
systemData: SystemStatsRecord[]
|
||||
}) {
|
||||
const chartRef = useRef<HTMLDivElement>(null)
|
||||
const yAxisWidth = useYaxisWidth(chartRef)
|
||||
const chartTime = useStore($chartTime)
|
||||
|
||||
/** Format temperature data for chart and assign colors */
|
||||
const newChartData = useMemo(() => {
|
||||
const chartData = { data: [], colors: {} } as {
|
||||
data: Record<string, number | string>[]
|
||||
colors: Record<string, string>
|
||||
}
|
||||
const tempSums = {} as Record<string, number>
|
||||
for (let data of systemData) {
|
||||
let newData = { created: data.created } as Record<string, number | string>
|
||||
let keys = Object.keys(data.stats?.t ?? {})
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let key = keys[i]
|
||||
newData[key] = data.stats.t![key]
|
||||
tempSums[key] = (tempSums[key] ?? 0) + newData[key]
|
||||
}
|
||||
chartData.data.push(newData)
|
||||
}
|
||||
const keys = Object.keys(tempSums).sort((a, b) => tempSums[b] - tempSums[a])
|
||||
for (let key of keys) {
|
||||
chartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
|
||||
}
|
||||
return chartData
|
||||
}, [systemData])
|
||||
|
||||
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||
|
||||
return (
|
||||
<div ref={chartRef}>
|
||||
{/* {!yAxisSet && <Spinner />} */}
|
||||
<ChartContainer
|
||||
config={{}}
|
||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||
'opacity-100': yAxisSet,
|
||||
})}
|
||||
>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={newChartData.data}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 10,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis
|
||||
className="tracking-tighter"
|
||||
width={yAxisWidth}
|
||||
tickFormatter={(value) => toFixedWithoutTrailingZeros(value, 2)}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
unit={' °C'}
|
||||
/>
|
||||
<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}
|
||||
// @ts-ignore
|
||||
itemSorter={(a, b) => b.value - a.value}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
unit=" °C"
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
indicator="line"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{Object.keys(newChartData.colors).map((key) => (
|
||||
<Line
|
||||
key={key}
|
||||
dataKey={key}
|
||||
name={key}
|
||||
type="monotoneX"
|
||||
dot={false}
|
||||
strokeWidth={1.5}
|
||||
fill="hsl(360, 60%, 55%)"
|
||||
stroke={newChartData.colors[key]}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
))}
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -20,6 +20,7 @@ const DiskIoChart = lazy(() => import('../charts/disk-io-chart'))
|
||||
const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
|
||||
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
|
||||
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
||||
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
||||
|
||||
export default function SystemDetail({ name }: { name: string }) {
|
||||
const systems = useStore($systems)
|
||||
@@ -271,6 +272,12 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
{systemStats.at(-1)?.stats.t && (
|
||||
<ChartCard title="Temperature" description="Temperature of system components">
|
||||
<TemperatureChart ticks={ticks} systemData={systemStats} />
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
<ChartCard
|
||||
title="Disk Usage"
|
||||
description="Usage of partition where the root filesystem is mounted"
|
||||
|
@@ -21,7 +21,6 @@ import {
|
||||
} from '@/components/ui/table'
|
||||
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
@@ -258,7 +258,7 @@ const ChartLegendContent = React.forwardRef<
|
||||
nameKey?: string
|
||||
}
|
||||
>(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
|
||||
const { config } = useChart()
|
||||
// const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
@@ -268,33 +268,35 @@ const ChartLegendContent = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-4',
|
||||
'flex items-center justify-center gap-4 gap-y-1 flex-wrap',
|
||||
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || 'value'}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
// const key = `${nameKey || item.dataKey || 'value'}`
|
||||
// const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
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 text-muted-foreground'
|
||||
'flex items-center gap-1.5 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
{/* {itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
) : ( */}
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
{item.value}
|
||||
{/* )} */}
|
||||
{/* {itemConfig?.label} */}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
2
beszel/site/src/types.d.ts
vendored
2
beszel/site/src/types.d.ts
vendored
@@ -57,6 +57,8 @@ export interface SystemStats {
|
||||
ns: number
|
||||
/** network received (mb) */
|
||||
nr: number
|
||||
/** temperatures */
|
||||
t?: Record<string, number>
|
||||
}
|
||||
|
||||
export interface ContainerStatsRecord extends RecordModel {
|
||||
|
Reference in New Issue
Block a user