Compare commits

..

1 Commits

Author SHA1 Message Date
Henry Dollman
8de2dee4e9 built-in agent 2024-10-07 18:58:57 -04:00
30 changed files with 553 additions and 579 deletions

View File

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

View File

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

View File

@@ -29,7 +29,6 @@ func NewAgent() *Agent {
return &Agent{ return &Agent{
sensorsContext: context.Background(), sensorsContext: context.Background(),
memCalc: os.Getenv("MEM_CALC"), memCalc: os.Getenv("MEM_CALC"),
fsStats: make(map[string]*system.FsStats),
} }
} }
@@ -71,14 +70,15 @@ func (a *Agent) Run(pubKey []byte, addr string) {
// if debugging, print stats // if debugging, print stats
if a.debug { if a.debug {
slog.Debug("Stats", "data", a.gatherStats()) slog.Debug("Stats", "data", a.GatherStats())
} }
a.startServer(pubKey, addr) if pubKey != nil {
a.startServer(pubKey, addr)
}
} }
func (a *Agent) gatherStats() system.CombinedData { func (a *Agent) GatherStats() system.CombinedData {
slog.Debug("Getting stats")
systemData := system.CombinedData{ systemData := system.CombinedData{
Stats: a.getSystemStats(), Stats: a.getSystemStats(),
Info: a.systemInfo, Info: a.systemInfo,

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ func (a *Agent) startServer(pubKey []byte, addr string) {
} }
func (a *Agent) handleSession(s sshServer.Session) { func (a *Agent) handleSession(s sshServer.Session) {
stats := a.gatherStats() stats := a.GatherStats()
slog.Debug("Sending stats", "data", stats) slog.Debug("Sending stats", "data", stats)
if err := json.NewEncoder(s).Encode(stats); err != nil { if err := json.NewEncoder(s).Encode(stats); err != nil {
slog.Error("Error encoding stats", "err", err) slog.Error("Error encoding stats", "err", err)

View File

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

View File

@@ -6,42 +6,35 @@ import (
) )
type Stats struct { type Stats struct {
Cpu float64 `json:"cpu"` Cpu float64 `json:"cpu"`
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"` NetworkRecv float64 `json:"nr"`
MaxDiskWritePs float64 `json:"dwm,omitempty"` Temperatures map[string]float64 `json:"t,omitempty"`
NetworkSent float64 `json:"ns"` ExtraFs map[string]*FsStats `json:"efs,omitempty"`
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 { 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:"-"`
DiskReadPs float64 `json:"r"` DiskWritePs float64 `json:"w"`
DiskWritePs float64 `json:"w"` DiskReadPs float64 `json:"r"`
MaxDiskReadPS float64 `json:"rm,omitempty"`
MaxDiskWritePS float64 `json:"wm,omitempty"`
} }
type NetIoStats struct { type NetIoStats struct {

View File

@@ -3,6 +3,7 @@ package hub
import ( import (
"beszel" "beszel"
"beszel/internal/agent"
"beszel/internal/alerts" "beszel/internal/alerts"
"beszel/internal/entities/system" "beszel/internal/entities/system"
"beszel/internal/records" "beszel/internal/records"
@@ -42,6 +43,7 @@ type Hub struct {
am *alerts.AlertManager am *alerts.AlertManager
um *users.UserManager um *users.UserManager
rm *records.RecordManager rm *records.RecordManager
hubAgent *agent.Agent
} }
func NewHub(app *pocketbase.PocketBase) *Hub { func NewHub(app *pocketbase.PocketBase) *Hub {
@@ -56,10 +58,6 @@ func NewHub(app *pocketbase.PocketBase) *Hub {
} }
func (h *Hub) Run() { func (h *Hub) Run() {
// rm := records.NewRecordManager(h.app)
// am := alerts.NewAlertManager(h.app)
// um := users.NewUserManager(h.app)
// loosely check if it was executed using "go run" // loosely check if it was executed using "go run"
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir()) isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
@@ -73,25 +71,22 @@ func (h *Hub) Run() {
// initial setup // initial setup
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error { h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// create ssh client config // create ssh client config
err := h.createSSHClientConfig() if err := h.createSSHClientConfig(); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// set auth settings // set auth settings
usersCollection, err := h.app.Dao().FindCollectionByNameOrId("users") if usersCollection, err := h.app.Dao().FindCollectionByNameOrId("users"); err == nil {
if err != nil { usersAuthOptions := usersCollection.AuthOptions()
return err usersAuthOptions.AllowUsernameAuth = false
} if os.Getenv("DISABLE_PASSWORD_AUTH") == "true" {
usersAuthOptions := usersCollection.AuthOptions() usersAuthOptions.AllowEmailAuth = false
usersAuthOptions.AllowUsernameAuth = false } else {
if os.Getenv("DISABLE_PASSWORD_AUTH") == "true" { usersAuthOptions.AllowEmailAuth = true
usersAuthOptions.AllowEmailAuth = false }
} else { usersCollection.SetOptions(usersAuthOptions)
usersAuthOptions.AllowEmailAuth = true if err := h.app.Dao().SaveCollection(usersCollection); err != nil {
} return err
usersCollection.SetOptions(usersAuthOptions) }
if err := h.app.Dao().SaveCollection(usersCollection); err != nil {
return err
} }
return nil return nil
}) })
@@ -159,6 +154,16 @@ func (h *Hub) Run() {
// system creation defaults // system creation defaults
h.app.OnModelBeforeCreate("systems").Add(func(e *core.ModelEvent) error { h.app.OnModelBeforeCreate("systems").Add(func(e *core.ModelEvent) error {
record := e.Model.(*models.Record) record := e.Model.(*models.Record)
if record.GetString("host") == "hubsys" {
// todo: check for hubsys existance and return error if exists (or make sure user is admin)
if record.GetString("name") == "x" {
hostname, _ := os.Hostname()
if hostname == "" {
hostname = "localhost"
}
record.Set("name", hostname)
}
}
record.Set("info", system.Info{}) record.Set("info", system.Info{})
record.Set("status", "pending") record.Set("status", "pending")
return nil return nil
@@ -246,6 +251,26 @@ func (h *Hub) updateSystems() {
} }
func (h *Hub) updateSystem(record *models.Record) { func (h *Hub) updateSystem(record *models.Record) {
switch record.GetString("host") {
case "hubsys":
h.updateHubSystem(record)
default:
h.updateRemoteSystem(record)
}
}
// Update hub system stats with built-in agent
func (h *Hub) updateHubSystem(record *models.Record) {
if h.hubAgent == nil {
h.hubAgent = agent.NewAgent()
h.hubAgent.Run(nil, "")
}
systemData := h.hubAgent.GatherStats()
h.saveSystemStats(record, &systemData)
}
// Connect to remote system and update system stats
func (h *Hub) updateRemoteSystem(record *models.Record) {
var client *ssh.Client var client *ssh.Client
var err error var err error
@@ -273,7 +298,7 @@ func (h *Hub) updateSystem(record *models.Record) {
// if previous connection was closed, try again // if previous connection was closed, try again
h.app.Logger().Error("Existing SSH connection closed. Retrying...", "host", record.GetString("host"), "port", record.GetString("port")) h.app.Logger().Error("Existing SSH connection closed. Retrying...", "host", record.GetString("host"), "port", record.GetString("port"))
h.deleteSystemConnection(record) h.deleteSystemConnection(record)
h.updateSystem(record) h.updateRemoteSystem(record)
return return
} }
h.app.Logger().Error("Failed to get system stats: ", "err", err.Error()) h.app.Logger().Error("Failed to get system stats: ", "err", err.Error())
@@ -281,6 +306,11 @@ func (h *Hub) updateSystem(record *models.Record) {
return return
} }
// update system record // update system record
h.saveSystemStats(record, &systemData)
}
// Update system record with provided system.CombinedData
func (h *Hub) saveSystemStats(record *models.Record, systemData *system.CombinedData) {
record.Set("status", "up") record.Set("status", "up")
record.Set("info", systemData.Info) record.Set("info", systemData.Info)
if err := h.app.Dao().SaveRecord(record); err != nil { if err := h.app.Dao().SaveRecord(record); err != nil {
@@ -320,14 +350,20 @@ func (h *Hub) updateSystemStatus(record *models.Record, status string) {
} }
} }
// Deletes the SSH connection (remote) or built-in agent reference
func (h *Hub) deleteSystemConnection(record *models.Record) { func (h *Hub) deleteSystemConnection(record *models.Record) {
if _, ok := h.systemConnections[record.Id]; ok { switch record.GetString("host") {
if h.systemConnections[record.Id] != nil { case "hubsys":
h.systemConnections[record.Id].Close() h.hubAgent = nil
default:
if _, ok := h.systemConnections[record.Id]; ok {
if h.systemConnections[record.Id] != nil {
h.systemConnections[record.Id].Close()
}
h.connectionLock.Lock()
defer h.connectionLock.Unlock()
delete(h.systemConnections, record.Id)
} }
h.connectionLock.Lock()
defer h.connectionLock.Unlock()
delete(h.systemConnections, record.Id)
} }
} }

View File

@@ -1,57 +0,0 @@
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,15 +118,17 @@ func (rm *RecordManager) CreateLongerRecords() {
continue continue
} }
// average the shorter records and create longer record // average the shorter records and create longer record
longerRecord := models.NewRecord(collection) var stats interface{}
longerRecord.Set("system", system.Id)
longerRecord.Set("type", recordData.longerType)
switch collection.Name { switch collection.Name {
case "system_stats": case "system_stats":
longerRecord.Set("stats", rm.AverageSystemStats(allShorterRecords)) stats = rm.AverageSystemStats(allShorterRecords)
case "container_stats": case "container_stats":
longerRecord.Set("stats", rm.AverageContainerStats(allShorterRecords)) stats = rm.AverageContainerStats(allShorterRecords)
} }
longerRecord := models.NewRecord(collection)
longerRecord.Set("system", system.Id)
longerRecord.Set("stats", stats)
longerRecord.Set("type", recordData.longerType)
if err := txDao.SaveRecord(longerRecord); err != nil { if err := txDao.SaveRecord(longerRecord); err != nil {
log.Println("failed to save longer record", "err", err.Error()) log.Println("failed to save longer record", "err", err.Error())
} }
@@ -169,12 +171,6 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
sum.DiskWritePs += stats.DiskWritePs sum.DiskWritePs += stats.DiskWritePs
sum.NetworkSent += stats.NetworkSent sum.NetworkSent += stats.NetworkSent
sum.NetworkRecv += stats.NetworkRecv 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 // add temps to sum
if stats.Temperatures != nil { if stats.Temperatures != nil {
tempCount++ tempCount++
@@ -195,34 +191,26 @@ 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),
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), 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 { if len(sum.Temperatures) != 0 {
@@ -236,12 +224,10 @@ 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,
} }
} }
} }

View File

@@ -1,4 +1,5 @@
package agent // Package update handles updating beszel and beszel-agent.
package update
import ( import (
"beszel" "beszel"
@@ -10,8 +11,51 @@ import (
"github.com/rhysd/go-github-selfupdate/selfupdate" "github.com/rhysd/go-github-selfupdate/selfupdate"
) )
// Update updates beszel-agent to the latest version func UpdateBeszel() {
func Update() { 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() {
var latest *selfupdate.Release var latest *selfupdate.Release
var found bool var found bool
var err error var err error

View File

@@ -1,131 +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,
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

@@ -0,0 +1,105 @@
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,14 +6,7 @@ import {
ChartTooltipContent, ChartTooltipContent,
} from '@/components/ui/chart' } from '@/components/ui/chart'
import { useMemo } from 'react' import { useMemo } from 'react'
import { import { useYAxisWidth, chartTimeData, cn, formatShortDate, twoDecimalString } from '@/lib/utils'
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'
@@ -72,6 +65,7 @@ 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,
})} })}
@@ -80,7 +74,9 @@ export default function ContainerCpuChart({
accessibilityLayer accessibilityLayer
// syncId={'cpu'} // syncId={'cpu'}
data={chartData} data={chartData}
margin={chartMargin} margin={{
top: 10,
}}
reverseStackOrder={true} reverseStackOrder={true}
> >
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />

View File

@@ -13,7 +13,6 @@ 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'
@@ -73,6 +72,7 @@ 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,7 +81,9 @@ export default function ContainerMemChart({
accessibilityLayer accessibilityLayer
data={chartData} data={chartData}
reverseStackOrder={true} reverseStackOrder={true}
margin={chartMargin} margin={{
top: 10,
}}
> >
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<YAxis <YAxis

View File

@@ -13,7 +13,6 @@ 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'
@@ -65,10 +64,15 @@ 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,
})} })}
@@ -76,7 +80,9 @@ export default function ContainerCpuChart({
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={chartData} data={chartData}
margin={chartMargin} margin={{
top: 10,
}}
reverseStackOrder={true} reverseStackOrder={true}
> >
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />

View File

@@ -1,19 +1,11 @@
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 { import { useYAxisWidth, chartTimeData, cn, formatShortDate, twoDecimalString } from '@/lib/utils'
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, $cpuMax } from '@/lib/stores' import { $chartTime } 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,
@@ -24,16 +16,11 @@ 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,
})} })}
@@ -41,7 +28,7 @@ export default function CpuChart({
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={systemData} data={systemData}
margin={chartMargin} margin={{ top: 10 }}
// syncId={'cpu'} // syncId={'cpu'}
> >
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
@@ -76,13 +63,16 @@ export default function CpuChart({
} }
/> />
<Area <Area
dataKey={dataKey} dataKey="stats.cpu"
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>

View File

@@ -10,7 +10,6 @@ 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'
@@ -36,11 +35,21 @@ 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 accessibilityLayer data={systemData} margin={chartMargin}> <AreaChart
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"

View File

@@ -0,0 +1,102 @@
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,7 +8,6 @@ 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'
@@ -33,11 +32,18 @@ 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 accessibilityLayer data={systemData} margin={chartMargin}> <AreaChart
accessibilityLayer
data={systemData}
margin={{
top: 10,
}}
>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
{totalMem && ( {totalMem && (
<YAxis <YAxis

View File

@@ -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,11 +27,12 @@ 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={chartMargin}> <AreaChart accessibilityLayer data={systemData} margin={{ top: 10 }}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<YAxis <YAxis
className="tracking-tighter" className="tracking-tighter"

View File

@@ -14,7 +14,6 @@ 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'
@@ -61,11 +60,21 @@ 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 accessibilityLayer data={newChartData.data} margin={chartMargin}> <LineChart
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"

View File

@@ -94,13 +94,15 @@ export function UserAuthForm({
setErrors({ passwordConfirm: msg }) setErrors({ passwordConfirm: msg })
return return
} }
// create admin user
await pb.admins.create({ await pb.admins.create({
email, email,
password, password,
passwordConfirm: password, passwordConfirm: password,
}) })
await pb.admins.authWithPassword(email, password) await pb.admins.authWithPassword(email, password)
await pb.collection('users').create({ // create regular user
const user = await pb.collection('users').create({
username, username,
email, email,
password, password,
@@ -108,6 +110,13 @@ export function UserAuthForm({
role: 'admin', role: 'admin',
verified: true, verified: true,
}) })
// create hubsys
await pb.collection('systems').create({
name: 'x',
port: 'x',
host: 'hubsys',
users: user.id,
})
await pb.collection('users').authWithPassword(email, password) await pb.collection('users').authWithPassword(email, password)
} else { } else {
await pb.collection('users').authWithPassword(email, password) await pb.collection('users').authWithPassword(email, password)

View File

@@ -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 React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription } from '../ui/card' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import Spinner from '../spinner' import Spinner from '../spinner'
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react' import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
@@ -12,34 +12,28 @@ 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 { ChartAverage, ChartMax, Rows, TuxIcon } from '../ui/icons' import { 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 AreaChartDefault = lazy(() => import('../charts/area-chart')) const DiskIoChart = lazy(() => import('../charts/disk-io-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'))
const cache = new Map<string, SystemStatsRecord[] | ContainerStatsRecord[]>()
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>[]>(
[] []
) )
@@ -49,18 +43,15 @@ export default function SystemDetail({ name }: { name: string }) {
const [dockerNetChartData, setDockerNetChartData] = useState<Record<string, number | number[]>[]>( const [dockerNetChartData, setDockerNetChartData] = useState<Record<string, number | number[]>[]>(
[] []
) )
const isLongerChart = chartTime !== '1h' const hasDockerStats = dockerCpuChartData.length > 0
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('')
cpuMaxStore[1](false) // setHasDocker(false)
bandwidthMaxStore[1](false)
diskIoMaxStore[1](false)
} }
}, [name]) }, [name])
@@ -97,12 +88,10 @@ export default function SystemDetail({ name }: { name: string }) {
}, [system]) }, [system])
async function getStats<T>(collection: string): Promise<T[]> { 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({ return await pb.collection<T>(collection).getFullList({
filter: pb.filter('system={:id} && created > {:created} && type={:type}', { filter: pb.filter('system={:id} && created > {:created} && type={:type}', {
id: system.id, id: system.id,
created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined), created: getPbTimestamp(chartTime),
type: chartTimeData[chartTime].type, type: chartTimeData[chartTime].type,
}), }),
fields: 'created,stats', fields: 'created,stats',
@@ -143,34 +132,13 @@ export default function SystemDetail({ name }: { name: string }) {
getStats<SystemStatsRecord>('system_stats'), getStats<SystemStatsRecord>('system_stats'),
getStats<ContainerStatsRecord>('container_stats'), getStats<ContainerStatsRecord>('container_stats'),
]).then(([systemStats, containerStats]) => { ]).then(([systemStats, containerStats]) => {
const { expectedInterval } = chartTimeData[chartTime] const expectedInterval = chartTimeData[chartTime].expectedInterval
// 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) { if (containerStats.status === 'fulfilled' && containerStats.value.length) {
containerData = containerData.concat(addEmptyValues(containerStats.value, expectedInterval)) makeContainerData(addEmptyValues(containerStats.value, expectedInterval))
if (containerData.length > 120) {
containerData = containerData.slice(-100)
}
cache.set(cs_cache_key, containerData)
} }
if (containerData.length) { if (systemStats.status === 'fulfilled') {
!containerFilterBar && setContainerFilterBar(<ContainerFilterBar />) setSystemStats(addEmptyValues(systemStats.value, expectedInterval))
} else if (containerFilterBar) {
setContainerFilterBar(null)
} }
makeContainerData(containerData)
}) })
}, [system, chartTime]) }, [system, chartTime])
@@ -181,10 +149,7 @@ 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])
const newTicks = scale.ticks(chartTimeData[chartTime].ticks).map((d) => d.getTime()) setTicks(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
@@ -227,12 +192,12 @@ 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`
} }
return [ return [
{ value: system.host, Icon: GlobeIcon }, { value: system.host, Icon: GlobeIcon, hide: system.host === 'hubsys' },
{ {
value: system.info.h, value: system.info.h,
Icon: MonitorIcon, Icon: MonitorIcon,
@@ -274,7 +239,7 @@ export default function SystemDetail({ name }: { name: string }) {
return ( return (
<> <>
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip"> <div id="chartwrap" className="grid gap-4 mb-10">
{/* 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">
@@ -359,27 +324,17 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard <ChartCard
grid={grid} grid={grid}
title="Total CPU Usage" title="Total CPU Usage"
description={`${ description="Average system-wide CPU utilization"
cpuMaxStore[0] && isLongerChart ? 'Max 1 min ' : 'Average'
} system-wide CPU utilization`}
cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null}
> >
<AreaChartDefault <CpuChart ticks={ticks} systemData={systemStats} />
ticks={ticks}
systemData={systemStats}
chartName="CPU Usage"
showMax={isLongerChart && cpuMaxStore[0]}
unit="%"
chartTime={chartTime}
/>
</ChartCard> </ChartCard>
{containerFilterBar && ( {hasDockerStats && (
<ChartCard <ChartCard
grid={grid} grid={grid}
title="Docker CPU Usage" title="Docker CPU Usage"
description="Average CPU utilization of containers" description="CPU utilization of docker containers"
cornerEl={containerFilterBar} isContainerChart={true}
> >
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} /> <ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
</ChartCard> </ChartCard>
@@ -393,12 +348,12 @@ export default function SystemDetail({ name }: { name: string }) {
<MemChart ticks={ticks} systemData={systemStats} /> <MemChart ticks={ticks} systemData={systemStats} />
</ChartCard> </ChartCard>
{containerFilterBar && ( {hasDockerStats && (
<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"
cornerEl={containerFilterBar} isContainerChart={true}
> >
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} /> <ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
</ChartCard> </ChartCard>
@@ -413,37 +368,23 @@ export default function SystemDetail({ name }: { name: string }) {
/> />
</ChartCard> </ChartCard>
<ChartCard <ChartCard grid={grid} title="Disk I/O" description="Throughput of root filesystem">
grid={grid} <DiskIoChart
title="Disk I/O"
description="Throughput of root filesystem"
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
>
<AreaChartDefault
ticks={ticks} ticks={ticks}
systemData={systemStats} systemData={systemStats}
showMax={isLongerChart && diskIoMaxStore[0]} dataKeys={['stats.dw', 'stats.dr']}
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"
> >
<AreaChartDefault <BandwidthChart ticks={ticks} systemData={systemStats} />
ticks={ticks}
systemData={systemStats}
showMax={isLongerChart && bandwidthMaxStore[0]}
chartName="bw"
chartTime={chartTime}
/>
</ChartCard> </ChartCard>
{containerFilterBar && dockerNetChartData.length > 0 && ( {hasDockerStats && dockerNetChartData.length > 0 && (
<div <div
ref={netCardRef} ref={netCardRef}
className={cn({ className={cn({
@@ -453,7 +394,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"
cornerEl={containerFilterBar} isContainerChart={true}
> >
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} /> <ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
</ChartCard> </ChartCard>
@@ -495,14 +436,11 @@ 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}
> >
<AreaChartDefault <DiskIoChart
ticks={ticks} ticks={ticks}
systemData={systemStats} systemData={systemStats}
showMax={isLongerChart && diskIoMaxStore[0]} dataKeys={[`stats.efs.${extraFsName}.w`, `stats.efs.${extraFsName}.r`]}
chartName={`efs.${extraFsName}`}
chartTime={chartTime}
/> />
</ChartCard> </ChartCard>
</div> </div>
@@ -523,10 +461,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"
@@ -545,33 +483,7 @@ 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>
) )
} }
@@ -580,13 +492,13 @@ function ChartCard({
description, description,
children, children,
grid, grid,
cornerEl, isContainerChart,
}: { }: {
title: string title: string
description: string description: string
children: React.ReactNode children: React.ReactNode
grid?: boolean grid?: boolean
cornerEl?: JSX.Element | null isContainerChart?: boolean
}) { }) {
const { isIntersecting, ref } = useIntersectionObserver() const { isIntersecting, ref } = useIntersectionObserver()
@@ -598,16 +510,12 @@ 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>
{cornerEl && ( {isContainerChart && <ContainerFilterBar />}
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
{cornerEl}
</div>
)}
</CardHeader> </CardHeader>
<div className="pl-0 w-[calc(100%-1.6em)] h-52 relative"> <CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
{<Spinner />} {<Spinner />}
{isIntersecting && <Suspense>{children}</Suspense>} {isIntersecting && <Suspense>{children}</Suspense>}
</div> </CardContent>
</Card> </Card>
) )
} }

View File

@@ -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, ...props }, ref) => { >(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId() const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}` const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
return ( return (
//<ChartContext.Provider value={{ config }}> <ChartContext.Provider value={{ config }}>
<div <div
data-chart={chartId} data-chart={chartId}
ref={ref} ref={ref}
className={cn( 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", "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 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,8 +126,7 @@ const ChartTooltipContent = React.forwardRef<
}, },
ref ref
) => { ) => {
// const { config } = useChart() const { config } = useChart()
const config = {}
React.useMemo(() => { React.useMemo(() => {
if (filter) { if (filter) {
@@ -147,7 +146,10 @@ 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 = !labelKey && typeof label === 'string' ? label : itemConfig?.label const value =
!labelKey && typeof label === 'string'
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) { if (labelFormatter) {
return ( return (
@@ -260,7 +262,7 @@ const ChartLegendContent = React.forwardRef<
hideIcon?: boolean hideIcon?: boolean
nameKey?: string nameKey?: string
} }
>(({ className, payload, verticalAlign = 'bottom' }, ref) => { >(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
// const { config } = useChart() // const { config } = useChart()
if (!payload?.length) { if (!payload?.length) {
@@ -340,5 +342,5 @@ export {
ChartTooltipContent, ChartTooltipContent,
ChartLegend, ChartLegend,
ChartLegendContent, ChartLegendContent,
// ChartStyle, ChartStyle,
} }

View File

@@ -23,25 +23,3 @@ 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>
)
}

View File

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

View File

@@ -35,8 +35,6 @@ export interface SystemInfo {
export interface SystemStats { export interface SystemStats {
/** cpu percent */ /** cpu percent */
cpu: number cpu: number
/** peak cpu */
cpum?: number
/** total memory (gb) */ /** total memory (gb) */
m: number m: number
/** memory used (gb) */ /** memory used (gb) */
@@ -61,18 +59,10 @@ 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
/** network received (mb) */ /** network received (mb) */
nr: number nr: number
/** max network sent (mb) */
nsm?: number
/** max network received (mb) */
nrm?: number
/** temperatures */ /** temperatures */
t?: Record<string, number> t?: Record<string, number>
/** extra filesystems */ /** extra filesystems */
@@ -88,10 +78,6 @@ 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 {

View File

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