mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 05:36:15 +01:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9fb9b856f | ||
|
|
66bca11d36 | ||
|
|
86e87f0d47 | ||
|
|
fadfc5d81d | ||
|
|
fc39ff1e4d | ||
|
|
82ccfc66e0 | ||
|
|
890bad1c39 | ||
|
|
9c458885f1 | ||
|
|
d2aed0dc72 | ||
|
|
3dbcb5d7da | ||
|
|
57a1a8b39e | ||
|
|
ab81c04569 |
6
.github/workflows/docker-images.yml
vendored
6
.github/workflows/docker-images.yml
vendored
@@ -93,7 +93,9 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: github.event_name != 'pull_request'
|
env:
|
||||||
|
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
|
||||||
|
if: github.event_name != 'pull_request' && env.password_secret_exists == 'true'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ matrix.username || secrets[matrix.username_secret] }}
|
username: ${{ matrix.username || secrets[matrix.username_secret] }}
|
||||||
@@ -108,6 +110,6 @@ jobs:
|
|||||||
context: "${{ matrix.context }}"
|
context: "${{ matrix.context }}"
|
||||||
file: ${{ matrix.dockerfile }}
|
file: ${{ matrix.dockerfile }}
|
||||||
platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
|
platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
|
||||||
push: ${{ github.ref_type == 'tag' }}
|
push: ${{ github.ref_type == 'tag' && secrets[matrix.password_secret] != '' }}
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
labels: ${{ steps.metadata.outputs.labels }}
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
|
|||||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -51,3 +51,4 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
|
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
IS_FORK: ${{ github.repository_owner != 'henrygd' }}
|
||||||
|
|||||||
@@ -38,12 +38,25 @@ builds:
|
|||||||
- mips64
|
- mips64
|
||||||
- riscv64
|
- riscv64
|
||||||
- mipsle
|
- mipsle
|
||||||
|
- mips
|
||||||
- ppc64le
|
- ppc64le
|
||||||
|
gomips:
|
||||||
|
- hardfloat
|
||||||
|
- softfloat
|
||||||
ignore:
|
ignore:
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: openbsd
|
- goos: openbsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
|
- goos: linux
|
||||||
|
goarch: mips64
|
||||||
|
gomips: softfloat
|
||||||
|
- goos: linux
|
||||||
|
goarch: mipsle
|
||||||
|
gomips: hardfloat
|
||||||
|
- goos: linux
|
||||||
|
goarch: mips
|
||||||
|
gomips: hardfloat
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
@@ -54,7 +67,7 @@ builds:
|
|||||||
archives:
|
archives:
|
||||||
- id: beszel-agent
|
- id: beszel-agent
|
||||||
formats: [tar.gz]
|
formats: [tar.gz]
|
||||||
builds:
|
ids:
|
||||||
- beszel-agent
|
- beszel-agent
|
||||||
name_template: >-
|
name_template: >-
|
||||||
{{ .Binary }}_
|
{{ .Binary }}_
|
||||||
@@ -66,7 +79,7 @@ archives:
|
|||||||
|
|
||||||
- id: beszel
|
- id: beszel
|
||||||
formats: [tar.gz]
|
formats: [tar.gz]
|
||||||
builds:
|
ids:
|
||||||
- beszel
|
- beszel
|
||||||
name_template: >-
|
name_template: >-
|
||||||
{{ .Binary }}_
|
{{ .Binary }}_
|
||||||
@@ -85,7 +98,7 @@ nfpms:
|
|||||||
API access.
|
API access.
|
||||||
maintainer: henrygd <hank@henrygd.me>
|
maintainer: henrygd <hank@henrygd.me>
|
||||||
section: net
|
section: net
|
||||||
builds:
|
ids:
|
||||||
- beszel-agent
|
- beszel-agent
|
||||||
formats:
|
formats:
|
||||||
- deb
|
- deb
|
||||||
@@ -122,6 +135,7 @@ scoops:
|
|||||||
homepage: "https://beszel.dev"
|
homepage: "https://beszel.dev"
|
||||||
description: "Agent for Beszel, a lightweight server monitoring platform."
|
description: "Agent for Beszel, a lightweight server monitoring platform."
|
||||||
license: MIT
|
license: MIT
|
||||||
|
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
|
||||||
|
|
||||||
# # Needs choco installed, so doesn't build on linux / default gh workflow :(
|
# # Needs choco installed, so doesn't build on linux / default gh workflow :(
|
||||||
# chocolateys:
|
# chocolateys:
|
||||||
@@ -155,7 +169,7 @@ brews:
|
|||||||
homepage: "https://beszel.dev"
|
homepage: "https://beszel.dev"
|
||||||
description: "Agent for Beszel, a lightweight server monitoring platform."
|
description: "Agent for Beszel, a lightweight server monitoring platform."
|
||||||
license: MIT
|
license: MIT
|
||||||
skip_upload: auto
|
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
|
||||||
extra_install: |
|
extra_install: |
|
||||||
(bin/"beszel-agent-launcher").write <<~EOS
|
(bin/"beszel-agent-launcher").write <<~EOS
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
@@ -187,7 +201,7 @@ winget:
|
|||||||
release_notes_url: "https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}"
|
release_notes_url: "https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}"
|
||||||
publisher_support_url: "https://github.com/henrygd/beszel/issues"
|
publisher_support_url: "https://github.com/henrygd/beszel/issues"
|
||||||
short_description: "Agent for Beszel, a lightweight server monitoring platform."
|
short_description: "Agent for Beszel, a lightweight server monitoring platform."
|
||||||
skip_upload: auto
|
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
|
||||||
description: |
|
description: |
|
||||||
Beszel is a lightweight server monitoring platform that includes Docker
|
Beszel is a lightweight server monitoring platform that includes Docker
|
||||||
statistics, historical data, and alert functions. It has a friendly web
|
statistics, historical data, and alert functions. It has a friendly web
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ SKIP_WEB ?= false
|
|||||||
# Set executable extension based on target OS
|
# Set executable extension based on target OS
|
||||||
EXE_EXT := $(if $(filter windows,$(OS)),.exe,)
|
EXE_EXT := $(if $(filter windows,$(OS)),.exe,)
|
||||||
|
|
||||||
.PHONY: tidy build-agent build-hub build clean lint dev-server dev-agent dev-hub dev generate-locales
|
.PHONY: tidy build-agent build-hub build-hub-dev build clean lint dev-server dev-agent dev-hub dev generate-locales
|
||||||
.DEFAULT_GOAL := build
|
.DEFAULT_GOAL := build
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@@ -53,6 +53,10 @@ build-agent: tidy build-dotnet-conditional
|
|||||||
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
|
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
|
||||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub
|
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub
|
||||||
|
|
||||||
|
build-hub-dev: tidy
|
||||||
|
mkdir -p ./site/dist && touch ./site/dist/index.html
|
||||||
|
GOOS=$(OS) GOARCH=$(ARCH) go build -tags development -o ./build/beszel-dev_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub
|
||||||
|
|
||||||
build: build-agent build-hub
|
build: build-agent build-hub
|
||||||
|
|
||||||
generate-locales:
|
generate-locales:
|
||||||
@@ -73,9 +77,9 @@ dev-hub: export ENV=dev
|
|||||||
dev-hub:
|
dev-hub:
|
||||||
mkdir -p ./site/dist && touch ./site/dist/index.html
|
mkdir -p ./site/dist && touch ./site/dist/index.html
|
||||||
@if command -v entr >/dev/null 2>&1; then \
|
@if command -v entr >/dev/null 2>&1; then \
|
||||||
find ./cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./cmd/hub && go run . serve --http 0.0.0.0:8090"; \
|
find ./cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
|
||||||
else \
|
else \
|
||||||
cd ./cmd/hub && go run . serve --http 0.0.0.0:8090; \
|
cd ./cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dev-agent:
|
dev-agent:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ require (
|
|||||||
github.com/shirou/gopsutil/v4 v4.25.6
|
github.com/shirou/gopsutil/v4 v4.25.6
|
||||||
github.com/spf13/cast v1.9.2
|
github.com/spf13/cast v1.9.2
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
|
github.com/spf13/pflag v1.0.7
|
||||||
github.com/stretchr/testify v1.11.0
|
github.com/stretchr/testify v1.11.0
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.41.0
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
|
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
|
||||||
@@ -49,7 +50,6 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/spf13/pflag v1.0.7 // indirect
|
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
|||||||
@@ -8,13 +8,10 @@ import (
|
|||||||
"beszel/internal/hub/systems"
|
"beszel/internal/hub/systems"
|
||||||
"beszel/internal/records"
|
"beszel/internal/records"
|
||||||
"beszel/internal/users"
|
"beszel/internal/users"
|
||||||
"beszel/site"
|
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -115,6 +112,8 @@ func (h *Hub) initialize(e *core.ServeEvent) error {
|
|||||||
// set URL if BASE_URL env is set
|
// set URL if BASE_URL env is set
|
||||||
if h.appURL != "" {
|
if h.appURL != "" {
|
||||||
settings.Meta.AppURL = h.appURL
|
settings.Meta.AppURL = h.appURL
|
||||||
|
} else {
|
||||||
|
h.appURL = settings.Meta.AppURL
|
||||||
}
|
}
|
||||||
if err := e.App.Save(settings); err != nil {
|
if err := e.App.Save(settings); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -164,55 +163,6 @@ func (h *Hub) initialize(e *core.ServeEvent) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// startServer sets up the server for Beszel
|
|
||||||
func (h *Hub) startServer(se *core.ServeEvent) error {
|
|
||||||
// TODO: exclude dev server from production binary
|
|
||||||
switch h.IsDev() {
|
|
||||||
case true:
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: "localhost:5173",
|
|
||||||
})
|
|
||||||
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
|
|
||||||
proxy.ServeHTTP(e.Response, e.Request)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
// parse app url
|
|
||||||
parsedURL, err := url.Parse(h.appURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// fix base paths in html if using subpath
|
|
||||||
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
|
|
||||||
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
|
|
||||||
indexContent := strings.ReplaceAll(string(indexFile), "./", basePath)
|
|
||||||
indexContent = strings.Replace(indexContent, "{{V}}", beszel.Version, 1)
|
|
||||||
indexContent = strings.Replace(indexContent, "{{HUB_URL}}", h.appURL, 1)
|
|
||||||
// set up static asset serving
|
|
||||||
staticPaths := [2]string{"/static/", "/assets/"}
|
|
||||||
serveStatic := apis.Static(site.DistDirFS, false)
|
|
||||||
// get CSP configuration
|
|
||||||
csp, cspExists := GetEnv("CSP")
|
|
||||||
// add route
|
|
||||||
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
|
|
||||||
// serve static assets if path is in staticPaths
|
|
||||||
for i := range staticPaths {
|
|
||||||
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
|
|
||||||
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
|
|
||||||
return serveStatic(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cspExists {
|
|
||||||
e.Response.Header().Del("X-Frame-Options")
|
|
||||||
e.Response.Header().Set("Content-Security-Policy", csp)
|
|
||||||
}
|
|
||||||
return e.HTML(http.StatusOK, indexContent)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerCronJobs sets up scheduled tasks
|
// registerCronJobs sets up scheduled tasks
|
||||||
func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
|
func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
|
||||||
// delete old system_stats and alerts_history records once every hour
|
// delete old system_stats and alerts_history records once every hour
|
||||||
|
|||||||
79
beszel/internal/hub/server_development.go
Normal file
79
beszel/internal/hub/server_development.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
//go:build development
|
||||||
|
|
||||||
|
package hub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wraps http.RoundTripper to modify dev proxy HTML responses
|
||||||
|
type responseModifier struct {
|
||||||
|
transport http.RoundTripper
|
||||||
|
hub *Hub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *responseModifier) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
resp, err := rm.transport.RoundTrip(req)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
// Only modify HTML responses
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
if !strings.Contains(contentType, "text/html") {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
// Create a new response with the modified body
|
||||||
|
modifiedBody := rm.modifyHTML(string(body))
|
||||||
|
resp.Body = io.NopCloser(strings.NewReader(modifiedBody))
|
||||||
|
resp.ContentLength = int64(len(modifiedBody))
|
||||||
|
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(modifiedBody)))
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *responseModifier) modifyHTML(html string) string {
|
||||||
|
parsedURL, err := url.Parse(rm.hub.appURL)
|
||||||
|
if err != nil {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
// fix base paths in html if using subpath
|
||||||
|
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
|
||||||
|
html = strings.ReplaceAll(html, "./", basePath)
|
||||||
|
html = strings.Replace(html, "{{V}}", beszel.Version, 1)
|
||||||
|
html = strings.Replace(html, "{{HUB_URL}}", rm.hub.appURL, 1)
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
// startServer sets up the development server for Beszel
|
||||||
|
func (h *Hub) startServer(se *core.ServeEvent) error {
|
||||||
|
slog.Info("starting server", "appURL", h.appURL)
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: "localhost:5173",
|
||||||
|
})
|
||||||
|
|
||||||
|
proxy.Transport = &responseModifier{
|
||||||
|
transport: http.DefaultTransport,
|
||||||
|
hub: h,
|
||||||
|
}
|
||||||
|
|
||||||
|
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
|
||||||
|
proxy.ServeHTTP(e.Response, e.Request)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
51
beszel/internal/hub/server_production.go
Normal file
51
beszel/internal/hub/server_production.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
//go:build !development
|
||||||
|
|
||||||
|
package hub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel"
|
||||||
|
"beszel/site"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// startServer sets up the production server for Beszel
|
||||||
|
func (h *Hub) startServer(se *core.ServeEvent) error {
|
||||||
|
// parse app url
|
||||||
|
parsedURL, err := url.Parse(h.appURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// fix base paths in html if using subpath
|
||||||
|
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
|
||||||
|
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
|
||||||
|
html := strings.ReplaceAll(string(indexFile), "./", basePath)
|
||||||
|
html = strings.Replace(html, "{{V}}", beszel.Version, 1)
|
||||||
|
html = strings.Replace(html, "{{HUB_URL}}", h.appURL, 1)
|
||||||
|
// set up static asset serving
|
||||||
|
staticPaths := [2]string{"/static/", "/assets/"}
|
||||||
|
serveStatic := apis.Static(site.DistDirFS, false)
|
||||||
|
// get CSP configuration
|
||||||
|
csp, cspExists := GetEnv("CSP")
|
||||||
|
// add route
|
||||||
|
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
|
||||||
|
// serve static assets if path is in staticPaths
|
||||||
|
for i := range staticPaths {
|
||||||
|
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
|
||||||
|
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
|
||||||
|
return serveStatic(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cspExists {
|
||||||
|
e.Response.Header().Del("X-Frame-Options")
|
||||||
|
e.Response.Header().Set("Content-Security-Policy", csp)
|
||||||
|
}
|
||||||
|
return e.HTML(http.StatusOK, html)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "0.12.6",
|
"version": "0.12.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host",
|
||||||
"build": "lingui extract --overwrite && lingui compile && vite build",
|
"build": "lingui extract --overwrite && lingui compile && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"sync": "lingui extract --overwrite && lingui compile",
|
"sync": "lingui extract --overwrite && lingui compile",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { $publicKey } from "@/lib/stores"
|
import { $publicKey } from "@/lib/stores"
|
||||||
import { cn, generateToken, tokenMap, useLocalStorage } from "@/lib/utils"
|
import { cn, generateToken, tokenMap, useBrowserStorage } from "@/lib/utils"
|
||||||
import { pb, isReadOnlyUser } from "@/lib/api"
|
import { pb, isReadOnlyUser } from "@/lib/api"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
|
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
|
||||||
@@ -77,7 +77,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
|
|||||||
const port = useRef<HTMLInputElement>(null)
|
const port = useRef<HTMLInputElement>(null)
|
||||||
const [hostValue, setHostValue] = useState(system?.host ?? "")
|
const [hostValue, setHostValue] = useState(system?.host ?? "")
|
||||||
const isUnixSocket = hostValue.startsWith("/")
|
const isUnixSocket = hostValue.startsWith("/")
|
||||||
const [tab, setTab] = useLocalStorage("as-tab", "docker")
|
const [tab, setTab] = useBrowserStorage("as-tab", "docker")
|
||||||
const [token, setToken] = useState(system?.token ?? "")
|
const [token, setToken] = useState(system?.token ?? "")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||||
import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils"
|
import { cn, formatShortDate, chartMargin } from "@/lib/utils"
|
||||||
|
import { useYAxisWidth } from "./hooks"
|
||||||
import { ChartData, SystemStatsRecord } from "@/types"
|
import { ChartData, SystemStatsRecord } from "@/types"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||||
import { memo, useMemo } from "react"
|
import { memo, useMemo } from "react"
|
||||||
import { useYAxisWidth, cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
|
import { cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { $containerFilter, $userSettings } from "@/lib/stores"
|
import { $containerFilter, $userSettings } from "@/lib/stores"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { Separator } from "../ui/separator"
|
import { Separator } from "../ui/separator"
|
||||||
import { ChartType, Unit } from "@/lib/enums"
|
import { ChartType, Unit } from "@/lib/enums"
|
||||||
|
import { useYAxisWidth } from "./hooks"
|
||||||
|
|
||||||
export default memo(function ContainerChart({
|
export default memo(function ContainerChart({
|
||||||
dataKey,
|
dataKey,
|
||||||
chartData,
|
chartData,
|
||||||
chartType,
|
chartType,
|
||||||
|
chartConfig,
|
||||||
unit = "%",
|
unit = "%",
|
||||||
}: {
|
}: {
|
||||||
dataKey: string
|
dataKey: string
|
||||||
chartData: ChartData
|
chartData: ChartData
|
||||||
chartType: ChartType
|
chartType: ChartType
|
||||||
|
chartConfig: ChartConfig
|
||||||
unit?: string
|
unit?: string
|
||||||
}) {
|
}) {
|
||||||
const filter = useStore($containerFilter)
|
const filter = useStore($containerFilter)
|
||||||
@@ -28,40 +31,6 @@ export default memo(function ContainerChart({
|
|||||||
|
|
||||||
const isNetChart = chartType === ChartType.Network
|
const isNetChart = chartType === ChartType.Network
|
||||||
|
|
||||||
const chartConfig = useMemo(() => {
|
|
||||||
const config = {} as Record<string, { label: string; color: string }>
|
|
||||||
const totalUsage = new Map<string, number>()
|
|
||||||
|
|
||||||
// calculate total usage of each container
|
|
||||||
for (const stats of containerData) {
|
|
||||||
for (const key in stats) {
|
|
||||||
if (!key || key === "created") continue
|
|
||||||
|
|
||||||
const currentTotal = totalUsage.get(key) ?? 0
|
|
||||||
const increment = isNetChart
|
|
||||||
? (stats[key]?.nr ?? 0) + (stats[key]?.ns ?? 0)
|
|
||||||
: // @ts-ignore
|
|
||||||
stats[key]?.[dataKey] ?? 0
|
|
||||||
|
|
||||||
totalUsage.set(key, currentTotal + increment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort keys and generate colors based on usage
|
|
||||||
const sortedEntries = Array.from(totalUsage.entries()).sort(([, a], [, b]) => b - a)
|
|
||||||
|
|
||||||
const length = sortedEntries.length
|
|
||||||
sortedEntries.forEach(([key], i) => {
|
|
||||||
const hue = ((i * 360) / length) % 360
|
|
||||||
config[key] = {
|
|
||||||
label: key,
|
|
||||||
color: `hsl(${hue}, 60%, 55%)`,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return config satisfies ChartConfig
|
|
||||||
}, [chartData])
|
|
||||||
|
|
||||||
const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {
|
const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {
|
||||||
const obj = {} as {
|
const obj = {} as {
|
||||||
toolTipFormatter: (item: any, key: string) => React.ReactNode | string
|
toolTipFormatter: (item: any, key: string) => React.ReactNode | string
|
||||||
@@ -119,7 +88,14 @@ export default memo(function ContainerChart({
|
|||||||
return obj
|
return obj
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const filterLower = filter?.toLowerCase()
|
// Filter with set lookup
|
||||||
|
const filteredKeys = useMemo(() => {
|
||||||
|
if (!filter) {
|
||||||
|
return new Set<string>()
|
||||||
|
}
|
||||||
|
const filterLower = filter.toLowerCase()
|
||||||
|
return new Set(Object.keys(chartConfig).filter((key) => !key.toLowerCase().includes(filterLower)))
|
||||||
|
}, [chartConfig, filter])
|
||||||
|
|
||||||
// console.log('rendered at', new Date())
|
// console.log('rendered at', new Date())
|
||||||
|
|
||||||
@@ -162,9 +138,9 @@ export default memo(function ContainerChart({
|
|||||||
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />}
|
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />}
|
||||||
/>
|
/>
|
||||||
{Object.keys(chartConfig).map((key) => {
|
{Object.keys(chartConfig).map((key) => {
|
||||||
const filtered = filterLower && !key.toLowerCase().includes(filterLower)
|
const filtered = filteredKeys.has(key)
|
||||||
let fillOpacity = filtered ? 0.05 : 0.4
|
const fillOpacity = filtered ? 0.05 : 0.4
|
||||||
let strokeOpacity = filtered ? 0.1 : 1
|
const strokeOpacity = filtered ? 0.1 : 1
|
||||||
return (
|
return (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||||
import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
import { cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { useLingui } from "@lingui/react/macro"
|
import { useLingui } from "@lingui/react/macro"
|
||||||
import { Unit } from "@/lib/enums"
|
import { Unit } from "@/lib/enums"
|
||||||
|
import { useYAxisWidth } from "./hooks"
|
||||||
|
|
||||||
export default memo(function DiskChart({
|
export default memo(function DiskChart({
|
||||||
dataKey,
|
dataKey,
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
xAxis,
|
xAxis,
|
||||||
} from "@/components/ui/chart"
|
} from "@/components/ui/chart"
|
||||||
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
|
import { cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { memo, useMemo } from "react"
|
import { memo, useMemo } from "react"
|
||||||
|
import { useYAxisWidth } from "./hooks"
|
||||||
|
|
||||||
export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData }) {
|
export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData }) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|||||||
107
beszel/site/src/components/charts/hooks.ts
Normal file
107
beszel/site/src/components/charts/hooks.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { ChartConfig } from "@/components/ui/chart"
|
||||||
|
import { ChartData } from "@/types"
|
||||||
|
|
||||||
|
/** Chart configurations for CPU, memory, and network usage charts */
|
||||||
|
export interface ContainerChartConfigs {
|
||||||
|
cpu: ChartConfig
|
||||||
|
memory: ChartConfig
|
||||||
|
network: ChartConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates chart configurations for container metrics visualization
|
||||||
|
* @param containerData - Array of container statistics data points
|
||||||
|
* @returns Chart configurations for CPU, memory, and network metrics
|
||||||
|
*/
|
||||||
|
export function useContainerChartConfigs(containerData: ChartData["containerData"]): ContainerChartConfigs {
|
||||||
|
return useMemo(() => {
|
||||||
|
const configs = {
|
||||||
|
cpu: {} as ChartConfig,
|
||||||
|
memory: {} as ChartConfig,
|
||||||
|
network: {} as ChartConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate usage metrics for each container
|
||||||
|
const totalUsage = {
|
||||||
|
cpu: new Map<string, number>(),
|
||||||
|
memory: new Map<string, number>(),
|
||||||
|
network: new Map<string, number>(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each data point to calculate totals
|
||||||
|
for (let i = 0; i < containerData.length; i++) {
|
||||||
|
const stats = containerData[i]
|
||||||
|
const containerNames = Object.keys(stats)
|
||||||
|
|
||||||
|
for (let j = 0; j < containerNames.length; j++) {
|
||||||
|
const containerName = containerNames[j]
|
||||||
|
// Skip metadata field
|
||||||
|
if (containerName === "created") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerStats = stats[containerName]
|
||||||
|
if (!containerStats) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accumulate metrics for CPU, memory, and network
|
||||||
|
const currentCpu = totalUsage.cpu.get(containerName) ?? 0
|
||||||
|
const currentMemory = totalUsage.memory.get(containerName) ?? 0
|
||||||
|
const currentNetwork = totalUsage.network.get(containerName) ?? 0
|
||||||
|
|
||||||
|
totalUsage.cpu.set(containerName, currentCpu + (containerStats.c ?? 0))
|
||||||
|
totalUsage.memory.set(containerName, currentMemory + (containerStats.m ?? 0))
|
||||||
|
totalUsage.network.set(containerName, currentNetwork + (containerStats.nr ?? 0) + (containerStats.ns ?? 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate chart configurations for each metric type
|
||||||
|
Object.entries(totalUsage).forEach(([chartType, usageMap]) => {
|
||||||
|
const sortedContainers = Array.from(usageMap.entries()).sort(([, a], [, b]) => b - a)
|
||||||
|
const chartConfig = {} as Record<string, { label: string; color: string }>
|
||||||
|
const count = sortedContainers.length
|
||||||
|
|
||||||
|
// Generate colors for each container
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const [containerName] = sortedContainers[i]
|
||||||
|
const hue = ((i * 360) / count) % 360
|
||||||
|
chartConfig[containerName] = {
|
||||||
|
label: containerName,
|
||||||
|
color: `hsl(${hue}, 60%, 55%)`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configs[chartType as keyof typeof configs] = chartConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
return configs
|
||||||
|
}, [containerData])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the correct width of the y axis in recharts based on the longest label */
|
||||||
|
export function useYAxisWidth() {
|
||||||
|
const [yAxisWidth, setYAxisWidth] = useState(0)
|
||||||
|
let maxChars = 0
|
||||||
|
let timeout: ReturnType<typeof setTimeout>
|
||||||
|
function updateYAxisWidth(str: string) {
|
||||||
|
if (str.length > maxChars) {
|
||||||
|
maxChars = str.length
|
||||||
|
const div = document.createElement("div")
|
||||||
|
div.className = "text-xs tabular-nums tracking-tighter table sr-only"
|
||||||
|
div.innerHTML = str
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
document.body.appendChild(div)
|
||||||
|
const width = div.offsetWidth + 24
|
||||||
|
if (width > yAxisWidth) {
|
||||||
|
setYAxisWidth(div.offsetWidth + 24)
|
||||||
|
}
|
||||||
|
document.body.removeChild(div)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return { yAxisWidth, updateYAxisWidth }
|
||||||
|
}
|
||||||
@@ -8,10 +8,11 @@ import {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
xAxis,
|
xAxis,
|
||||||
} from "@/components/ui/chart"
|
} from "@/components/ui/chart"
|
||||||
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
|
import { cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
|
||||||
import { ChartData, SystemStats } from "@/types"
|
import { ChartData, SystemStats } from "@/types"
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { t } from "@lingui/core/macro"
|
import { t } from "@lingui/core/macro"
|
||||||
|
import { useYAxisWidth } from "./hooks"
|
||||||
|
|
||||||
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
|
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||||
import { useYAxisWidth, cn, decimalString, formatShortDate, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
import { cn, decimalString, formatShortDate, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { useLingui } from "@lingui/react/macro"
|
import { useLingui } from "@lingui/react/macro"
|
||||||
import { Unit } from "@/lib/enums"
|
import { Unit } from "@/lib/enums"
|
||||||
|
import { useYAxisWidth } from "./hooks"
|
||||||
|
|
||||||
export default memo(function MemChart({ chartData, showMax }: { chartData: ChartData; showMax: boolean }) {
|
export default memo(function MemChart({ chartData, showMax }: { chartData: ChartData; showMax: boolean }) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { t } from "@lingui/core/macro"
|
|||||||
|
|
||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||||
import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
import { cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { $userSettings } from "@/lib/stores"
|
import { $userSettings } from "@/lib/stores"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
|
import { useYAxisWidth } from "./hooks"
|
||||||
|
|
||||||
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {
|
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|||||||
@@ -8,19 +8,12 @@ import {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
xAxis,
|
xAxis,
|
||||||
} from "@/components/ui/chart"
|
} from "@/components/ui/chart"
|
||||||
import {
|
import { cn, formatShortDate, toFixedFloat, chartMargin, formatTemperature, decimalString } from "@/lib/utils"
|
||||||
useYAxisWidth,
|
|
||||||
cn,
|
|
||||||
formatShortDate,
|
|
||||||
toFixedFloat,
|
|
||||||
chartMargin,
|
|
||||||
formatTemperature,
|
|
||||||
decimalString,
|
|
||||||
} from "@/lib/utils"
|
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { memo, useMemo } from "react"
|
import { memo, useMemo } from "react"
|
||||||
import { $temperatureFilter, $userSettings } from "@/lib/stores"
|
import { $temperatureFilter, $userSettings } from "@/lib/stores"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
|
import { useYAxisWidth } from "./hooks"
|
||||||
|
|
||||||
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
|
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
|
||||||
const filter = useStore($temperatureFilter)
|
const filter = useStore($temperatureFilter)
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { Suspense, memo, useEffect, useMemo } from "react"
|
import { Suspense, memo, useEffect, useMemo } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
|
||||||
import { $alerts, $systems } from "@/lib/stores"
|
import { $alerts, $allSystemsById } from "@/lib/stores"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { GithubIcon } from "lucide-react"
|
import { GithubIcon } from "lucide-react"
|
||||||
import { Separator } from "../ui/separator"
|
import { Separator } from "../ui/separator"
|
||||||
import { getSystemNameFromId } from "@/lib/utils"
|
import { AlertRecord } from "@/types"
|
||||||
import { pb, updateRecordList, updateSystemList } from "@/lib/api"
|
|
||||||
import { AlertRecord, SystemRecord } from "@/types"
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||||
import { $router, Link } from "../router"
|
import { $router, Link } from "../router"
|
||||||
import { Plural, Trans, useLingui } from "@lingui/react/macro"
|
import { Plural, Trans, useLingui } from "@lingui/react/macro"
|
||||||
@@ -14,8 +12,6 @@ import { getPagePath } from "@nanostores/router"
|
|||||||
import { alertInfo } from "@/lib/alerts"
|
import { alertInfo } from "@/lib/alerts"
|
||||||
import SystemsTable from "@/components/systems-table/systems-table"
|
import SystemsTable from "@/components/systems-table/systems-table"
|
||||||
|
|
||||||
// const SystemsTable = lazy(() => import("../systems-table/systems-table"))
|
|
||||||
|
|
||||||
export default memo(function () {
|
export default memo(function () {
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
|
|
||||||
@@ -23,19 +19,6 @@ export default memo(function () {
|
|||||||
document.title = t`Dashboard` + " / Beszel"
|
document.title = t`Dashboard` + " / Beszel"
|
||||||
}, [t])
|
}, [t])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// make sure we have the latest list of systems
|
|
||||||
updateSystemList()
|
|
||||||
|
|
||||||
// subscribe to real time updates for systems / alerts
|
|
||||||
pb.collection<SystemRecord>("systems").subscribe("*", (e) => {
|
|
||||||
updateRecordList(e, $systems)
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
pb.collection("systems").unsubscribe("*")
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => (
|
() => (
|
||||||
<>
|
<>
|
||||||
@@ -69,6 +52,7 @@ export default memo(function () {
|
|||||||
|
|
||||||
const ActiveAlerts = () => {
|
const ActiveAlerts = () => {
|
||||||
const alerts = useStore($alerts)
|
const alerts = useStore($alerts)
|
||||||
|
const systems = useStore($allSystemsById)
|
||||||
|
|
||||||
const { activeAlerts, alertsKey } = useMemo(() => {
|
const { activeAlerts, alertsKey } = useMemo(() => {
|
||||||
const activeAlerts: AlertRecord[] = []
|
const activeAlerts: AlertRecord[] = []
|
||||||
@@ -112,7 +96,7 @@ const ActiveAlerts = () => {
|
|||||||
>
|
>
|
||||||
<info.icon className="h-4 w-4" />
|
<info.icon className="h-4 w-4" />
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
{getSystemNameFromId(alert.system)} {info.name().toLowerCase().replace("cpu", "CPU")}
|
{systems[alert.system]?.name} {info.name().toLowerCase().replace("cpu", "CPU")}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{alert.name === "Status" ? (
|
{alert.name === "Status" ? (
|
||||||
@@ -125,7 +109,7 @@ const ActiveAlerts = () => {
|
|||||||
)}
|
)}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
<Link
|
<Link
|
||||||
href={getPagePath($router, "system", { name: getSystemNameFromId(alert.system) })}
|
href={getPagePath($router, "system", { name: systems[alert.system]?.name })}
|
||||||
className="absolute inset-0 w-full h-full"
|
className="absolute inset-0 w-full h-full"
|
||||||
aria-label="View system"
|
aria-label="View system"
|
||||||
></Link>
|
></Link>
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import {
|
|||||||
$direction,
|
$direction,
|
||||||
$maxValues,
|
$maxValues,
|
||||||
$temperatureFilter,
|
$temperatureFilter,
|
||||||
|
$allSystemsByName,
|
||||||
} from "@/lib/stores"
|
} from "@/lib/stores"
|
||||||
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
|
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
|
||||||
|
import { useContainerChartConfigs } from "@/components/charts/hooks"
|
||||||
import { ChartType, Unit, Os, SystemStatus } from "@/lib/enums"
|
import { ChartType, Unit, Os, SystemStatus } from "@/lib/enums"
|
||||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react"
|
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react"
|
||||||
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
||||||
@@ -26,7 +28,7 @@ import {
|
|||||||
listen,
|
listen,
|
||||||
parseSemVer,
|
parseSemVer,
|
||||||
toFixedFloat,
|
toFixedFloat,
|
||||||
useLocalStorage,
|
useBrowserStorage,
|
||||||
} from "@/lib/utils"
|
} from "@/lib/utils"
|
||||||
import { getPbTimestamp, pb } from "@/lib/api"
|
import { getPbTimestamp, pb } from "@/lib/api"
|
||||||
import { Separator } from "../ui/separator"
|
import { Separator } from "../ui/separator"
|
||||||
@@ -49,6 +51,7 @@ import SwapChart from "@/components/charts/swap-chart"
|
|||||||
import TemperatureChart from "@/components/charts/temperature-chart"
|
import TemperatureChart from "@/components/charts/temperature-chart"
|
||||||
import GpuPowerChart from "@/components/charts/gpu-power-chart"
|
import GpuPowerChart from "@/components/charts/gpu-power-chart"
|
||||||
import LoadAverageChart from "@/components/charts/load-average-chart"
|
import LoadAverageChart from "@/components/charts/load-average-chart"
|
||||||
|
import { subscribeKeys } from "nanostores"
|
||||||
|
|
||||||
const cache = new Map<string, any>()
|
const cache = new Map<string, any>()
|
||||||
|
|
||||||
@@ -117,13 +120,13 @@ function dockerOrPodman(str: string, system: SystemRecord) {
|
|||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SystemDetail({ name }: { name: string }) {
|
export default memo(function SystemDetail({ name }: { name: string }) {
|
||||||
const direction = useStore($direction)
|
const direction = useStore($direction)
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
const maxValues = useStore($maxValues)
|
const maxValues = useStore($maxValues)
|
||||||
const [grid, setGrid] = useLocalStorage("grid", true)
|
const [grid, setGrid] = useBrowserStorage("grid", true)
|
||||||
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 [containerData, setContainerData] = useState([] as ChartData["containerData"])
|
const [containerData, setContainerData] = useState([] as ChartData["containerData"])
|
||||||
@@ -149,36 +152,13 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
}
|
}
|
||||||
}, [name])
|
}, [name])
|
||||||
|
|
||||||
// function resetCharts() {
|
// find matching system and update when it changes
|
||||||
// setSystemStats([])
|
|
||||||
// setContainerData([])
|
|
||||||
// }
|
|
||||||
|
|
||||||
// useEffect(resetCharts, [chartTime])
|
|
||||||
|
|
||||||
// find matching system
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (system.id && system.name === name) {
|
return subscribeKeys($allSystemsByName, [name], (newSystems) => {
|
||||||
return
|
const sys = newSystems[name]
|
||||||
}
|
sys?.id && setSystem(sys)
|
||||||
const matchingSystem = systems.find((s) => s.name === name) as SystemRecord
|
|
||||||
if (matchingSystem) {
|
|
||||||
setSystem(matchingSystem)
|
|
||||||
}
|
|
||||||
}, [name, system, systems])
|
|
||||||
|
|
||||||
// update system when new data is available
|
|
||||||
useEffect(() => {
|
|
||||||
if (!system.id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pb.collection<SystemRecord>("systems").subscribe(system.id, (e) => {
|
|
||||||
setSystem(e.record)
|
|
||||||
})
|
})
|
||||||
return () => {
|
}, [name])
|
||||||
pb.collection("systems").unsubscribe(system.id)
|
|
||||||
}
|
|
||||||
}, [system.id])
|
|
||||||
|
|
||||||
const chartData: ChartData = useMemo(() => {
|
const chartData: ChartData = useMemo(() => {
|
||||||
const lastCreated = Math.max(
|
const lastCreated = Math.max(
|
||||||
@@ -195,6 +175,9 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
}
|
}
|
||||||
}, [systemStats, containerData, direction])
|
}, [systemStats, containerData, direction])
|
||||||
|
|
||||||
|
// Share chart config computation for all container charts
|
||||||
|
const containerChartConfigs = useContainerChartConfigs(containerData)
|
||||||
|
|
||||||
// get stats
|
// get stats
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!system.id || !chartTime) {
|
if (!system.id || !chartTime) {
|
||||||
@@ -503,7 +486,12 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
description={t`Average CPU utilization of containers`}
|
description={t`Average CPU utilization of containers`}
|
||||||
cornerEl={containerFilterBar}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerChart chartData={chartData} dataKey="c" chartType={ChartType.CPU} />
|
<ContainerChart
|
||||||
|
chartData={chartData}
|
||||||
|
dataKey="c"
|
||||||
|
chartType={ChartType.CPU}
|
||||||
|
chartConfig={containerChartConfigs.cpu}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -525,7 +513,12 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
description={dockerOrPodman(t`Memory usage of docker containers`, system)}
|
description={dockerOrPodman(t`Memory usage of docker containers`, system)}
|
||||||
cornerEl={containerFilterBar}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
<ContainerChart chartData={chartData} dataKey="m" chartType={ChartType.Memory} />
|
<ContainerChart
|
||||||
|
chartData={chartData}
|
||||||
|
dataKey="m"
|
||||||
|
chartType={ChartType.Memory}
|
||||||
|
chartConfig={containerChartConfigs.memory}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -627,8 +620,12 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
description={dockerOrPodman(t`Network traffic of docker containers`, system)}
|
description={dockerOrPodman(t`Network traffic of docker containers`, system)}
|
||||||
cornerEl={containerFilterBar}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
{/* @ts-ignore */}
|
<ContainerChart
|
||||||
<ContainerChart chartData={chartData} chartType={ChartType.Network} dataKey="n" />
|
chartData={chartData}
|
||||||
|
chartType={ChartType.Network}
|
||||||
|
dataKey="n"
|
||||||
|
chartConfig={containerChartConfigs.network}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -835,7 +832,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
{bottomSpacing > 0 && <span className="block" style={{ height: bottomSpacing }} />}
|
{bottomSpacing > 0 && <span className="block" style={{ height: bottomSpacing }} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
|
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
|
||||||
const containerFilter = useStore(store)
|
const containerFilter = useStore(store)
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ import {
|
|||||||
FilterIcon,
|
FilterIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { $systems } from "@/lib/stores"
|
import { $pausedSystems, $downSystems, $upSystems, $systems } from "@/lib/stores"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { cn, runOnce, useLocalStorage } from "@/lib/utils"
|
import { cn, runOnce, useBrowserStorage } from "@/lib/utils"
|
||||||
import { $router, Link } from "../router"
|
import { $router, Link } from "../router"
|
||||||
import { useLingui, Trans } from "@lingui/react/macro"
|
import { useLingui, Trans } from "@lingui/react/macro"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||||
@@ -50,23 +50,25 @@ import { SystemStatus } from "@/lib/enums"
|
|||||||
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual"
|
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual"
|
||||||
|
|
||||||
type ViewMode = "table" | "grid"
|
type ViewMode = "table" | "grid"
|
||||||
type StatusFilter = "all" | "up" | "down" | "paused"
|
type StatusFilter = "all" | SystemRecord["status"]
|
||||||
|
|
||||||
const preloadSystemDetail = runOnce(() => import("@/components/routes/system.tsx"))
|
const preloadSystemDetail = runOnce(() => import("@/components/routes/system.tsx"))
|
||||||
|
|
||||||
export default function SystemsTable() {
|
export default function SystemsTable() {
|
||||||
const data = useStore($systems)
|
const data = useStore($systems)
|
||||||
|
const downSystems = $downSystems.get()
|
||||||
|
const upSystems = $upSystems.get()
|
||||||
|
const pausedSystems = $pausedSystems.get()
|
||||||
const { i18n, t } = useLingui()
|
const { i18n, t } = useLingui()
|
||||||
const [filter, setFilter] = useState<string>()
|
const [filter, setFilter] = useState<string>()
|
||||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
|
||||||
const [sorting, setSorting] = useState<SortingState>([{ id: "system", desc: false }])
|
const [sorting, setSorting] = useBrowserStorage<SortingState>(
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
"sortMode",
|
||||||
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {})
|
[{ id: "system", desc: false }],
|
||||||
const [viewMode, setViewMode] = useLocalStorage<ViewMode>(
|
sessionStorage
|
||||||
"viewMode",
|
|
||||||
// show grid view on mobile if there are less than 200 systems (looks better but table is more efficient)
|
|
||||||
window.innerWidth < 1024 && data.length < 200 ? "grid" : "table"
|
|
||||||
)
|
)
|
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||||
|
const [columnVisibility, setColumnVisibility] = useBrowserStorage<VisibilityState>("cols", {})
|
||||||
|
|
||||||
const locale = i18n.locale
|
const locale = i18n.locale
|
||||||
|
|
||||||
@@ -75,9 +77,21 @@ export default function SystemsTable() {
|
|||||||
if (statusFilter === "all") {
|
if (statusFilter === "all") {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
return data.filter((system) => system.status === statusFilter)
|
if (statusFilter === SystemStatus.Up) {
|
||||||
|
return Object.values(upSystems) ?? []
|
||||||
|
}
|
||||||
|
if (statusFilter === SystemStatus.Down) {
|
||||||
|
return Object.values(downSystems) ?? []
|
||||||
|
}
|
||||||
|
return Object.values(pausedSystems) ?? []
|
||||||
}, [data, statusFilter])
|
}, [data, statusFilter])
|
||||||
|
|
||||||
|
const [viewMode, setViewMode] = useBrowserStorage<ViewMode>(
|
||||||
|
"viewMode",
|
||||||
|
// show grid view on mobile if there are less than 200 systems (looks better but table is more efficient)
|
||||||
|
window.innerWidth < 1024 && filteredData.length < 200 ? "grid" : "table"
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filter !== undefined) {
|
if (filter !== undefined) {
|
||||||
table.getColumn("system")?.setFilterValue(filter)
|
table.getColumn("system")?.setFilterValue(filter)
|
||||||
@@ -101,7 +115,6 @@ export default function SystemsTable() {
|
|||||||
columnVisibility,
|
columnVisibility,
|
||||||
},
|
},
|
||||||
defaultColumn: {
|
defaultColumn: {
|
||||||
// sortDescFirst: true,
|
|
||||||
invertSorting: true,
|
invertSorting: true,
|
||||||
sortUndefined: "last",
|
sortUndefined: "last",
|
||||||
minSize: 0,
|
minSize: 0,
|
||||||
@@ -113,17 +126,22 @@ export default function SystemsTable() {
|
|||||||
const rows = table.getRowModel().rows
|
const rows = table.getRowModel().rows
|
||||||
const columns = table.getAllColumns()
|
const columns = table.getAllColumns()
|
||||||
const visibleColumns = table.getVisibleLeafColumns()
|
const visibleColumns = table.getVisibleLeafColumns()
|
||||||
|
|
||||||
|
const [upSystemsLength, downSystemsLength, pausedSystemsLength] = useMemo(() => {
|
||||||
|
return [Object.values(upSystems).length, Object.values(downSystems).length, Object.values(pausedSystems).length]
|
||||||
|
}, [upSystems, downSystems, pausedSystems])
|
||||||
|
|
||||||
// TODO: hiding temp then gpu messes up table headers
|
// TODO: hiding temp then gpu messes up table headers
|
||||||
const CardHead = useMemo(() => {
|
const CardHead = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
<CardHeader className="pb-4.5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||||
<div className="grid md:flex gap-5 w-full items-end">
|
<div className="grid md:flex gap-5 w-full items-end">
|
||||||
<div className="px-2 sm:px-1">
|
<div className="px-2 sm:px-1">
|
||||||
<CardTitle className="mb-2.5">
|
<CardTitle className="mb-2">
|
||||||
<Trans>All Systems</Trans>
|
<Trans>All Systems</Trans>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="flex">
|
||||||
<Trans>Updated in real time. Click on a system to view information.</Trans>
|
<Trans>Click on a system to view more information.</Trans>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -175,13 +193,13 @@ export default function SystemsTable() {
|
|||||||
<Trans>All Systems</Trans>
|
<Trans>All Systems</Trans>
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
<DropdownMenuRadioItem value="up" onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuRadioItem value="up" onSelect={(e) => e.preventDefault()}>
|
||||||
<Trans>Up</Trans>
|
<Trans>Up ({upSystemsLength})</Trans>
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
<DropdownMenuRadioItem value="down" onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuRadioItem value="down" onSelect={(e) => e.preventDefault()}>
|
||||||
<Trans>Down</Trans>
|
<Trans>Down ({downSystemsLength})</Trans>
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
<DropdownMenuRadioItem value="paused" onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuRadioItem value="paused" onSelect={(e) => e.preventDefault()}>
|
||||||
<Trans>Paused</Trans>
|
<Trans>Paused ({pausedSystemsLength})</Trans>
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
</DropdownMenuRadioGroup>
|
</DropdownMenuRadioGroup>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,7 +270,16 @@ export default function SystemsTable() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
)
|
)
|
||||||
}, [visibleColumns.length, sorting, viewMode, locale, statusFilter])
|
}, [
|
||||||
|
visibleColumns.length,
|
||||||
|
sorting,
|
||||||
|
viewMode,
|
||||||
|
locale,
|
||||||
|
statusFilter,
|
||||||
|
upSystemsLength,
|
||||||
|
downSystemsLength,
|
||||||
|
pausedSystemsLength,
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { ChartTimes, SystemRecord, UserSettings } from "@/types"
|
import { ChartTimes, UserSettings } from "@/types"
|
||||||
import { $alerts, $longestSystemNameLen, $systems, $userSettings } from "./stores"
|
import { $alerts, $allSystemsByName, $userSettings } from "./stores"
|
||||||
import { toast } from "@/components/ui/use-toast"
|
import { toast } from "@/components/ui/use-toast"
|
||||||
import { t } from "@lingui/core/macro"
|
import { t } from "@lingui/core/macro"
|
||||||
import { chartTimeData } from "./utils"
|
import { chartTimeData } from "./utils"
|
||||||
import { WritableAtom } from "nanostores"
|
|
||||||
import { RecordModel, RecordSubscription } from "pocketbase"
|
|
||||||
import PocketBase from "pocketbase"
|
import PocketBase from "pocketbase"
|
||||||
import { basePath } from "@/components/router"
|
import { basePath } from "@/components/router"
|
||||||
|
|
||||||
@@ -14,7 +12,7 @@ export const pb = new PocketBase(basePath)
|
|||||||
export const isAdmin = () => pb.authStore.record?.role === "admin"
|
export const isAdmin = () => pb.authStore.record?.role === "admin"
|
||||||
export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly"
|
export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly"
|
||||||
|
|
||||||
const verifyAuth = () => {
|
export const verifyAuth = () => {
|
||||||
pb.collection("users")
|
pb.collection("users")
|
||||||
.authRefresh()
|
.authRefresh()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -29,7 +27,7 @@ const verifyAuth = () => {
|
|||||||
|
|
||||||
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
|
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
|
||||||
export async function logOut() {
|
export async function logOut() {
|
||||||
$systems.set([])
|
$allSystemsByName.set({})
|
||||||
$alerts.set({})
|
$alerts.set({})
|
||||||
$userSettings.set({} as UserSettings)
|
$userSettings.set({} as UserSettings)
|
||||||
sessionStorage.setItem("lo", "t") // prevent auto login on logout
|
sessionStorage.setItem("lo", "t") // prevent auto login on logout
|
||||||
@@ -54,74 +52,6 @@ export async function updateUserSettings() {
|
|||||||
console.error("create settings", e)
|
console.error("create settings", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** Update systems / alerts list when records change */
|
|
||||||
export function updateRecordList<T extends RecordModel>(e: RecordSubscription<T>, $store: WritableAtom<T[]>) {
|
|
||||||
const curRecords = $store.get()
|
|
||||||
const newRecords = []
|
|
||||||
if (e.action === "delete") {
|
|
||||||
for (const server of curRecords) {
|
|
||||||
if (server.id !== e.record.id) {
|
|
||||||
newRecords.push(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let found = 0
|
|
||||||
for (const server of curRecords) {
|
|
||||||
if (server.id === e.record.id) {
|
|
||||||
found = newRecords.push(e.record)
|
|
||||||
} else {
|
|
||||||
newRecords.push(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
newRecords.push(e.record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$store.set(newRecords)
|
|
||||||
}
|
|
||||||
/** Fetches updated system list from database */
|
|
||||||
export const updateSystemList = (() => {
|
|
||||||
let isFetchingSystems = false
|
|
||||||
return async () => {
|
|
||||||
if (isFetchingSystems) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isFetchingSystems = true
|
|
||||||
try {
|
|
||||||
let records = await pb
|
|
||||||
.collection<SystemRecord>("systems")
|
|
||||||
.getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
|
|
||||||
|
|
||||||
if (records.length) {
|
|
||||||
// records = [
|
|
||||||
// ...records,
|
|
||||||
// ...records,
|
|
||||||
// ...records,
|
|
||||||
// ...records,
|
|
||||||
// ...records,
|
|
||||||
// ...records,
|
|
||||||
// ...records,
|
|
||||||
// ...records,
|
|
||||||
// ...records,
|
|
||||||
// ]
|
|
||||||
// we need to loop once to get the longest name
|
|
||||||
let longestName = $longestSystemNameLen.get()
|
|
||||||
for (const { name } of records) {
|
|
||||||
const nameLen = Math.min(20, name.length)
|
|
||||||
if (nameLen > longestName) {
|
|
||||||
$longestSystemNameLen.set(nameLen)
|
|
||||||
longestName = nameLen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$systems.set(records)
|
|
||||||
} else {
|
|
||||||
verifyAuth()
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isFetchingSystems = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
|
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
|
||||||
d ||= chartTimeData[timeString].getOffset(new Date())
|
d ||= chartTimeData[timeString].getOffset(new Date())
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { atom, map } from "nanostores"
|
import { atom, computed, map, ReadableAtom } from "nanostores"
|
||||||
import { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
|
import { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
|
||||||
import { Unit } from "./enums"
|
import { Unit } from "./enums"
|
||||||
import { pb } from "./api"
|
import { pb } from "./api"
|
||||||
@@ -6,8 +6,18 @@ import { pb } from "./api"
|
|||||||
/** Store if user is authenticated */
|
/** Store if user is authenticated */
|
||||||
export const $authenticated = atom(pb.authStore.isValid)
|
export const $authenticated = atom(pb.authStore.isValid)
|
||||||
|
|
||||||
/** List of system records */
|
/** Map of system records by name */
|
||||||
export const $systems = atom<SystemRecord[]>([])
|
export const $allSystemsByName = map<Record<string, SystemRecord>>({})
|
||||||
|
/** Map of system records by id */
|
||||||
|
export const $allSystemsById = map<Record<string, SystemRecord>>({})
|
||||||
|
/** Map of up systems by id */
|
||||||
|
export const $upSystems = map<Record<string, SystemRecord>>({})
|
||||||
|
/** Map of down systems by id */
|
||||||
|
export const $downSystems = map<Record<string, SystemRecord>>({})
|
||||||
|
/** Map of paused systems by id */
|
||||||
|
export const $pausedSystems = map<Record<string, SystemRecord>>({})
|
||||||
|
/** List of all system records */
|
||||||
|
export const $systems: ReadableAtom<SystemRecord[]> = computed($allSystemsByName, Object.values)
|
||||||
|
|
||||||
/** Map of alert records by system id and alert name */
|
/** Map of alert records by system id and alert name */
|
||||||
export const $alerts = map<AlertMap>({})
|
export const $alerts = map<AlertMap>({})
|
||||||
|
|||||||
161
beszel/site/src/lib/systemsManager.ts
Normal file
161
beszel/site/src/lib/systemsManager.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { SystemRecord } from "@/types"
|
||||||
|
import { PreinitializedMapStore } from "nanostores"
|
||||||
|
import { pb, verifyAuth } from "@/lib/api"
|
||||||
|
import {
|
||||||
|
$allSystemsByName,
|
||||||
|
$upSystems,
|
||||||
|
$downSystems,
|
||||||
|
$pausedSystems,
|
||||||
|
$allSystemsById,
|
||||||
|
$longestSystemNameLen,
|
||||||
|
} from "@/lib/stores"
|
||||||
|
import { updateFavicon, FAVICON_DEFAULT, FAVICON_GREEN, FAVICON_RED } from "@/lib/utils"
|
||||||
|
import { SystemStatus } from "./enums"
|
||||||
|
|
||||||
|
const COLLECTION = pb.collection<SystemRecord>("systems")
|
||||||
|
const FIELDS_DEFAULT = "id,name,host,port,info,status"
|
||||||
|
|
||||||
|
/** Maximum system name length for display purposes */
|
||||||
|
const MAX_SYSTEM_NAME_LENGTH = 20
|
||||||
|
|
||||||
|
let initialized = false
|
||||||
|
let unsub: (() => void) | undefined | void
|
||||||
|
|
||||||
|
/** Initialize the systems manager and set up listeners */
|
||||||
|
export function init() {
|
||||||
|
if (initialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
initialized = true
|
||||||
|
|
||||||
|
// sync system stores on change
|
||||||
|
$allSystemsByName.listen((newSystems, oldSystems, changedKey) => {
|
||||||
|
const oldSystem = oldSystems[changedKey]
|
||||||
|
const newSystem = newSystems[changedKey]
|
||||||
|
|
||||||
|
// if system is undefined (deleted), remove it from the stores
|
||||||
|
if (oldSystem && !newSystem?.id) {
|
||||||
|
removeFromStore(oldSystem, $upSystems)
|
||||||
|
removeFromStore(oldSystem, $downSystems)
|
||||||
|
removeFromStore(oldSystem, $pausedSystems)
|
||||||
|
removeFromStore(oldSystem, $allSystemsById)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newSystem) {
|
||||||
|
onSystemsChanged(newSystems, undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStatus = newSystem.status
|
||||||
|
if (newStatus === SystemStatus.Up) {
|
||||||
|
$upSystems.setKey(newSystem.id, newSystem)
|
||||||
|
removeFromStore(newSystem, $downSystems)
|
||||||
|
removeFromStore(newSystem, $pausedSystems)
|
||||||
|
} else if (newStatus === SystemStatus.Down) {
|
||||||
|
$downSystems.setKey(newSystem.id, newSystem)
|
||||||
|
removeFromStore(newSystem, $upSystems)
|
||||||
|
removeFromStore(newSystem, $pausedSystems)
|
||||||
|
} else if (newStatus === SystemStatus.Paused) {
|
||||||
|
$pausedSystems.setKey(newSystem.id, newSystem)
|
||||||
|
removeFromStore(newSystem, $upSystems)
|
||||||
|
removeFromStore(newSystem, $downSystems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// run things that need to be done when systems change
|
||||||
|
onSystemsChanged(newSystems, newSystem)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update the longest system name length and favicon based on system status */
|
||||||
|
function onSystemsChanged(_: Record<string, SystemRecord>, changedSystem: SystemRecord | undefined) {
|
||||||
|
const upSystemsStore = $upSystems.get()
|
||||||
|
const downSystemsStore = $downSystems.get()
|
||||||
|
const upSystems = Object.values(upSystemsStore)
|
||||||
|
const downSystems = Object.values(downSystemsStore)
|
||||||
|
|
||||||
|
// Update longest system name length
|
||||||
|
const longestName = $longestSystemNameLen.get()
|
||||||
|
const nameLen = Math.min(MAX_SYSTEM_NAME_LENGTH, changedSystem?.name.length || 0)
|
||||||
|
if (nameLen > longestName) {
|
||||||
|
$longestSystemNameLen.set(nameLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update favicon based on system status
|
||||||
|
if (downSystems.length > 0) {
|
||||||
|
updateFavicon(FAVICON_RED)
|
||||||
|
} else if (upSystems.length > 0) {
|
||||||
|
updateFavicon(FAVICON_GREEN)
|
||||||
|
} else {
|
||||||
|
updateFavicon(FAVICON_DEFAULT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetch systems from collection */
|
||||||
|
async function fetchSystems(): Promise<SystemRecord[]> {
|
||||||
|
try {
|
||||||
|
return await COLLECTION.getFullList({ sort: "+name", fields: FIELDS_DEFAULT })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch systems:", error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store management functions
|
||||||
|
/** Add system to both name and ID stores */
|
||||||
|
export function add(system: SystemRecord) {
|
||||||
|
$allSystemsByName.setKey(system.name, system)
|
||||||
|
$allSystemsById.setKey(system.id, system)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove system from stores */
|
||||||
|
export function remove(system: SystemRecord) {
|
||||||
|
removeFromStore(system, $allSystemsByName)
|
||||||
|
removeFromStore(system, $allSystemsById)
|
||||||
|
removeFromStore(system, $upSystems)
|
||||||
|
removeFromStore(system, $downSystems)
|
||||||
|
removeFromStore(system, $pausedSystems)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove system from specific store */
|
||||||
|
function removeFromStore(system: SystemRecord, store: PreinitializedMapStore<Record<string, SystemRecord>>) {
|
||||||
|
const key = store === $allSystemsByName ? system.name : system.id
|
||||||
|
store.setKey(key, undefined as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Action functions for subscription */
|
||||||
|
const actionFns: Record<string, (system: SystemRecord) => void> = {
|
||||||
|
create: add,
|
||||||
|
update: add,
|
||||||
|
delete: remove,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subscribe to real-time system updates from the collection */
|
||||||
|
export async function subscribe() {
|
||||||
|
try {
|
||||||
|
unsub = await COLLECTION.subscribe("*", ({ action, record }) => actionFns[action]?.(record), {
|
||||||
|
fields: FIELDS_DEFAULT,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to subscribe to systems collection:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Refresh all systems with latest data from the hub */
|
||||||
|
export async function refresh() {
|
||||||
|
try {
|
||||||
|
const records = await fetchSystems()
|
||||||
|
if (!records.length) {
|
||||||
|
// No systems found, verify authentication
|
||||||
|
verifyAuth()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const record of records) {
|
||||||
|
add(record)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to refresh systems:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unsubscribe from real-time system updates */
|
||||||
|
export const unsubscribe = () => (unsub = unsub?.())
|
||||||
@@ -2,13 +2,17 @@ import { t } from "@lingui/core/macro"
|
|||||||
import { toast } from "@/components/ui/use-toast"
|
import { toast } from "@/components/ui/use-toast"
|
||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
import { $copyContent, $systems, $userSettings } from "./stores"
|
import { $copyContent, $userSettings } from "./stores"
|
||||||
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
|
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
|
||||||
import { timeDay, timeHour } from "d3-time"
|
import { timeDay, timeHour } from "d3-time"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { MeterState, Unit } from "./enums"
|
import { MeterState, Unit } from "./enums"
|
||||||
import { prependBasePath } from "@/components/router"
|
import { prependBasePath } from "@/components/router"
|
||||||
|
|
||||||
|
export const FAVICON_DEFAULT = "favicon.svg"
|
||||||
|
export const FAVICON_GREEN = "favicon-green.svg"
|
||||||
|
export const FAVICON_RED = "favicon-red.svg"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
@@ -104,32 +108,6 @@ export const chartTimeData: ChartTimeData = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the correct width of the y axis in recharts based on the longest label */
|
|
||||||
export function useYAxisWidth() {
|
|
||||||
const [yAxisWidth, setYAxisWidth] = useState(0)
|
|
||||||
let maxChars = 0
|
|
||||||
let timeout: Timer
|
|
||||||
function updateYAxisWidth(str: string) {
|
|
||||||
if (str.length > maxChars) {
|
|
||||||
maxChars = str.length
|
|
||||||
const div = document.createElement("div")
|
|
||||||
div.className = "text-xs tabular-nums tracking-tighter table sr-only"
|
|
||||||
div.innerHTML = str
|
|
||||||
clearTimeout(timeout)
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
document.body.appendChild(div)
|
|
||||||
const width = div.offsetWidth + 24
|
|
||||||
if (width > yAxisWidth) {
|
|
||||||
setYAxisWidth(div.offsetWidth + 24)
|
|
||||||
}
|
|
||||||
document.body.removeChild(div)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return { yAxisWidth, updateYAxisWidth }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Format number to x decimal places, without trailing zeros */
|
/** Format number to x decimal places, without trailing zeros */
|
||||||
export function toFixedFloat(num: number, digits: number) {
|
export function toFixedFloat(num: number, digits: number) {
|
||||||
return parseFloat((digits === 0 ? Math.ceil(num) : num).toFixed(digits))
|
return parseFloat((digits === 0 ? Math.ceil(num) : num).toFixed(digits))
|
||||||
@@ -152,20 +130,20 @@ export function decimalString(num: number, digits = 2) {
|
|||||||
return formatter.format(num)
|
return formatter.format(num)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get value from local storage */
|
/** Get value from local or session storage */
|
||||||
function getStorageValue(key: string, defaultValue: any) {
|
function getStorageValue(key: string, defaultValue: any, storageInterface: Storage = localStorage) {
|
||||||
const saved = localStorage?.getItem(key)
|
const saved = storageInterface?.getItem(key)
|
||||||
return saved ? JSON.parse(saved) : defaultValue
|
return saved ? JSON.parse(saved) : defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hook to sync value in local storage */
|
/** Hook to sync value in local or session storage */
|
||||||
export function useLocalStorage<T>(key: string, defaultValue: T) {
|
export function useBrowserStorage<T>(key: string, defaultValue: T, storageInterface: Storage = localStorage) {
|
||||||
key = `besz-${key}`
|
key = `besz-${key}`
|
||||||
const [value, setValue] = useState(() => {
|
const [value, setValue] = useState(() => {
|
||||||
return getStorageValue(key, defaultValue)
|
return getStorageValue(key, defaultValue, storageInterface)
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage?.setItem(key, JSON.stringify(value))
|
storageInterface?.setItem(key, JSON.stringify(value))
|
||||||
}, [key, value])
|
}, [key, value])
|
||||||
|
|
||||||
return [value, setValue]
|
return [value, setValue]
|
||||||
@@ -344,28 +322,20 @@ export function debounce<T extends (...args: any[]) => any>(func: T, wait: numbe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* returns the name of a system from its id */
|
// Cache for runOnce
|
||||||
export const getSystemNameFromId = (() => {
|
const runOnceCache = new WeakMap<Function, { done: boolean; result: unknown }>()
|
||||||
const cache = new Map<string, string>()
|
|
||||||
return (systemId: string): string => {
|
|
||||||
if (cache.has(systemId)) {
|
|
||||||
return cache.get(systemId)!
|
|
||||||
}
|
|
||||||
const sysName = $systems.get().find((s) => s.id === systemId)?.name ?? ""
|
|
||||||
cache.set(systemId, sysName)
|
|
||||||
return sysName
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
/** Run a function only once */
|
/** Run a function only once */
|
||||||
export function runOnce<T extends (...args: any[]) => any>(fn: T): (...args: Parameters<T>) => ReturnType<T> {
|
export function runOnce<T extends (...args: any[]) => any>(fn: T): T {
|
||||||
let done = false
|
return ((...args: Parameters<T>) => {
|
||||||
let result: any
|
let state = runOnceCache.get(fn)
|
||||||
return (...args: any) => {
|
if (!state) {
|
||||||
if (!done) {
|
state = { done: false, result: undefined }
|
||||||
result = fn(...args)
|
runOnceCache.set(fn, state)
|
||||||
done = true
|
|
||||||
}
|
}
|
||||||
return result
|
if (!state.done) {
|
||||||
|
state.result = fn(...args)
|
||||||
|
state.done = true
|
||||||
}
|
}
|
||||||
|
return state.result
|
||||||
|
}) as T
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ import { Suspense, lazy, memo, useEffect } from "react"
|
|||||||
import ReactDOM from "react-dom/client"
|
import ReactDOM from "react-dom/client"
|
||||||
import { ThemeProvider } from "./components/theme-provider.tsx"
|
import { ThemeProvider } from "./components/theme-provider.tsx"
|
||||||
import { DirectionProvider } from "@radix-ui/react-direction"
|
import { DirectionProvider } from "@radix-ui/react-direction"
|
||||||
import { $authenticated, $systems, $publicKey, $copyContent, $direction } from "./lib/stores.ts"
|
import { $authenticated, $publicKey, $copyContent, $direction } from "./lib/stores.ts"
|
||||||
import { pb, updateSystemList, updateUserSettings } from "./lib/api.ts"
|
import { pb, updateUserSettings } from "./lib/api.ts"
|
||||||
|
import * as systemsManager from "./lib/systemsManager.ts"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { Toaster } from "./components/ui/toaster.tsx"
|
import { Toaster } from "./components/ui/toaster.tsx"
|
||||||
import { $router } from "./components/router.tsx"
|
import { $router } from "./components/router.tsx"
|
||||||
import { updateFavicon } from "@/lib/utils"
|
|
||||||
import Navbar from "./components/navbar.tsx"
|
import Navbar from "./components/navbar.tsx"
|
||||||
import { I18nProvider } from "@lingui/react"
|
import { I18nProvider } from "@lingui/react"
|
||||||
import { i18n } from "@lingui/core"
|
import { i18n } from "@lingui/core"
|
||||||
import { getLocale, dynamicActivate } from "./lib/i18n"
|
import { getLocale, dynamicActivate } from "./lib/i18n"
|
||||||
import { SystemStatus } from "./lib/enums"
|
|
||||||
import { alertManager } from "./lib/alerts"
|
import { alertManager } from "./lib/alerts"
|
||||||
import Settings from "./components/routes/settings/layout.tsx"
|
import Settings from "./components/routes/settings/layout.tsx"
|
||||||
|
|
||||||
@@ -25,8 +24,6 @@ const CopyToClipboardDialog = lazy(() => import("@/components/copy-to-clipboard.
|
|||||||
|
|
||||||
const App = memo(() => {
|
const App = memo(() => {
|
||||||
const page = useStore($router)
|
const page = useStore($router)
|
||||||
const authenticated = useStore($authenticated)
|
|
||||||
const systems = useStore($systems)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// change auth store on auth change
|
// change auth store on auth change
|
||||||
@@ -37,40 +34,26 @@ const App = memo(() => {
|
|||||||
pb.send("/api/beszel/getkey", {}).then((data) => {
|
pb.send("/api/beszel/getkey", {}).then((data) => {
|
||||||
$publicKey.set(data.key)
|
$publicKey.set(data.key)
|
||||||
})
|
})
|
||||||
// get servers / alerts / settings
|
// get user settings
|
||||||
updateUserSettings()
|
updateUserSettings()
|
||||||
// need to get system list before alerts
|
// need to get system list before alerts
|
||||||
updateSystemList()
|
systemsManager.init()
|
||||||
// get alerts
|
systemsManager
|
||||||
|
// get current systems list
|
||||||
|
.refresh()
|
||||||
|
// subscribe to new system updates
|
||||||
|
.then(systemsManager.subscribe)
|
||||||
|
// get current alerts
|
||||||
.then(alertManager.refresh)
|
.then(alertManager.refresh)
|
||||||
// subscribe to new alert updates
|
// subscribe to new alert updates
|
||||||
.then(alertManager.subscribe)
|
.then(alertManager.subscribe)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
updateFavicon("favicon.svg")
|
// updateFavicon("favicon.svg")
|
||||||
alertManager.unsubscribe()
|
alertManager.unsubscribe()
|
||||||
|
systemsManager.unsubscribe()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// update favicon
|
|
||||||
useEffect(() => {
|
|
||||||
if (!systems.length || !authenticated) {
|
|
||||||
updateFavicon("favicon.svg")
|
|
||||||
} else {
|
|
||||||
let up = false
|
|
||||||
for (const system of systems) {
|
|
||||||
if (system.status === SystemStatus.Down) {
|
|
||||||
updateFavicon("favicon-red.svg")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (system.status === SystemStatus.Up) {
|
|
||||||
up = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateFavicon(up ? "favicon-green.svg" : "favicon.svg")
|
|
||||||
}
|
|
||||||
}, [systems])
|
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return <h1 className="text-3xl text-center my-14">404</h1>
|
return <h1 className="text-3xl text-center my-14">404</h1>
|
||||||
} else if (page.route === "home") {
|
} else if (page.route === "home") {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import path from "path"
|
|||||||
import tailwindcss from "@tailwindcss/vite"
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
import react from "@vitejs/plugin-react-swc"
|
import react from "@vitejs/plugin-react-swc"
|
||||||
import { lingui } from "@lingui/vite-plugin"
|
import { lingui } from "@lingui/vite-plugin"
|
||||||
import { version } from "./package.json"
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "./",
|
base: "./",
|
||||||
@@ -13,13 +12,6 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
lingui(),
|
lingui(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
{
|
|
||||||
name: "replace version in index.html during dev",
|
|
||||||
apply: "serve",
|
|
||||||
transformIndexHtml(html) {
|
|
||||||
return html.replace("{{V}}", version).replace("{{HUB_URL}}", "")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
esbuild: {
|
esbuild: {
|
||||||
legalComments: "external",
|
legalComments: "external",
|
||||||
|
|||||||
@@ -216,11 +216,11 @@ if [ "$UNINSTALL" = true ]; then
|
|||||||
echo "Removing the OpenRC service files..."
|
echo "Removing the OpenRC service files..."
|
||||||
rm -f /etc/init.d/beszel-agent
|
rm -f /etc/init.d/beszel-agent
|
||||||
|
|
||||||
# Remove the update service if it exists
|
# Remove the daily update cron job if it exists
|
||||||
echo "Removing the daily update service..."
|
echo "Removing the daily update cron job..."
|
||||||
rc-service beszel-agent-update stop 2>/dev/null
|
if crontab -u root -l 2>/dev/null | grep -q "beszel-agent.*update"; then
|
||||||
rc-update del beszel-agent-update default 2>/dev/null
|
crontab -u root -l 2>/dev/null | grep -v "beszel-agent.*update" | crontab -u root -
|
||||||
rm -f /etc/init.d/beszel-agent-update
|
fi
|
||||||
|
|
||||||
# Remove log files
|
# Remove log files
|
||||||
echo "Removing log files..."
|
echo "Removing log files..."
|
||||||
@@ -321,6 +321,9 @@ if [ -z "$KEY" ]; then
|
|||||||
read KEY
|
read KEY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Remove newlines from KEY
|
||||||
|
KEY=$(echo "$KEY" | tr -d '\n')
|
||||||
|
|
||||||
# TOKEN and HUB_URL are optional for backwards compatibility - no interactive prompts
|
# TOKEN and HUB_URL are optional for backwards compatibility - no interactive prompts
|
||||||
# They will be set as empty environment variables if not provided
|
# They will be set as empty environment variables if not provided
|
||||||
|
|
||||||
@@ -398,7 +401,7 @@ fi
|
|||||||
echo "Downloading and installing the agent..."
|
echo "Downloading and installing the agent..."
|
||||||
|
|
||||||
OS=$(uname -s | sed -e 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/')
|
OS=$(uname -s | sed -e 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/')
|
||||||
ARCH=$(uname -m | sed -e 's/x86_64/amd64/' -e 's/armv6l/arm/' -e 's/armv7l/arm/' -e 's/aarch64/arm64/' -e 's/mips/mipsle/')
|
ARCH=$(uname -m | sed -e 's/x86_64/amd64/' -e 's/armv6l/arm/' -e 's/armv7l/arm/' -e 's/aarch64/arm64/')
|
||||||
FILE_NAME="beszel-agent_${OS}_${ARCH}.tar.gz"
|
FILE_NAME="beszel-agent_${OS}_${ARCH}.tar.gz"
|
||||||
|
|
||||||
# Determine version to install
|
# Determine version to install
|
||||||
@@ -523,35 +526,19 @@ EOF
|
|||||||
elif [ "$AUTO_UPDATE_FLAG" = "false" ]; then
|
elif [ "$AUTO_UPDATE_FLAG" = "false" ]; then
|
||||||
AUTO_UPDATE="n"
|
AUTO_UPDATE="n"
|
||||||
else
|
else
|
||||||
printf "\nWould you like to enable automatic daily updates for beszel-agent? (y/n): "
|
printf "\nEnable automatic daily updates for beszel-agent? (y/n): "
|
||||||
read AUTO_UPDATE
|
read AUTO_UPDATE
|
||||||
fi
|
fi
|
||||||
case "$AUTO_UPDATE" in
|
case "$AUTO_UPDATE" in
|
||||||
[Yy]*)
|
[Yy]*)
|
||||||
echo "Setting up daily automatic updates for beszel-agent..."
|
echo "Setting up daily automatic updates for beszel-agent..."
|
||||||
|
|
||||||
cat >/etc/init.d/beszel-agent-update <<EOF
|
# Create cron job to run beszel-agent update command daily at midnight
|
||||||
#!/sbin/openrc-run
|
if ! crontab -u root -l 2>/dev/null | grep -q "beszel-agent.*update"; then
|
||||||
|
(crontab -u root -l 2>/dev/null; echo "12 0 * * * /opt/beszel-agent/beszel-agent update >/dev/null 2>&1") | crontab -u root -
|
||||||
|
fi
|
||||||
|
|
||||||
name="beszel-agent-update"
|
printf "\nDaily updates have been enabled via cron job.\n"
|
||||||
description="Update beszel-agent if needed"
|
|
||||||
|
|
||||||
depend() {
|
|
||||||
need beszel-agent
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
ebegin "Checking for beszel-agent updates"
|
|
||||||
/opt/beszel-agent/beszel-agent update
|
|
||||||
eend $?
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x /etc/init.d/beszel-agent-update
|
|
||||||
rc-update add beszel-agent-update default
|
|
||||||
rc-service beszel-agent-update start
|
|
||||||
|
|
||||||
printf "\nAutomatic daily updates have been enabled.\n"
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -612,7 +599,7 @@ EOF
|
|||||||
AUTO_UPDATE="n"
|
AUTO_UPDATE="n"
|
||||||
sleep 1 # give time for the service to start
|
sleep 1 # give time for the service to start
|
||||||
else
|
else
|
||||||
printf "\nWould you like to enable automatic daily updates for beszel-agent? (y/n): "
|
printf "\nEnable automatic daily updates for beszel-agent? (y/n): "
|
||||||
read AUTO_UPDATE
|
read AUTO_UPDATE
|
||||||
fi
|
fi
|
||||||
case "$AUTO_UPDATE" in
|
case "$AUTO_UPDATE" in
|
||||||
@@ -620,12 +607,12 @@ EOF
|
|||||||
echo "Setting up daily automatic updates for beszel-agent..."
|
echo "Setting up daily automatic updates for beszel-agent..."
|
||||||
|
|
||||||
cat >/etc/crontabs/beszel <<EOF
|
cat >/etc/crontabs/beszel <<EOF
|
||||||
0 0 * * * /etc/init.d/beszel-agent update
|
12 0 * * * /etc/init.d/beszel-agent update
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
/etc/init.d/cron restart
|
/etc/init.d/cron restart
|
||||||
|
|
||||||
printf "\nAutomatic daily updates have been enabled.\n"
|
printf "\nDaily updates have been enabled.\n"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -695,7 +682,7 @@ EOF
|
|||||||
AUTO_UPDATE="n"
|
AUTO_UPDATE="n"
|
||||||
sleep 1 # give time for the service to start
|
sleep 1 # give time for the service to start
|
||||||
else
|
else
|
||||||
printf "\nWould you like to enable automatic daily updates for beszel-agent? (y/n): "
|
printf "\nEnable automatic daily updates for beszel-agent? (y/n): "
|
||||||
read AUTO_UPDATE
|
read AUTO_UPDATE
|
||||||
fi
|
fi
|
||||||
case "$AUTO_UPDATE" in
|
case "$AUTO_UPDATE" in
|
||||||
@@ -730,7 +717,7 @@ EOF
|
|||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable --now beszel-agent-update.timer
|
systemctl enable --now beszel-agent-update.timer
|
||||||
|
|
||||||
printf "\nAutomatic daily updates have been enabled.\n"
|
printf "\nDaily updates have been enabled.\n"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user