Compare commits

...

9 Commits

Author SHA1 Message Date
Henry Dollman
23ab1208cd release 0.5.3 2024-10-10 18:36:28 -04:00
Henry Dollman
5b0fac429b move update functions to agent / hub packages 2024-10-10 18:36:01 -04:00
Henry Dollman
efca56ceca add temp debug logs to troubleshoot #196 2024-10-10 18:28:24 -04:00
Henry Dollman
64f0a23969 move fsStats creation to NewAgent function 2024-10-10 18:18:57 -04:00
Henry Dollman
4245da7792 Add caching to the web app to reduce requests for the same data 2024-10-10 18:11:36 -04:00
Henry Dollman
cedf80a869 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
2024-10-10 15:11:48 -04:00
Henry Dollman
76cea9d3c3 increase docker client timeout to 2100ms 2024-10-08 19:17:03 -04:00
Henry Dollman
10ef430826 small refactor in CreateLongerRecords 2024-10-08 12:42:02 -04:00
Henry Dollman
d672017af0 add peak values for cpu and net 2024-10-07 19:20:53 -04:00
27 changed files with 547 additions and 474 deletions

View File

@@ -3,7 +3,6 @@ package main
import (
"beszel"
"beszel/internal/agent"
"beszel/internal/update"
"fmt"
"log"
"os"
@@ -17,7 +16,7 @@ func main() {
case "-v":
fmt.Println(beszel.AppName+"-agent", beszel.Version)
case "update":
update.UpdateBeszelAgent()
agent.Update()
}
os.Exit(0)
}

View File

