mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 05:36:15 +01:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23ab1208cd | ||
|
|
5b0fac429b | ||
|
|
efca56ceca | ||
|
|
64f0a23969 | ||
|
|
4245da7792 | ||
|
|
cedf80a869 | ||
|
|
76cea9d3c3 | ||
|
|
10ef430826 | ||
|
|
d672017af0 | ||
|
|
7a82571921 | ||
|
|
e81f8ac387 |
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"beszel"
|
"beszel"
|
||||||
"beszel/internal/agent"
|
"beszel/internal/agent"
|
||||||
"beszel/internal/update"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@@ -17,7 +16,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":
|
||||||
update.UpdateBeszelAgent()
|
agent.Update()
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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"
|
||||||
@@ -22,7 +21,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: func(_ *cobra.Command, _ []string) { update.UpdateBeszel() },
|
Run: hub.Update,
|
||||||
})
|
})
|
||||||
|
|
||||||
hub.NewHub(app).Run()
|
hub.NewHub(app).Run()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ 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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ func (a *Agent) Run(pubKey []byte, addr string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ 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)
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ func newDockerManager() *dockerManager {
|
|||||||
|
|
||||||
dockerClient := &dockerManager{
|
dockerClient := &dockerManager{
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: time.Millisecond * 1100,
|
Timeout: time.Millisecond * 2100,
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
},
|
},
|
||||||
containerStatsMap: make(map[string]*container.Stats),
|
containerStatsMap: make(map[string]*container.Stats),
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ 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)
|
||||||
@@ -61,6 +62,7 @@ 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)
|
||||||
@@ -89,6 +91,7 @@ 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)
|
||||||
@@ -109,6 +112,7 @@ 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]
|
||||||
@@ -132,6 +136,7 @@ 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()
|
||||||
@@ -172,6 +177,7 @@ 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
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// Package update handles updating beszel and beszel-agent.
|
package agent
|
||||||
package update
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"beszel"
|
"beszel"
|
||||||
@@ -11,51 +10,8 @@ import (
|
|||||||
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpdateBeszel() {
|
// Update updates beszel-agent to the latest version
|
||||||
var latest *selfupdate.Release
|
func Update() {
|
||||||
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
|
||||||
@@ -6,35 +6,42 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
Cpu float64 `json:"cpu"`
|
Cpu float64 `json:"cpu"`
|
||||||
Mem float64 `json:"m"`
|
MaxCpu float64 `json:"cpum,omitempty"`
|
||||||
MemUsed float64 `json:"mu"`
|
Mem float64 `json:"m"`
|
||||||
MemPct float64 `json:"mp"`
|
MemUsed float64 `json:"mu"`
|
||||||
MemBuffCache float64 `json:"mb"`
|
MemPct float64 `json:"mp"`
|
||||||
MemZfsArc float64 `json:"mz,omitempty"` // ZFS ARC memory
|
MemBuffCache float64 `json:"mb"`
|
||||||
Swap float64 `json:"s,omitempty"`
|
MemZfsArc float64 `json:"mz,omitempty"` // ZFS ARC memory
|
||||||
SwapUsed float64 `json:"su,omitempty"`
|
Swap float64 `json:"s,omitempty"`
|
||||||
DiskTotal float64 `json:"d"`
|
SwapUsed float64 `json:"su,omitempty"`
|
||||||
DiskUsed float64 `json:"du"`
|
DiskTotal float64 `json:"d"`
|
||||||
DiskPct float64 `json:"dp"`
|
DiskUsed float64 `json:"du"`
|
||||||
DiskReadPs float64 `json:"dr"`
|
DiskPct float64 `json:"dp"`
|
||||||
DiskWritePs float64 `json:"dw"`
|
DiskReadPs float64 `json:"dr"`
|
||||||
NetworkSent float64 `json:"ns"`
|
DiskWritePs float64 `json:"dw"`
|
||||||
NetworkRecv float64 `json:"nr"`
|
MaxDiskReadPs float64 `json:"drm,omitempty"`
|
||||||
Temperatures map[string]float64 `json:"t,omitempty"`
|
MaxDiskWritePs float64 `json:"dwm,omitempty"`
|
||||||
ExtraFs map[string]*FsStats `json:"efs,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 {
|
type FsStats struct {
|
||||||
Time time.Time `json:"-"`
|
Time time.Time `json:"-"`
|
||||||
Root bool `json:"-"`
|
Root bool `json:"-"`
|
||||||
Mountpoint string `json:"-"`
|
Mountpoint string `json:"-"`
|
||||||
DiskTotal float64 `json:"d"`
|
DiskTotal float64 `json:"d"`
|
||||||
DiskUsed float64 `json:"du"`
|
DiskUsed float64 `json:"du"`
|
||||||
TotalRead uint64 `json:"-"`
|
TotalRead uint64 `json:"-"`
|
||||||
TotalWrite uint64 `json:"-"`
|
TotalWrite uint64 `json:"-"`
|
||||||
DiskWritePs float64 `json:"w"`
|
DiskReadPs float64 `json:"r"`
|
||||||
DiskReadPs float64 `json:"r"`
|
DiskWritePs float64 `json:"w"`
|
||||||
|
MaxDiskReadPS float64 `json:"rm,omitempty"`
|
||||||
|
MaxDiskWritePS float64 `json:"wm,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetIoStats struct {
|
type NetIoStats struct {
|
||||||
|
|||||||
57
beszel/internal/hub/update.go
Normal file
57
beszel/internal/hub/update.go
Normal 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))
|
||||||
|
}
|
||||||
@@ -118,17 +118,15 @@ func (rm *RecordManager) CreateLongerRecords() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// average the shorter records and create longer record
|
// 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 := models.NewRecord(collection)
|
||||||
longerRecord.Set("system", system.Id)
|
longerRecord.Set("system", system.Id)
|
||||||
longerRecord.Set("stats", stats)
|
|
||||||
longerRecord.Set("type", recordData.longerType)
|
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 {
|
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())
|
||||||
}
|
}
|
||||||
@@ -161,6 +159,7 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
sum.MemUsed += stats.MemUsed
|
sum.MemUsed += stats.MemUsed
|
||||||
sum.MemPct += stats.MemPct
|
sum.MemPct += stats.MemPct
|
||||||
sum.MemBuffCache += stats.MemBuffCache
|
sum.MemBuffCache += stats.MemBuffCache
|
||||||
|
sum.MemZfsArc += stats.MemZfsArc
|
||||||
sum.Swap += stats.Swap
|
sum.Swap += stats.Swap
|
||||||
sum.SwapUsed += stats.SwapUsed
|
sum.SwapUsed += stats.SwapUsed
|
||||||
sum.DiskTotal += stats.DiskTotal
|
sum.DiskTotal += stats.DiskTotal
|
||||||
@@ -170,6 +169,12 @@ 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++
|
||||||
@@ -190,25 +195,34 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
sum.ExtraFs[key].DiskUsed += value.DiskUsed
|
sum.ExtraFs[key].DiskUsed += value.DiskUsed
|
||||||
sum.ExtraFs[key].DiskWritePs += value.DiskWritePs
|
sum.ExtraFs[key].DiskWritePs += value.DiskWritePs
|
||||||
sum.ExtraFs[key].DiskReadPs += value.DiskReadPs
|
sum.ExtraFs[key].DiskReadPs += value.DiskReadPs
|
||||||
|
// peak values
|
||||||
|
sum.ExtraFs[key].MaxDiskReadPS = max(sum.ExtraFs[key].MaxDiskReadPS, value.MaxDiskReadPS, value.DiskReadPs)
|
||||||
|
sum.ExtraFs[key].MaxDiskWritePS = max(sum.ExtraFs[key].MaxDiskWritePS, value.MaxDiskWritePS, value.DiskWritePs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stats = system.Stats{
|
stats = system.Stats{
|
||||||
Cpu: twoDecimals(sum.Cpu / count),
|
Cpu: twoDecimals(sum.Cpu / count),
|
||||||
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),
|
||||||
Swap: twoDecimals(sum.Swap / count),
|
MemZfsArc: twoDecimals(sum.MemZfsArc / count),
|
||||||
SwapUsed: twoDecimals(sum.SwapUsed / count),
|
Swap: twoDecimals(sum.Swap / count),
|
||||||
DiskTotal: twoDecimals(sum.DiskTotal / count),
|
SwapUsed: twoDecimals(sum.SwapUsed / count),
|
||||||
DiskUsed: twoDecimals(sum.DiskUsed / count),
|
DiskTotal: twoDecimals(sum.DiskTotal / count),
|
||||||
DiskPct: twoDecimals(sum.DiskPct / count),
|
DiskUsed: twoDecimals(sum.DiskUsed / count),
|
||||||
DiskReadPs: twoDecimals(sum.DiskReadPs / count),
|
DiskPct: twoDecimals(sum.DiskPct / count),
|
||||||
DiskWritePs: twoDecimals(sum.DiskWritePs / count),
|
DiskReadPs: twoDecimals(sum.DiskReadPs / count),
|
||||||
NetworkSent: twoDecimals(sum.NetworkSent / count),
|
DiskWritePs: twoDecimals(sum.DiskWritePs / count),
|
||||||
NetworkRecv: twoDecimals(sum.NetworkRecv / 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 {
|
if len(sum.Temperatures) != 0 {
|
||||||
@@ -222,10 +236,12 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
stats.ExtraFs = make(map[string]*system.FsStats)
|
stats.ExtraFs = make(map[string]*system.FsStats)
|
||||||
for key, value := range sum.ExtraFs {
|
for key, value := range sum.ExtraFs {
|
||||||
stats.ExtraFs[key] = &system.FsStats{
|
stats.ExtraFs[key] = &system.FsStats{
|
||||||
DiskTotal: twoDecimals(value.DiskTotal / count),
|
DiskTotal: twoDecimals(value.DiskTotal / count),
|
||||||
DiskUsed: twoDecimals(value.DiskUsed / count),
|
DiskUsed: twoDecimals(value.DiskUsed / count),
|
||||||
DiskWritePs: twoDecimals(value.DiskWritePs / count),
|
DiskWritePs: twoDecimals(value.DiskWritePs / count),
|
||||||
DiskReadPs: twoDecimals(value.DiskReadPs / count),
|
DiskReadPs: twoDecimals(value.DiskReadPs / count),
|
||||||
|
MaxDiskReadPS: value.MaxDiskReadPS,
|
||||||
|
MaxDiskWritePS: value.MaxDiskWritePS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
beszel/site/src/components/charts/area-chart.tsx
Normal file
131
beszel/site/src/components/charts/area-chart.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
|
import {
|
||||||
|
useYAxisWidth,
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
toFixedWithoutTrailingZeros,
|
||||||
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
|
} from '@/lib/utils'
|
||||||
|
// import Spinner from '../spinner'
|
||||||
|
import { ChartTimes, SystemStatsRecord } from '@/types'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
/** [label, key, color, opacity] */
|
||||||
|
type DataKeys = [string, string, number, number]
|
||||||
|
|
||||||
|
const getNestedValue = (path: string, max = false, data: any): number | null => {
|
||||||
|
// fallback value (obj?.stats?.cpum ? 0 : null) should only come into play when viewing
|
||||||
|
// a max value which doesn't exist, or the value was zero and omitted from the stats object.
|
||||||
|
// so we check if cpum is present. if so, return 0 to make sure the zero value is displayed.
|
||||||
|
// if not, return null - there is no max data so do not display anything.
|
||||||
|
return `stats.${path}${max ? 'm' : ''}`
|
||||||
|
.split('.')
|
||||||
|
.reduce((acc: any, key: string) => acc?.[key] ?? (data.stats?.cpum ? 0 : null), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AreaChartDefault({
|
||||||
|
ticks,
|
||||||
|
systemData,
|
||||||
|
showMax = false,
|
||||||
|
unit = ' MB/s',
|
||||||
|
chartName,
|
||||||
|
chartTime,
|
||||||
|
}: {
|
||||||
|
ticks: number[]
|
||||||
|
systemData: SystemStatsRecord[]
|
||||||
|
showMax?: boolean
|
||||||
|
unit?: string
|
||||||
|
chartName: string
|
||||||
|
chartTime: ChartTimes
|
||||||
|
}) {
|
||||||
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|
||||||
|
const dataKeys: DataKeys[] = useMemo(() => {
|
||||||
|
// [label, key, color, opacity]
|
||||||
|
if (chartName === 'CPU Usage') {
|
||||||
|
return [[chartName, 'cpu', 1, 0.4]]
|
||||||
|
} else if (chartName === 'dio') {
|
||||||
|
return [
|
||||||
|
['Write', 'dw', 3, 0.3],
|
||||||
|
['Read', 'dr', 1, 0.3],
|
||||||
|
]
|
||||||
|
} else if (chartName === 'bw') {
|
||||||
|
return [
|
||||||
|
['Sent', 'ns', 5, 0.2],
|
||||||
|
['Received', 'nr', 2, 0.2],
|
||||||
|
]
|
||||||
|
} else if (chartName.startsWith('efs')) {
|
||||||
|
return [
|
||||||
|
['Write', `${chartName}.w`, 3, 0.3],
|
||||||
|
['Read', `${chartName}.r`, 1, 0.3],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ChartContainer
|
||||||
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
|
'opacity-100': yAxisWidth,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis
|
||||||
|
className="tracking-tighter"
|
||||||
|
width={yAxisWidth}
|
||||||
|
tickFormatter={(value) => {
|
||||||
|
const val = toFixedWithoutTrailingZeros(value, 2) + unit
|
||||||
|
return updateYAxisWidth(val)
|
||||||
|
}}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="created"
|
||||||
|
domain={[ticks[0], ticks.at(-1)!]}
|
||||||
|
ticks={ticks}
|
||||||
|
type="number"
|
||||||
|
scale={'time'}
|
||||||
|
minTickGap={35}
|
||||||
|
tickMargin={8}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={chartTimeData[chartTime].format}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + unit}
|
||||||
|
indicator="line"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{dataKeys.map((key, i) => {
|
||||||
|
const color = `hsl(var(--chart-${key[2]}))`
|
||||||
|
return (
|
||||||
|
<Area
|
||||||
|
key={i}
|
||||||
|
dataKey={getNestedValue.bind(null, key[1], showMax)}
|
||||||
|
name={key[0]}
|
||||||
|
type="monotoneX"
|
||||||
|
fill={color}
|
||||||
|
fillOpacity={key[3]}
|
||||||
|
stroke={color}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{/* <ChartLegend content={<ChartLegendContent />} /> */}
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
|
||||||
import {
|
|
||||||
useYAxisWidth,
|
|
||||||
chartTimeData,
|
|
||||||
cn,
|
|
||||||
formatShortDate,
|
|
||||||
toFixedWithoutTrailingZeros,
|
|
||||||
twoDecimalString,
|
|
||||||
} from '@/lib/utils'
|
|
||||||
// import Spinner from '../spinner'
|
|
||||||
import { useStore } from '@nanostores/react'
|
|
||||||
import { $chartTime } from '@/lib/stores'
|
|
||||||
import { SystemStatsRecord } from '@/types'
|
|
||||||
|
|
||||||
export default function BandwidthChart({
|
|
||||||
ticks,
|
|
||||||
systemData,
|
|
||||||
}: {
|
|
||||||
ticks: number[]
|
|
||||||
systemData: SystemStatsRecord[]
|
|
||||||
}) {
|
|
||||||
const chartTime = useStore($chartTime)
|
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
|
||||||
<ChartContainer
|
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
|
||||||
'opacity-100': yAxisWidth,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<AreaChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={systemData}
|
|
||||||
margin={{
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 10,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<YAxis
|
|
||||||
className="tracking-tighter"
|
|
||||||
width={yAxisWidth}
|
|
||||||
// domain={[0, (max: number) => (max <= 0.4 ? 0.4 : Math.ceil(max))]}
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
const val = toFixedWithoutTrailingZeros(value, 2) + ' MB/s'
|
|
||||||
return updateYAxisWidth(val)
|
|
||||||
}}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
// unit={' MB/s'}
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="created"
|
|
||||||
domain={[ticks[0], ticks.at(-1)!]}
|
|
||||||
ticks={ticks}
|
|
||||||
type="number"
|
|
||||||
scale={'time'}
|
|
||||||
minTickGap={35}
|
|
||||||
tickMargin={8}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={chartTimeData[chartTime].format}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
|
||||||
animationEasing="ease-out"
|
|
||||||
animationDuration={150}
|
|
||||||
content={
|
|
||||||
<ChartTooltipContent
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
|
||||||
contentFormatter={(item) => twoDecimalString(item.value) + ' MB/s'}
|
|
||||||
indicator="line"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
dataKey="stats.ns"
|
|
||||||
name="Sent"
|
|
||||||
type="monotoneX"
|
|
||||||
fill="hsl(var(--chart-5))"
|
|
||||||
fillOpacity={0.2}
|
|
||||||
stroke="hsl(var(--chart-5))"
|
|
||||||
// animationDuration={1200}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
dataKey="stats.nr"
|
|
||||||
name="Received"
|
|
||||||
type="monotoneX"
|
|
||||||
fill="hsl(var(--chart-2))"
|
|
||||||
fillOpacity={0.2}
|
|
||||||
stroke="hsl(var(--chart-2))"
|
|
||||||
// animationDuration={1200}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,14 @@ import {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from '@/components/ui/chart'
|
} from '@/components/ui/chart'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useYAxisWidth, chartTimeData, cn, formatShortDate, twoDecimalString } from '@/lib/utils'
|
import {
|
||||||
|
useYAxisWidth,
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime, $containerFilter } from '@/lib/stores'
|
import { $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
@@ -65,7 +72,6 @@ export default function ContainerCpuChart({
|
|||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
@@ -74,9 +80,7 @@ export default function ContainerCpuChart({
|
|||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
// syncId={'cpu'}
|
// syncId={'cpu'}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
margin={{
|
margin={chartMargin}
|
||||||
top: 10,
|
|
||||||
}}
|
|
||||||
reverseStackOrder={true}
|
reverseStackOrder={true}
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
twoDecimalString,
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
@@ -72,7 +73,6 @@ export default function ContainerMemChart({
|
|||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
@@ -81,9 +81,7 @@ export default function ContainerMemChart({
|
|||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={chartData}
|
data={chartData}
|
||||||
reverseStackOrder={true}
|
reverseStackOrder={true}
|
||||||
margin={{
|
margin={chartMargin}
|
||||||
top: 10,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
twoDecimalString,
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
@@ -64,15 +65,10 @@ export default function ContainerCpuChart({
|
|||||||
return config satisfies ChartConfig
|
return config satisfies ChartConfig
|
||||||
}, [chartData])
|
}, [chartData])
|
||||||
|
|
||||||
// if (!chartData.length || !ticks.length) {
|
|
||||||
// return <Spinner />
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
@@ -80,9 +76,7 @@ export default function ContainerCpuChart({
|
|||||||
<AreaChart
|
<AreaChart
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={chartData}
|
data={chartData}
|
||||||
margin={{
|
margin={chartMargin}
|
||||||
top: 10,
|
|
||||||
}}
|
|
||||||
reverseStackOrder={true}
|
reverseStackOrder={true}
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
import { useYAxisWidth, chartTimeData, cn, formatShortDate, twoDecimalString } from '@/lib/utils'
|
import {
|
||||||
|
useYAxisWidth,
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime, $cpuMax } from '@/lib/stores'
|
||||||
import { SystemStatsRecord } from '@/types'
|
import { SystemStatsRecord } from '@/types'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
export default function CpuChart({
|
export default function CpuChart({
|
||||||
ticks,
|
ticks,
|
||||||
@@ -16,11 +24,16 @@ export default function CpuChart({
|
|||||||
}) {
|
}) {
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
const showMax = useStore($cpuMax)
|
||||||
|
|
||||||
|
const dataKey = useMemo(
|
||||||
|
() => `stats.cpu${showMax && chartTime !== '1h' ? 'm' : ''}`,
|
||||||
|
[showMax, systemData]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
@@ -28,7 +41,7 @@ export default function CpuChart({
|
|||||||
<AreaChart
|
<AreaChart
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={systemData}
|
data={systemData}
|
||||||
margin={{ top: 10 }}
|
margin={chartMargin}
|
||||||
// syncId={'cpu'}
|
// syncId={'cpu'}
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
@@ -63,16 +76,13 @@ export default function CpuChart({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="stats.cpu"
|
dataKey={dataKey}
|
||||||
name="CPU Usage"
|
name="CPU Usage"
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill="hsl(var(--chart-1))"
|
fill="hsl(var(--chart-1))"
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.4}
|
||||||
stroke="hsl(var(--chart-1))"
|
stroke="hsl(var(--chart-1))"
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
// animationEasing="ease-out"
|
|
||||||
// animationDuration={1200}
|
|
||||||
// animateNewValues={true}
|
|
||||||
/>
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
toFixedFloat,
|
toFixedFloat,
|
||||||
getSizeVal,
|
getSizeVal,
|
||||||
getSizeUnit,
|
getSizeUnit,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import { useMemo } from 'react'
|
// import { useMemo } from 'react'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
@@ -35,21 +36,11 @@ export default function DiskChart({
|
|||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AreaChart
|
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
|
||||||
accessibilityLayer
|
|
||||||
data={systemData}
|
|
||||||
margin={{
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 10,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
|
||||||
import {
|
|
||||||
useYAxisWidth,
|
|
||||||
chartTimeData,
|
|
||||||
cn,
|
|
||||||
formatShortDate,
|
|
||||||
toFixedWithoutTrailingZeros,
|
|
||||||
twoDecimalString,
|
|
||||||
} from '@/lib/utils'
|
|
||||||
// import Spinner from '../spinner'
|
|
||||||
import { useStore } from '@nanostores/react'
|
|
||||||
import { $chartTime } from '@/lib/stores'
|
|
||||||
import { SystemStatsRecord } from '@/types'
|
|
||||||
|
|
||||||
export default function DiskIoChart({
|
|
||||||
ticks,
|
|
||||||
systemData,
|
|
||||||
dataKeys,
|
|
||||||
}: {
|
|
||||||
ticks: number[]
|
|
||||||
systemData: SystemStatsRecord[]
|
|
||||||
dataKeys: string[]
|
|
||||||
}) {
|
|
||||||
const chartTime = useStore($chartTime)
|
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
|
||||||
<ChartContainer
|
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
|
||||||
'opacity-100': yAxisWidth,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<AreaChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={systemData}
|
|
||||||
margin={{
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 10,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<YAxis
|
|
||||||
className="tracking-tighter"
|
|
||||||
width={yAxisWidth}
|
|
||||||
// domain={[0, (max: number) => (max <= 0.4 ? 0.4 : Math.ceil(max))]}
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
const val = toFixedWithoutTrailingZeros(value, 2) + ' MB/s'
|
|
||||||
return updateYAxisWidth(val)
|
|
||||||
}}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="created"
|
|
||||||
domain={[ticks[0], ticks.at(-1)!]}
|
|
||||||
ticks={ticks}
|
|
||||||
type="number"
|
|
||||||
scale={'time'}
|
|
||||||
minTickGap={35}
|
|
||||||
tickMargin={8}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={chartTimeData[chartTime].format}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
|
||||||
animationEasing="ease-out"
|
|
||||||
animationDuration={150}
|
|
||||||
content={
|
|
||||||
<ChartTooltipContent
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
|
||||||
contentFormatter={(item) => twoDecimalString(item.value) + ' MB/s'}
|
|
||||||
indicator="line"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{dataKeys.map((dataKey, i) => {
|
|
||||||
const action = i ? 'Read' : 'Write'
|
|
||||||
const color = i ? 'hsl(var(--chart-1))' : 'hsl(var(--chart-3))'
|
|
||||||
return (
|
|
||||||
<Area
|
|
||||||
key={i}
|
|
||||||
dataKey={dataKey}
|
|
||||||
name={action}
|
|
||||||
type="monotoneX"
|
|
||||||
fill={color}
|
|
||||||
fillOpacity={0.3}
|
|
||||||
stroke={color}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
import { useYAxisWidth, chartTimeData, cn, toFixedFloat, twoDecimalString } from '@/lib/utils'
|
import {
|
||||||
|
useYAxisWidth,
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
toFixedFloat,
|
||||||
|
twoDecimalString,
|
||||||
|
formatShortDate,
|
||||||
|
chartMargin,
|
||||||
|
} from '@/lib/utils'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime } from '@/lib/stores'
|
||||||
@@ -25,18 +33,11 @@ export default function MemChart({
|
|||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AreaChart
|
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
|
||||||
accessibilityLayer
|
|
||||||
data={systemData}
|
|
||||||
margin={{
|
|
||||||
top: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
{totalMem && (
|
{totalMem && (
|
||||||
<YAxis
|
<YAxis
|
||||||
@@ -72,6 +73,7 @@ export default function MemChart({
|
|||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
itemSorter={(a, b) => a.order - b.order}
|
itemSorter={(a, b) => a.order - b.order}
|
||||||
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
contentFormatter={(item) => twoDecimalString(item.value) + ' GB'}
|
contentFormatter={(item) => twoDecimalString(item.value) + ' GB'}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
twoDecimalString,
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime } from '@/lib/stores'
|
||||||
import { SystemStatsRecord } from '@/types'
|
import { SystemStatsRecord } from '@/types'
|
||||||
@@ -27,12 +27,11 @@ export default function SwapChart({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AreaChart accessibilityLayer data={systemData} margin={{ top: 10 }}>
|
<AreaChart accessibilityLayer data={systemData} margin={chartMargin}>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
twoDecimalString,
|
twoDecimalString,
|
||||||
|
chartMargin,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime } from '@/lib/stores'
|
||||||
@@ -60,21 +61,11 @@ export default function TemperatureChart({
|
|||||||
<div>
|
<div>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{}}
|
|
||||||
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
'opacity-100': yAxisWidth,
|
'opacity-100': yAxisWidth,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<LineChart
|
<LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}>
|
||||||
accessibilityLayer
|
|
||||||
data={newChartData.data}
|
|
||||||
margin={{
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 10,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from '@/lib/stores'
|
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from '@/lib/stores'
|
||||||
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
||||||
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
import { Card, CardHeader, CardTitle, CardDescription } from '../ui/card'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
|
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
|
||||||
@@ -12,28 +12,34 @@ import { scaleTime } from 'd3-scale'
|
|||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||||
import { Button, buttonVariants } from '../ui/button'
|
import { Button, buttonVariants } from '../ui/button'
|
||||||
import { Input } from '../ui/input'
|
import { Input } from '../ui/input'
|
||||||
import { Rows, TuxIcon } from '../ui/icons'
|
import { ChartAverage, ChartMax, Rows, TuxIcon } from '../ui/icons'
|
||||||
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
|
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||||
|
|
||||||
const CpuChart = lazy(() => import('../charts/cpu-chart'))
|
|
||||||
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
|
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
|
||||||
const MemChart = lazy(() => import('../charts/mem-chart'))
|
const MemChart = lazy(() => import('../charts/mem-chart'))
|
||||||
const ContainerMemChart = lazy(() => import('../charts/container-mem-chart'))
|
const ContainerMemChart = lazy(() => import('../charts/container-mem-chart'))
|
||||||
const DiskChart = lazy(() => import('../charts/disk-chart'))
|
const DiskChart = lazy(() => import('../charts/disk-chart'))
|
||||||
const DiskIoChart = lazy(() => import('../charts/disk-io-chart'))
|
const AreaChartDefault = lazy(() => import('../charts/area-chart'))
|
||||||
const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
|
|
||||||
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
|
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
|
||||||
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
||||||
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
||||||
|
|
||||||
|
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>[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -43,15 +49,18 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
const [dockerNetChartData, setDockerNetChartData] = useState<Record<string, number | number[]>[]>(
|
const [dockerNetChartData, setDockerNetChartData] = useState<Record<string, number | number[]>[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const hasDockerStats = dockerCpuChartData.length > 0
|
const isLongerChart = chartTime !== '1h'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${name} / Beszel`
|
document.title = `${name} / Beszel`
|
||||||
return () => {
|
return () => {
|
||||||
resetCharts()
|
resetCharts()
|
||||||
$chartTime.set($userSettings.get().chartTime)
|
$chartTime.set($userSettings.get().chartTime)
|
||||||
|
setContainerFilterBar(null)
|
||||||
$containerFilter.set('')
|
$containerFilter.set('')
|
||||||
// setHasDocker(false)
|
cpuMaxStore[1](false)
|
||||||
|
bandwidthMaxStore[1](false)
|
||||||
|
diskIoMaxStore[1](false)
|
||||||
}
|
}
|
||||||
}, [name])
|
}, [name])
|
||||||
|
|
||||||
@@ -88,10 +97,12 @@ 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),
|
created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined),
|
||||||
type: chartTimeData[chartTime].type,
|
type: chartTimeData[chartTime].type,
|
||||||
}),
|
}),
|
||||||
fields: 'created,stats',
|
fields: 'created,stats',
|
||||||
@@ -132,13 +143,34 @@ 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].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) {
|
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') {
|
if (containerData.length) {
|
||||||
setSystemStats(addEmptyValues(systemStats.value, expectedInterval))
|
!containerFilterBar && setContainerFilterBar(<ContainerFilterBar />)
|
||||||
|
} else if (containerFilterBar) {
|
||||||
|
setContainerFilterBar(null)
|
||||||
}
|
}
|
||||||
|
makeContainerData(containerData)
|
||||||
})
|
})
|
||||||
}, [system, chartTime])
|
}, [system, chartTime])
|
||||||
|
|
||||||
@@ -149,7 +181,10 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const startTime = chartTimeData[chartTime].getOffset(now)
|
const startTime = chartTimeData[chartTime].getOffset(now)
|
||||||
const scale = scaleTime([startTime.getTime(), now], [0, systemStats.length])
|
const scale = scaleTime([startTime.getTime(), now], [0, systemStats.length])
|
||||||
setTicks(scale.ticks(chartTimeData[chartTime].ticks).map((d) => d.getTime()))
|
const newTicks = scale.ticks(chartTimeData[chartTime].ticks).map((d) => d.getTime())
|
||||||
|
if (newTicks[0] !== ticks[0]) {
|
||||||
|
setTicks(newTicks)
|
||||||
|
}
|
||||||
}, [chartTime, systemStats])
|
}, [chartTime, systemStats])
|
||||||
|
|
||||||
// make container stats for charts
|
// make container stats for charts
|
||||||
@@ -192,7 +227,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
let uptime: number | string = system.info.u
|
let uptime: number | string = system.info.u
|
||||||
if (system.info.u < 172800) {
|
if (system.info.u < 172800) {
|
||||||
const hours = Math.trunc(uptime / 3600)
|
const hours = Math.trunc(uptime / 3600)
|
||||||
uptime = `${hours} hour${hours > 1 ? 's' : ''}`
|
uptime = `${hours} hour${hours == 1 ? '' : 's'}`
|
||||||
} else {
|
} else {
|
||||||
uptime = `${Math.trunc(system.info?.u / 86400)} days`
|
uptime = `${Math.trunc(system.info?.u / 86400)} days`
|
||||||
}
|
}
|
||||||
@@ -239,7 +274,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="chartwrap" className="grid gap-4 mb-10">
|
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
|
||||||
{/* system info */}
|
{/* system info */}
|
||||||
<Card>
|
<Card>
|
||||||
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
||||||
@@ -324,17 +359,27 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<ChartCard
|
<ChartCard
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title="Total CPU Usage"
|
title="Total CPU Usage"
|
||||||
description="Average system-wide CPU utilization"
|
description={`${
|
||||||
|
cpuMaxStore[0] && isLongerChart ? 'Max 1 min ' : 'Average'
|
||||||
|
} system-wide CPU utilization`}
|
||||||
|
cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null}
|
||||||
>
|
>
|
||||||
<CpuChart ticks={ticks} systemData={systemStats} />
|
<AreaChartDefault
|
||||||
|
ticks={ticks}
|
||||||
|
systemData={systemStats}
|
||||||
|
chartName="CPU Usage"
|
||||||
|
showMax={isLongerChart && cpuMaxStore[0]}
|
||||||
|
unit="%"
|
||||||
|
chartTime={chartTime}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && (
|
{containerFilterBar && (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title="Docker CPU Usage"
|
title="Docker CPU Usage"
|
||||||
description="CPU utilization of docker containers"
|
description="Average CPU utilization of containers"
|
||||||
isContainerChart={true}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
@@ -348,12 +393,12 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<MemChart ticks={ticks} systemData={systemStats} />
|
<MemChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && (
|
{containerFilterBar && (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title="Docker Memory Usage"
|
title="Docker Memory Usage"
|
||||||
description="Memory usage of docker containers"
|
description="Memory usage of docker containers"
|
||||||
isContainerChart={true}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
@@ -368,23 +413,37 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
<ChartCard grid={grid} title="Disk I/O" description="Throughput of root filesystem">
|
<ChartCard
|
||||||
<DiskIoChart
|
grid={grid}
|
||||||
|
title="Disk I/O"
|
||||||
|
description="Throughput of root filesystem"
|
||||||
|
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
|
||||||
|
>
|
||||||
|
<AreaChartDefault
|
||||||
ticks={ticks}
|
ticks={ticks}
|
||||||
systemData={systemStats}
|
systemData={systemStats}
|
||||||
dataKeys={['stats.dw', 'stats.dr']}
|
showMax={isLongerChart && diskIoMaxStore[0]}
|
||||||
|
chartName="dio"
|
||||||
|
chartTime={chartTime}
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
<ChartCard
|
<ChartCard
|
||||||
grid={grid}
|
grid={grid}
|
||||||
title="Bandwidth"
|
title="Bandwidth"
|
||||||
|
cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null}
|
||||||
description="Network traffic of public interfaces"
|
description="Network traffic of public interfaces"
|
||||||
>
|
>
|
||||||
<BandwidthChart ticks={ticks} systemData={systemStats} />
|
<AreaChartDefault
|
||||||
|
ticks={ticks}
|
||||||
|
systemData={systemStats}
|
||||||
|
showMax={isLongerChart && bandwidthMaxStore[0]}
|
||||||
|
chartName="bw"
|
||||||
|
chartTime={chartTime}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && dockerNetChartData.length > 0 && (
|
{containerFilterBar && dockerNetChartData.length > 0 && (
|
||||||
<div
|
<div
|
||||||
ref={netCardRef}
|
ref={netCardRef}
|
||||||
className={cn({
|
className={cn({
|
||||||
@@ -394,7 +453,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<ChartCard
|
<ChartCard
|
||||||
title="Docker Network I/O"
|
title="Docker Network I/O"
|
||||||
description="Includes traffic between internal services"
|
description="Includes traffic between internal services"
|
||||||
isContainerChart={true}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
|
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
@@ -436,11 +495,14 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
grid={grid}
|
grid={grid}
|
||||||
title={`${extraFsName} I/O`}
|
title={`${extraFsName} I/O`}
|
||||||
description={`Throughput of ${extraFsName}`}
|
description={`Throughput of ${extraFsName}`}
|
||||||
|
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
|
||||||
>
|
>
|
||||||
<DiskIoChart
|
<AreaChartDefault
|
||||||
ticks={ticks}
|
ticks={ticks}
|
||||||
systemData={systemStats}
|
systemData={systemStats}
|
||||||
dataKeys={[`stats.efs.${extraFsName}.w`, `stats.efs.${extraFsName}.r`]}
|
showMax={isLongerChart && diskIoMaxStore[0]}
|
||||||
|
chartName={`efs.${extraFsName}`}
|
||||||
|
chartTime={chartTime}
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -461,10 +523,10 @@ function ContainerFilterBar() {
|
|||||||
|
|
||||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
$containerFilter.set(e.target.value)
|
$containerFilter.set(e.target.value)
|
||||||
}, []) // Use an empty dependency array to prevent re-creation
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
|
<>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter..."
|
placeholder="Filter..."
|
||||||
className="pl-4 pr-8"
|
className="pl-4 pr-8"
|
||||||
@@ -483,7 +545,33 @@ function ContainerFilterBar() {
|
|||||||
<XIcon className="h-4 w-4" />
|
<XIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectAvgMax({
|
||||||
|
store,
|
||||||
|
}: {
|
||||||
|
store: [boolean, React.Dispatch<React.SetStateAction<boolean>>]
|
||||||
|
}) {
|
||||||
|
const [max, setMax] = store
|
||||||
|
const Icon = max ? ChartMax : ChartAverage
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select value={max ? 'max' : 'avg'} onValueChange={(e) => setMax(e === 'max')}>
|
||||||
|
<SelectTrigger className="relative pl-10 pr-5">
|
||||||
|
<Icon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" />
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem key="avg" value="avg">
|
||||||
|
Average
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem key="max" value="max">
|
||||||
|
Max 1 min
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,13 +580,13 @@ function ChartCard({
|
|||||||
description,
|
description,
|
||||||
children,
|
children,
|
||||||
grid,
|
grid,
|
||||||
isContainerChart,
|
cornerEl,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
grid?: boolean
|
grid?: boolean
|
||||||
isContainerChart?: boolean
|
cornerEl?: JSX.Element | null
|
||||||
}) {
|
}) {
|
||||||
const { isIntersecting, ref } = useIntersectionObserver()
|
const { isIntersecting, ref } = useIntersectionObserver()
|
||||||
|
|
||||||
@@ -510,12 +598,16 @@ function ChartCard({
|
|||||||
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
|
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
|
||||||
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
||||||
<CardDescription>{description}</CardDescription>
|
<CardDescription>{description}</CardDescription>
|
||||||
{isContainerChart && <ContainerFilterBar />}
|
{cornerEl && (
|
||||||
|
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
|
||||||
|
{cornerEl}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
<div className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
||||||
{<Spinner />}
|
{<Spinner />}
|
||||||
{isIntersecting && <Suspense>{children}</Suspense>}
|
{isIntersecting && <Suspense>{children}</Suspense>}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,77 +16,77 @@ export type ChartConfig = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartContextProps = {
|
// type ChartContextProps = {
|
||||||
config: ChartConfig
|
// config: ChartConfig
|
||||||
}
|
// }
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
// const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||||
|
|
||||||
function useChart() {
|
// function useChart() {
|
||||||
const context = React.useContext(ChartContext)
|
// const context = React.useContext(ChartContext)
|
||||||
|
|
||||||
if (!context) {
|
// if (!context) {
|
||||||
throw new Error('useChart must be used within a <ChartContainer />')
|
// throw new Error('useChart must be used within a <ChartContainer />')
|
||||||
}
|
// }
|
||||||
|
|
||||||
return context
|
// return context
|
||||||
}
|
// }
|
||||||
|
|
||||||
const ChartContainer = React.forwardRef<
|
const ChartContainer = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<'div'> & {
|
React.ComponentProps<'div'> & {
|
||||||
config: ChartConfig
|
// config: ChartConfig
|
||||||
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children']
|
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children']
|
||||||
}
|
}
|
||||||
>(({ id, className, children, config, ...props }, ref) => {
|
>(({ id, className, children, ...props }, ref) => {
|
||||||
const uniqueId = React.useId()
|
const uniqueId = React.useId()
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
|
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider value={{ config }}>
|
//<ChartContext.Provider value={{ config }}>
|
||||||
<div
|
<div
|
||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
"text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChartStyle id={chartId} config={config} />
|
{/* <ChartStyle id={chartId} config={config} /> */}
|
||||||
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
</ChartContext.Provider>
|
//</ChartContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
ChartContainer.displayName = 'Chart'
|
ChartContainer.displayName = 'Chart'
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
// const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
|
// const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
// if (!colorConfig.length) {
|
||||||
return null
|
// return null
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<style
|
// <style
|
||||||
dangerouslySetInnerHTML={{
|
// dangerouslySetInnerHTML={{
|
||||||
__html: Object.entries(THEMES).map(
|
// __html: Object.entries(THEMES).map(
|
||||||
([theme, prefix]) => `
|
// ([theme, prefix]) => `
|
||||||
${prefix} [data-chart=${id}] {
|
// ${prefix} [data-chart=${id}] {
|
||||||
${colorConfig
|
// ${colorConfig
|
||||||
.map(([key, itemConfig]) => {
|
// .map(([key, itemConfig]) => {
|
||||||
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
|
// const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
|
||||||
return color ? ` --color-${key}: ${color};` : null
|
// return color ? ` --color-${key}: ${color};` : null
|
||||||
})
|
// })
|
||||||
.join('\n')}
|
// .join('\n')}
|
||||||
}
|
// }
|
||||||
`
|
// `
|
||||||
),
|
// ),
|
||||||
}}
|
// }}
|
||||||
/>
|
// />
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||||
|
|
||||||
@@ -126,7 +126,8 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { config } = useChart()
|
// const { config } = useChart()
|
||||||
|
const config = {}
|
||||||
|
|
||||||
React.useMemo(() => {
|
React.useMemo(() => {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
@@ -146,10 +147,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
const [item] = payload
|
const [item] = payload
|
||||||
const key = `${labelKey || item.dataKey || item.name || 'value'}`
|
const key = `${labelKey || item.dataKey || item.name || 'value'}`
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
const value =
|
const value = !labelKey && typeof label === 'string' ? label : itemConfig?.label
|
||||||
!labelKey && typeof label === 'string'
|
|
||||||
? config[label as keyof typeof config]?.label || label
|
|
||||||
: itemConfig?.label
|
|
||||||
|
|
||||||
if (labelFormatter) {
|
if (labelFormatter) {
|
||||||
return (
|
return (
|
||||||
@@ -262,7 +260,7 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
hideIcon?: boolean
|
hideIcon?: boolean
|
||||||
nameKey?: string
|
nameKey?: string
|
||||||
}
|
}
|
||||||
>(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
|
>(({ className, payload, verticalAlign = 'bottom' }, ref) => {
|
||||||
// const { config } = useChart()
|
// const { config } = useChart()
|
||||||
|
|
||||||
if (!payload?.length) {
|
if (!payload?.length) {
|
||||||
@@ -342,5 +340,5 @@ export {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
ChartLegend,
|
ChartLegend,
|
||||||
ChartLegendContent,
|
ChartLegendContent,
|
||||||
ChartStyle,
|
// ChartStyle,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,3 +23,25 @@ export function Rows(props: SVGProps<SVGSVGElement>) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IconPark Apache License 2.0 https://github.com/bytedance/IconPark
|
||||||
|
export function ChartAverage(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" stroke="currentColor" {...props}>
|
||||||
|
<path strokeWidth="3" d="M4 4v40h40" />
|
||||||
|
<path strokeWidth="3" d="M10 38S15.3 4 27 4s17 34 17 34" />
|
||||||
|
<path strokeWidth="4" d="M10 24h34" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IconPark Apache License 2.0 https://github.com/bytedance/IconPark
|
||||||
|
export function ChartMax(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" stroke="currentColor" {...props}>
|
||||||
|
<path strokeWidth="3" d="M4 4v40h40" />
|
||||||
|
<path strokeWidth="3" d="M10 38S15.3 4 27 4s17 34 17 34" />
|
||||||
|
<path strokeWidth="4" d="M10 4h34" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ export function updateRecordList<T extends RecordModel>(
|
|||||||
$store.set(newRecords)
|
$store.set(newRecords)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPbTimestamp(timeString: ChartTimes) {
|
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
|
||||||
const d = chartTimeData[timeString].getOffset(new Date())
|
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,7 +204,10 @@ export function useYAxisWidth() {
|
|||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
document.body.appendChild(div)
|
document.body.appendChild(div)
|
||||||
setYAxisWidth(div.offsetWidth + 24)
|
const width = div.offsetWidth + 24
|
||||||
|
if (width > yAxisWidth) {
|
||||||
|
setYAxisWidth(div.offsetWidth + 24)
|
||||||
|
}
|
||||||
document.body.removeChild(div)
|
document.body.removeChild(div)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -285,3 +288,5 @@ export const getSizeUnit = (n: number) => (n >= 1_000 ? ' TB' : ' GB')
|
|||||||
* @returns value in GB if less than 1000, otherwise value in TB
|
* @returns value in GB if less than 1000, otherwise value in TB
|
||||||
*/
|
*/
|
||||||
export const getSizeVal = (n: number) => (n >= 1_000 ? n / 1_000 : n)
|
export const getSizeVal = (n: number) => (n >= 1_000 ? n / 1_000 : n)
|
||||||
|
|
||||||
|
export const chartMargin = { top: 12 }
|
||||||
|
|||||||
14
beszel/site/src/types.d.ts
vendored
14
beszel/site/src/types.d.ts
vendored
@@ -35,6 +35,8 @@ 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) */
|
||||||
@@ -59,10 +61,18 @@ export interface SystemStats {
|
|||||||
dr: number
|
dr: number
|
||||||
/** disk write (mb) */
|
/** disk write (mb) */
|
||||||
dw: number
|
dw: number
|
||||||
|
/** max disk read (mb) */
|
||||||
|
drm?: number
|
||||||
|
/** max disk write (mb) */
|
||||||
|
dwm?: number
|
||||||
/** network sent (mb) */
|
/** network sent (mb) */
|
||||||
ns: number
|
ns: number
|
||||||
/** 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 */
|
||||||
@@ -78,6 +88,10 @@ export interface ExtraFsStats {
|
|||||||
r: number
|
r: number
|
||||||
/** total write (mb) */
|
/** total write (mb) */
|
||||||
w: number
|
w: number
|
||||||
|
/** max read (mb) */
|
||||||
|
rm: number
|
||||||
|
/** max write (mb) */
|
||||||
|
wm: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerStatsRecord extends RecordModel {
|
export interface ContainerStatsRecord extends RecordModel {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package beszel
|
package beszel
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version = "0.5.2"
|
Version = "0.5.3"
|
||||||
AppName = "beszel"
|
AppName = "beszel"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user