@@ -3,7 +3,6 @@ package main
import (
"beszel"
"beszel/internal/hub"
"beszel/internal/update"
_ "beszel/migrations"
"github.com/pocketbase/pocketbase"
@@ -22,7 +21,7 @@ func main() {
app.RootCmd.AddCommand(&cobra.Command{
Use: "update",
Short: "Update " + beszel.AppName + " to the latest version",
Run: func(_ *cobra.Command, _ []string) { update.UpdateBeszel() },
Run: hub.Update,
})
hub.NewHub(app).Run()

View File

@@ -29,6 +29,7 @@ func NewAgent() *Agent {
return &Agent{
sensorsContext: context.Background(),
memCalc: os.Getenv("MEM_CALC"),
fsStats: make(map[string]*system.FsStats),
}
}
@@ -77,6 +78,7 @@ func (a *Agent) Run(pubKey []byte, addr string) {
}
func (a *Agent) gatherStats() system.CombinedData {
slog.Debug("Getting stats")
systemData := system.CombinedData{
Stats: a.getSystemStats(),
Info: a.systemInfo,

View File

@@ -18,9 +18,6 @@ func (a *Agent) initializeDiskInfo() {
efPath := "/extra-filesystems"
hasRoot := false
// Create map for disk stats
a.fsStats = make(map[string]*system.FsStats)
partitions, err := disk.Partitions(false)
if err != nil {
slog.Error("Error getting disk partitions", "err", err)

View File

@@ -219,7 +219,7 @@ func newDockerManager() *dockerManager {
dockerClient := &dockerManager{
client: &http.Client{
Timeout: time.Millisecond * 1100,
Timeout: time.Millisecond * 2100,
Transport: transport,
},
containerStatsMap: make(map[string]*container.Stats),

View File

@@ -53,6 +53,7 @@ func (a *Agent) getSystemStats() system.Stats {
systemStats := system.Stats{}
// cpu percent
slog.Debug("Getting cpu percent")
cpuPct, err := cpu.Percent(0, false)
if err != nil {
slog.Error("Error getting cpu percent", "err", err)
@@ -61,6 +62,7 @@ func (a *Agent) getSystemStats() system.Stats {
}
// memory
slog.Debug("Getting memory stats")
if v, err := mem.VirtualMemory(); err == nil {
// swap
systemStats.Swap = bytesToGigabytes(v.SwapTotal)
@@ -89,6 +91,7 @@ func (a *Agent) getSystemStats() system.Stats {
}
// disk usage
slog.Debug("Getting disk stats")
for _, stats := range a.fsStats {
if d, err := disk.Usage(stats.Mountpoint); err == nil {
stats.DiskTotal = bytesToGigabytes(d.Total)
@@ -109,6 +112,7 @@ func (a *Agent) getSystemStats() system.Stats {
}
// disk i/o
slog.Debug("Getting disk I/O stats")
if ioCounters, err := disk.IOCounters(a.fsNames...); err == nil {
for _, d := range ioCounters {
stats := a.fsStats[d.Name]
@@ -132,6 +136,7 @@ func (a *Agent) getSystemStats() system.Stats {
}
// network stats
slog.Debug("Getting network stats")
if netIO, err := psutilNet.IOCounters(true); err == nil {
secondsElapsed := time.Since(a.netIoStats.Time).Seconds()
a.netIoStats.Time = time.Now()
@@ -172,6 +177,7 @@ func (a *Agent) getSystemStats() system.Stats {
}
// temperatures
slog.Debug("Getting temperatures")
temps, err := sensors.TemperaturesWithContext(a.sensorsContext)
if err != nil && a.debug {
err.(*sensors.Warnings).Verbose = true

View File

@@ -1,5 +1,4 @@
// Package update handles updating beszel and beszel-agent.
package update
package agent
import (
"beszel"
@@ -11,51 +10,8 @@ import (
"github.com/rhysd/go-github-selfupdate/selfupdate"
)
func UpdateBeszel() {
var latest *selfupdate.Release
var found bool
var err error
currentVersion := semver.MustParse(beszel.Version)
fmt.Println("beszel", currentVersion)
fmt.Println("Checking for updates...")
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
Filters: []string{"beszel_"},
})
latest, found, err = updater.DetectLatest("henrygd/beszel")
if err != nil {
fmt.Println("Error checking for updates:", err)
os.Exit(1)
}
if !found {
fmt.Println("No updates found")
os.Exit(0)
}
fmt.Println("Latest version:", latest.Version)
if latest.Version.LTE(currentVersion) {
fmt.Println("You are up to date")
return
}
var binaryPath string
fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
binaryPath, err = os.Executable()
if err != nil {
fmt.Println("Error getting binary path:", err)
os.Exit(1)
}
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
if err != nil {
fmt.Println("Please try rerunning with sudo. Error:", err)
os.Exit(1)
}
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
}
func UpdateBeszelAgent() {
// Update updates beszel-agent to the latest version
func Update() {
var latest *selfupdate.Release
var found bool
var err error

View File

@@ -6,35 +6,42 @@ import (
)
type Stats struct {
Cpu float64 `json:"cpu"`
Mem float64 `json:"m"`
MemUsed float64 `json:"mu"`
MemPct float64 `json:"mp"`
MemBuffCache float64 `json:"mb"`
MemZfsArc float64 `json:"mz,omitempty"` // ZFS ARC memory
Swap float64 `json:"s,omitempty"`
SwapUsed float64 `json:"su,omitempty"`
DiskTotal float64 `json:"d"`
DiskUsed float64 `json:"du"`
DiskPct float64 `json:"dp"`
DiskReadPs float64 `json:"dr"`
DiskWritePs float64 `json:"dw"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
Temperatures map[string]float64 `json:"t,omitempty"`
ExtraFs map[string]*FsStats `json:"efs,omitempty"`
Cpu float64 `json:"cpu"`
MaxCpu float64 `json:"cpum,omitempty"`
Mem float64 `json:"m"`
MemUsed float64 `json:"mu"`
MemPct float64 `json:"mp"`
MemBuffCache float64 `json:"mb"`
MemZfsArc float64 `json:"mz,omitempty"` // ZFS ARC memory
Swap float64 `json:"s,omitempty"`
SwapUsed float64 `json:"su,omitempty"`
DiskTotal float64 `json:"d"`
DiskUsed float64 `json:"du"`
DiskPct float64 `json:"dp"`
DiskReadPs float64 `json:"dr"`
DiskWritePs float64 `json:"dw"`
MaxDiskReadPs float64 `json:"drm,omitempty"`
MaxDiskWritePs float64 `json:"dwm,omitempty"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
MaxNetworkSent float64 `json:"nsm,omitempty"`
MaxNetworkRecv float64 `json:"nrm,omitempty"`
Temperatures map[string]float64 `json:"t,omitempty"`
ExtraFs map[string]*FsStats `json:"efs,omitempty"`
}
type FsStats struct {
Time time.Time `json:"-"`
Root bool `json:"-"`
Mountpoint string `json:"-"`
DiskTotal float64 `json:"d"`
DiskUsed float64 `json:"du"`
TotalRead uint64 `json:"-"`
TotalWrite uint64 `json:"-"`
DiskWritePs float64 `json:"w"`
DiskReadPs float64 `json:"r"`
Time time.Time `json:"-"`
Root bool `json:"-"`
Mountpoint string `json:"-"`
DiskTotal float64 `json:"d"`
DiskUsed float64 `json:"du"`
TotalRead uint64 `json:"-"`
TotalWrite uint64 `json:"-"`
DiskReadPs float64 `json:"r"`
DiskWritePs float64 `json:"w"`
MaxDiskReadPS float64 `json:"rm,omitempty"`
MaxDiskWritePS float64 `json:"wm,omitempty"`
}
type NetIoStats struct {

View File

@@ -0,0 +1,57 @@
package hub
import (
"beszel"
"fmt"
"os"
"strings"
"github.com/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
"github.com/spf13/cobra"
)
// Update updates beszel to the latest version
func Update(_ *cobra.Command, _ []string) {
var latest *selfupdate.Release
var found bool
var err error
currentVersion := semver.MustParse(beszel.Version)
fmt.Println("beszel", currentVersion)
fmt.Println("Checking for updates...")
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
Filters: []string{"beszel_"},
})
latest, found, err = updater.DetectLatest("henrygd/beszel")
if err != nil {
fmt.Println("Error checking for updates:", err)
os.Exit(1)
}
if !found {
fmt.Println("No updates found")
os.Exit(0)
}
fmt.Println("Latest version:", latest.Version)
if latest.Version.LTE(currentVersion) {
fmt.Println("You are up to date")
return
}
var binaryPath string
fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
binaryPath, err = os.Executable()
if err != nil {
fmt.Println("Error getting binary path:", err)
os.Exit(1)
}
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
if err != nil {
fmt.Println("Please try rerunning with sudo. Error:", err)
os.Exit(1)
}
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
}

View File

@@ -118,17 +118,15 @@ func (rm *RecordManager) CreateLongerRecords() {
continue
}
// average the shorter records and create longer record
var stats interface{}
switch collection.Name {
case "system_stats":
stats = rm.AverageSystemStats(allShorterRecords)
case "container_stats":
stats = rm.AverageContainerStats(allShorterRecords)
}
longerRecord := models.NewRecord(collection)
longerRecord.Set("system", system.Id)
longerRecord.Set("stats", stats)
longerRecord.Set("type", recordData.longerType)
switch collection.Name {
case "system_stats":
longerRecord.Set("stats", rm.AverageSystemStats(allShorterRecords))
case "container_stats":
longerRecord.Set("stats", rm.AverageContainerStats(allShorterRecords))
}
if err := txDao.SaveRecord(longerRecord); err != nil {
log.Println("failed to save longer record", "err", err.Error())
}
@@ -171,6 +169,12 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
sum.DiskWritePs += stats.DiskWritePs
sum.NetworkSent += stats.NetworkSent
sum.NetworkRecv += stats.NetworkRecv
// set peak values
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
sum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent)
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
if stats.Temperatures != nil {
tempCount++
@@ -191,26 +195,34 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
sum.ExtraFs[key].DiskUsed += value.DiskUsed
sum.ExtraFs[key].DiskWritePs += value.DiskWritePs
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{
Cpu: twoDecimals(sum.Cpu / count),
Mem: twoDecimals(sum.Mem / count),
MemUsed: twoDecimals(sum.MemUsed / count),
MemPct: twoDecimals(sum.MemPct / count),
MemBuffCache: twoDecimals(sum.MemBuffCache / count),
MemZfsArc: twoDecimals(sum.MemZfsArc / count),
Swap: twoDecimals(sum.Swap / count),
SwapUsed: twoDecimals(sum.SwapUsed / count),
DiskTotal: twoDecimals(sum.DiskTotal / count),
DiskUsed: twoDecimals(sum.DiskUsed / count),
DiskPct: twoDecimals(sum.DiskPct / count),
DiskReadPs: twoDecimals(sum.DiskReadPs / count),
DiskWritePs: twoDecimals(sum.DiskWritePs / count),
NetworkSent: twoDecimals(sum.NetworkSent / count),
NetworkRecv: twoDecimals(sum.NetworkRecv / count),
Cpu: twoDecimals(sum.Cpu / count),
Mem: twoDecimals(sum.Mem / count),
MemUsed: twoDecimals(sum.MemUsed / count),
MemPct: twoDecimals(sum.MemPct / count),
MemBuffCache: twoDecimals(sum.MemBuffCache / count),
MemZfsArc: twoDecimals(sum.MemZfsArc / count),
Swap: twoDecimals(sum.Swap / count),
SwapUsed: twoDecimals(sum.SwapUsed / count),
DiskTotal: twoDecimals(sum.DiskTotal / count),
DiskUsed: twoDecimals(sum.DiskUsed / count),
DiskPct: twoDecimals(sum.DiskPct / count),
DiskReadPs: twoDecimals(sum.DiskReadPs / count),
DiskWritePs: twoDecimals(sum.DiskWritePs / count),
NetworkSent: twoDecimals(sum.NetworkSent / count),
NetworkRecv: twoDecimals(sum.NetworkRecv / count),
MaxCpu: sum.MaxCpu,
MaxDiskReadPs: sum.MaxDiskReadPs,
MaxDiskWritePs: sum.MaxDiskWritePs,
MaxNetworkSent: sum.MaxNetworkSent,
MaxNetworkRecv: sum.MaxNetworkRecv,
}
if len(sum.Temperatures) != 0 {
@@ -224,10 +236,12 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
stats.ExtraFs = make(map[string]*system.FsStats)
for key, value := range sum.ExtraFs {
stats.ExtraFs[key] = &system.FsStats{
DiskTotal: twoDecimals(value.DiskTotal / count),
DiskUsed: twoDecimals(value.DiskUsed / count),
DiskWritePs: twoDecimals(value.DiskWritePs / count),
DiskReadPs: twoDecimals(value.DiskReadPs / count),
DiskTotal: twoDecimals(value.DiskTotal / count),
DiskUsed: twoDecimals(value.DiskUsed / count),
DiskWritePs: twoDecimals(value.DiskWritePs / count),
DiskReadPs: twoDecimals(value.DiskReadPs / count),
MaxDiskReadPS: value.MaxDiskReadPS,
MaxDiskWritePS: value.MaxDiskWritePS,
}
}
}

View 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>
)
}

View File

@@ -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>
)
}

View File

@@ -6,7 +6,14 @@ import {
ChartTooltipContent,
} from '@/components/ui/chart'
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 { useStore } from '@nanostores/react'
import { $chartTime, $containerFilter } from '@/lib/stores'
@@ -65,7 +72,6 @@ export default function ContainerCpuChart({
<div>
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisWidth,
})}
@@ -74,9 +80,7 @@ export default function ContainerCpuChart({
accessibilityLayer
// syncId={'cpu'}
data={chartData}
margin={{
top: 10,
}}
margin={chartMargin}
reverseStackOrder={true}
>
<CartesianGrid vertical={false} />

View File

@@ -13,6 +13,7 @@ import {
formatShortDate,
toFixedWithoutTrailingZeros,
twoDecimalString,
chartMargin,
} from '@/lib/utils'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
@@ -72,7 +73,6 @@ export default function ContainerMemChart({
<div>
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisWidth,
})}
@@ -81,9 +81,7 @@ export default function ContainerMemChart({
accessibilityLayer
data={chartData}
reverseStackOrder={true}
margin={{
top: 10,
}}
margin={chartMargin}
>
<CartesianGrid vertical={false} />
<YAxis

View File

@@ -13,6 +13,7 @@ import {
formatShortDate,
toFixedWithoutTrailingZeros,
twoDecimalString,
chartMargin,
} from '@/lib/utils'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
@@ -64,15 +65,10 @@ export default function ContainerCpuChart({
return config satisfies ChartConfig
}, [chartData])
// if (!chartData.length || !ticks.length) {
// return <Spinner />
// }
return (
<div>
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisWidth,
})}
@@ -80,9 +76,7 @@ export default function ContainerCpuChart({
<AreaChart
accessibilityLayer
data={chartData}
margin={{
top: 10,
}}
margin={chartMargin}
reverseStackOrder={true}
>
<CartesianGrid vertical={false} />

View File

@@ -1,11 +1,19 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
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 { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
import { $chartTime, $cpuMax } from '@/lib/stores'
import { SystemStatsRecord } from '@/types'
import { useMemo } from 'react'
export default function CpuChart({
ticks,
@@ -16,11 +24,16 @@ export default function CpuChart({
}) {
const chartTime = useStore($chartTime)
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const showMax = useStore($cpuMax)
const dataKey = useMemo(
() => `stats.cpu${showMax && chartTime !== '1h' ? 'm' : ''}`,
[showMax, systemData]
)
return (
<div>
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisWidth,
})}
@@ -28,7 +41,7 @@ export default function CpuChart({
<AreaChart
accessibilityLayer
data={systemData}
margin={{ top: 10 }}
margin={chartMargin}
// syncId={'cpu'}
>
<CartesianGrid vertical={false} />
@@ -63,16 +76,13 @@ export default function CpuChart({
}
/>
<Area
dataKey="stats.cpu"
dataKey={dataKey}
name="CPU Usage"
type="monotoneX"
fill="hsl(var(--chart-1))"
fillOpacity={0.4}
stroke="hsl(var(--chart-1))"
isAnimationActive={false}
// animationEasing="ease-out"
// animationDuration={1200}
// animateNewValues={true}
/>
</AreaChart>
</ChartContainer>

View File

@@ -10,6 +10,7 @@ import {
toFixedFloat,
getSizeVal,
getSizeUnit,
chartMargin,
} from '@/lib/utils'
// import { useMemo } from 'react'
// import Spinner from '../spinner'
@@ -35,21 +36,11 @@ export default function DiskChart({
<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,
}}
>
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
className="tracking-tighter"

View File

@@ -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>
)
}

View File

@@ -8,6 +8,7 @@ import {
toFixedFloat,
twoDecimalString,
formatShortDate,
chartMargin,
} from '@/lib/utils'
import { useMemo } from 'react'
import { useStore } from '@nanostores/react'
@@ -32,18 +33,11 @@ export default function MemChart({
<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={{
top: 10,
}}
>
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
<CartesianGrid vertical={false} />
{totalMem && (
<YAxis

View File

@@ -8,8 +8,8 @@ import {
formatShortDate,
toFixedWithoutTrailingZeros,
twoDecimalString,
chartMargin,
} from '@/lib/utils'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types'
@@ -27,12 +27,11 @@ export default function SwapChart({
return (
<div>
<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={{ top: 10 }}>
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
className="tracking-tighter"

View File

@@ -14,6 +14,7 @@ import {
formatShortDate,
toFixedWithoutTrailingZeros,
twoDecimalString,
chartMargin,
} from '@/lib/utils'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
@@ -60,21 +61,11 @@ export default function TemperatureChart({
<div>
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisWidth,
})}
>
<LineChart
accessibilityLayer
data={newChartData.data}
margin={{
left: 0,
right: 0,
top: 10,
bottom: 0,
}}
>
<LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
className="tracking-tighter"

View File

@@ -1,7 +1,7 @@
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from '@/lib/stores'
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription } from '../ui/card'
import { useStore } from '@nanostores/react'
import Spinner from '../spinner'
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
@@ -12,28 +12,34 @@ import { scaleTime } from 'd3-scale'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { Button, buttonVariants } from '../ui/button'
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
const CpuChart = lazy(() => import('../charts/cpu-chart'))
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
const MemChart = lazy(() => import('../charts/mem-chart'))
const ContainerMemChart = lazy(() => import('../charts/container-mem-chart'))
const DiskChart = lazy(() => import('../charts/disk-chart'))
const DiskIoChart = lazy(() => import('../charts/disk-io-chart'))
const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
const AreaChartDefault = lazy(() => import('../charts/area-chart'))
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
const SwapChart = lazy(() => import('../charts/swap-chart'))
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
const cache = new Map<string, SystemStatsRecord[] | ContainerStatsRecord[]>()
export default function SystemDetail({ name }: { name: string }) {
const systems = useStore($systems)
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 [ticks, setTicks] = useState([] as number[])
const [system, setSystem] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const netCardRef = useRef<HTMLDivElement>(null)
const [containerFilterBar, setContainerFilterBar] = useState(null as null | JSX.Element)
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>(
[]
)
@@ -43,15 +49,18 @@ export default function SystemDetail({ name }: { name: string }) {
const [dockerNetChartData, setDockerNetChartData] = useState<Record<string, number | number[]>[]>(
[]
)
const hasDockerStats = dockerCpuChartData.length > 0
const isLongerChart = chartTime !== '1h'
useEffect(() => {
document.title = `${name} / Beszel`
return () => {
resetCharts()
$chartTime.set($userSettings.get().chartTime)
setContainerFilterBar(null)
$containerFilter.set('')
// setHasDocker(false)
cpuMaxStore[1](false)
bandwidthMaxStore[1](false)
diskIoMaxStore[1](false)
}
}, [name])
@@ -88,10 +97,12 @@ export default function SystemDetail({ name }: { name: string }) {
}, [system])
async function getStats<T>(collection: string): Promise<T[]> {
const lastCached = cache.get(`${system.id}_${chartTime}_${collection}`)?.at(-1)
?.created as number
return await pb.collection<T>(collection).getFullList({
filter: pb.filter('system={:id} && created > {:created} && type={:type}', {
id: system.id,
created: getPbTimestamp(chartTime),
created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined),
type: chartTimeData[chartTime].type,
}),
fields: 'created,stats',
@@ -132,13 +143,34 @@ export default function SystemDetail({ name }: { name: string }) {
getStats<SystemStatsRecord>('system_stats'),
getStats<ContainerStatsRecord>('container_stats'),
]).then(([systemStats, containerStats]) => {
const expectedInterval = chartTimeData[chartTime].expectedInterval
const { expectedInterval } = chartTimeData[chartTime]
// make new system stats
const ss_cache_key = `${system.id}_${chartTime}_system_stats`
let systemData = (cache.get(ss_cache_key) || []) as SystemStatsRecord[]
if (systemStats.status === 'fulfilled' && systemStats.value.length) {
systemData = systemData.concat(addEmptyValues(systemStats.value, expectedInterval))
if (systemData.length > 120) {
systemData = systemData.slice(-100)
}
cache.set(ss_cache_key, systemData)
}
setSystemStats(systemData)
// make new container stats
const cs_cache_key = `${system.id}_${chartTime}_container_stats`
let containerData = (cache.get(cs_cache_key) || []) as ContainerStatsRecord[]
if (containerStats.status === 'fulfilled' && containerStats.value.length) {
makeContainerData(addEmptyValues(containerStats.value, expectedInterval))
containerData = containerData.concat(addEmptyValues(containerStats.value, expectedInterval))
if (containerData.length > 120) {
containerData = containerData.slice(-100)
}
cache.set(cs_cache_key, containerData)
}
if (systemStats.status === 'fulfilled') {
setSystemStats(addEmptyValues(systemStats.value, expectedInterval))
if (containerData.length) {
!containerFilterBar && setContainerFilterBar(<ContainerFilterBar />)
} else if (containerFilterBar) {
setContainerFilterBar(null)
}
makeContainerData(containerData)
})
}, [system, chartTime])
@@ -149,7 +181,10 @@ export default function SystemDetail({ name }: { name: string }) {
const now = new Date()
const startTime = chartTimeData[chartTime].getOffset(now)
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])
// make container stats for charts
@@ -192,7 +227,7 @@ export default function SystemDetail({ name }: { name: string }) {
let uptime: number | string = system.info.u
if (system.info.u < 172800) {
const hours = Math.trunc(uptime / 3600)
uptime = `${hours} hour${hours > 1 ? 's' : ''}`
uptime = `${hours} hour${hours == 1 ? '' : 's'}`
} else {
uptime = `${Math.trunc(system.info?.u / 86400)} days`
}
@@ -239,7 +274,7 @@ export default function SystemDetail({ name }: { name: string }) {
return (
<>
<div id="chartwrap" className="grid gap-4 mb-10">
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
{/* system info */}
<Card>
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
@@ -324,17 +359,27 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
grid={grid}
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>
{hasDockerStats && (
{containerFilterBar && (
<ChartCard
grid={grid}
title="Docker CPU Usage"
description="CPU utilization of docker containers"
isContainerChart={true}
description="Average CPU utilization of containers"
cornerEl={containerFilterBar}
>
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
</ChartCard>
@@ -348,12 +393,12 @@ export default function SystemDetail({ name }: { name: string }) {
<MemChart ticks={ticks} systemData={systemStats} />
</ChartCard>
{hasDockerStats && (
{containerFilterBar && (
<ChartCard
grid={grid}
title="Docker Memory Usage"
description="Memory usage of docker containers"
isContainerChart={true}
cornerEl={containerFilterBar}
>
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
</ChartCard>
@@ -368,23 +413,37 @@ export default function SystemDetail({ name }: { name: string }) {
/>
</ChartCard>
<ChartCard grid={grid} title="Disk I/O" description="Throughput of root filesystem">
<DiskIoChart
<ChartCard
grid={grid}
title="Disk I/O"
description="Throughput of root filesystem"
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
>
<AreaChartDefault
ticks={ticks}
systemData={systemStats}
dataKeys={['stats.dw', 'stats.dr']}
showMax={isLongerChart && diskIoMaxStore[0]}
chartName="dio"
chartTime={chartTime}
/>
</ChartCard>
<ChartCard
grid={grid}
title="Bandwidth"
cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null}
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>
{hasDockerStats && dockerNetChartData.length > 0 && (
{containerFilterBar && dockerNetChartData.length > 0 && (
<div
ref={netCardRef}
className={cn({
@@ -394,7 +453,7 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
title="Docker Network I/O"
description="Includes traffic between internal services"
isContainerChart={true}
cornerEl={containerFilterBar}
>
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
</ChartCard>
@@ -436,11 +495,14 @@ export default function SystemDetail({ name }: { name: string }) {
grid={grid}
title={`${extraFsName} I/O`}
description={`Throughput of ${extraFsName}`}
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
>
<DiskIoChart
<AreaChartDefault
ticks={ticks}
systemData={systemStats}
dataKeys={[`stats.efs.${extraFsName}.w`, `stats.efs.${extraFsName}.r`]}
showMax={isLongerChart && diskIoMaxStore[0]}
chartName={`efs.${extraFsName}`}
chartTime={chartTime}
/>
</ChartCard>
</div>
@@ -461,10 +523,10 @@ function ContainerFilterBar() {
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
$containerFilter.set(e.target.value)
}, []) // Use an empty dependency array to prevent re-creation
}, [])
return (
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
<>
<Input
placeholder="Filter..."
className="pl-4 pr-8"
@@ -483,7 +545,33 @@ function ContainerFilterBar() {
<XIcon className="h-4 w-4" />
</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 +580,13 @@ function ChartCard({
description,
children,
grid,
isContainerChart,
cornerEl,
}: {
title: string
description: string
children: React.ReactNode
grid?: boolean
isContainerChart?: boolean
cornerEl?: JSX.Element | null
}) {
const { isIntersecting, ref } = useIntersectionObserver()
@@ -510,12 +598,16 @@ function ChartCard({
<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>
<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>
<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 />}
{isIntersecting && <Suspense>{children}</Suspense>}
</CardContent>
</div>
</Card>
)
}

View File

@@ -16,77 +16,77 @@ export type ChartConfig = {
)
}
type ChartContextProps = {
config: ChartConfig
}
// type ChartContextProps = {
// config: ChartConfig
// }
const ChartContext = React.createContext<ChartContextProps | null>(null)
// const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
// function useChart() {
// const context = React.useContext(ChartContext)
if (!context) {
throw new Error('useChart must be used within a <ChartContainer />')
}
// if (!context) {
// throw new Error('useChart must be used within a <ChartContainer />')
// }
return context
}
// return context
// }
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
config: ChartConfig
// config: ChartConfig
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children']
}
>(({ id, className, children, config, ...props }, ref) => {
>(({ id, className, children, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
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",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
//<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"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
)}
{...props}
>
{/* <ChartStyle id={chartId} config={config} /> */}
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
//</ChartContext.Provider>
)
})
ChartContainer.displayName = 'Chart'
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
// const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
// const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
if (!colorConfig.length) {
return null
}
// if (!colorConfig.length) {
// return null
// }
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES).map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join('\n')}
}
`
),
}}
/>
)
}
// return (
// <style
// dangerouslySetInnerHTML={{
// __html: Object.entries(THEMES).map(
// ([theme, prefix]) => `
// ${prefix} [data-chart=${id}] {
// ${colorConfig
// .map(([key, itemConfig]) => {
// const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
// return color ? ` --color-${key}: ${color};` : null
// })
// .join('\n')}
// }
// `
// ),
// }}
// />
// )
// }
const ChartTooltip = RechartsPrimitive.Tooltip
@@ -126,7 +126,8 @@ const ChartTooltipContent = React.forwardRef<
},
ref
) => {
const { config } = useChart()
// const { config } = useChart()
const config = {}
React.useMemo(() => {
if (filter) {
@@ -146,10 +147,7 @@ const ChartTooltipContent = React.forwardRef<
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || 'value'}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === 'string'
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
const value = !labelKey && typeof label === 'string' ? label : itemConfig?.label
if (labelFormatter) {
return (
@@ -262,7 +260,7 @@ const ChartLegendContent = React.forwardRef<
hideIcon?: boolean
nameKey?: string
}
>(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
>(({ className, payload, verticalAlign = 'bottom' }, ref) => {
// const { config } = useChart()
if (!payload?.length) {
@@ -342,5 +340,5 @@ export {
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
// ChartStyle,
}

View File

@@ -23,3 +23,25 @@ export function Rows(props: SVGProps<SVGSVGElement>) {
</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>
)
}

View File

@@ -136,8 +136,8 @@ export function updateRecordList<T extends RecordModel>(
$store.set(newRecords)
}
export function getPbTimestamp(timeString: ChartTimes) {
const d = chartTimeData[timeString].getOffset(new Date())
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
d ||= chartTimeData[timeString].getOffset(new Date())
const year = d.getUTCFullYear()
const month = String(d.getUTCMonth() + 1).padStart(2, '0')
const day = String(d.getUTCDate()).padStart(2, '0')
@@ -204,7 +204,10 @@ export function useYAxisWidth() {
clearTimeout(timeout)
timeout = setTimeout(() => {
document.body.appendChild(div)
setYAxisWidth(div.offsetWidth + 24)
const width = div.offsetWidth + 24
if (width > yAxisWidth) {
setYAxisWidth(div.offsetWidth + 24)
}
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
*/
export const getSizeVal = (n: number) => (n >= 1_000 ? n / 1_000 : n)
export const chartMargin = { top: 12 }

View File

@@ -35,6 +35,8 @@ export interface SystemInfo {
export interface SystemStats {
/** cpu percent */
cpu: number
/** peak cpu */
cpum?: number
/** total memory (gb) */
m: number
/** memory used (gb) */
@@ -59,10 +61,18 @@ export interface SystemStats {
dr: number
/** disk write (mb) */
dw: number
/** max disk read (mb) */
drm?: number
/** max disk write (mb) */
dwm?: number
/** network sent (mb) */
ns: number
/** network received (mb) */
nr: number
/** max network sent (mb) */
nsm?: number
/** max network received (mb) */
nrm?: number
/** temperatures */
t?: Record<string, number>
/** extra filesystems */
@@ -78,6 +88,10 @@ export interface ExtraFsStats {
r: number
/** total write (mb) */
w: number
/** max read (mb) */
rm: number
/** max write (mb) */
wm: number
}
export interface ContainerStatsRecord extends RecordModel {

View File

@@ -1,6 +1,6 @@
package beszel
const (
Version = "0.5.2"
Version = "0.5.3"
AppName = "beszel"
)