mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-04 20:11:50 +02:00
Compare commits
3 Commits
bd74ab8d7b
...
temp-pve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eca353429 | ||
|
|
d9e3c4678a | ||
|
|
1243a7bd8d |
203
agent/pve.go
203
agent/pve.go
@@ -18,91 +18,11 @@ type pveManager struct {
|
|||||||
nodeName string // Cluster node name
|
nodeName string // Cluster node name
|
||||||
cpuCount int // CPU count on node
|
cpuCount int // CPU count on node
|
||||||
nodeStatsMap map[string]*container.PveNodeStats // Keeps track of pve node stats
|
nodeStatsMap map[string]*container.PveNodeStats // Keeps track of pve node stats
|
||||||
|
lastInitTry time.Time // Last time node initialization was attempted
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns stats for all running VMs/LXCs
|
// newPVEManager creates a new PVE manager - may return nil if required environment variables
|
||||||
func (pm *pveManager) getPVEStats() ([]*container.PveNodeStats, error) {
|
// are not set or if there is an error connecting to the API
|
||||||
if pm.client == nil {
|
|
||||||
return nil, errors.New("PVE client not configured")
|
|
||||||
}
|
|
||||||
cluster, err := pm.client.Cluster(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Error getting cluster", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resources, err := cluster.Resources(context.Background(), "vm")
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Error getting resources", "err", err, "resources", resources)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
containersLength := len(resources)
|
|
||||||
containerIds := make(map[string]struct{}, containersLength)
|
|
||||||
|
|
||||||
// only include running vms and lxcs on selected node
|
|
||||||
for _, resource := range resources {
|
|
||||||
if resource.Node == pm.nodeName && resource.Status == "running" {
|
|
||||||
containerIds[resource.ID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// remove invalid container stats
|
|
||||||
for id := range pm.nodeStatsMap {
|
|
||||||
if _, exists := containerIds[id]; !exists {
|
|
||||||
delete(pm.nodeStatsMap, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate stats
|
|
||||||
stats := make([]*container.PveNodeStats, 0, len(containerIds))
|
|
||||||
for _, resource := range resources {
|
|
||||||
if _, exists := containerIds[resource.ID]; !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
resourceStats, initialized := pm.nodeStatsMap[resource.ID]
|
|
||||||
if !initialized {
|
|
||||||
resourceStats = &container.PveNodeStats{}
|
|
||||||
pm.nodeStatsMap[resource.ID] = resourceStats
|
|
||||||
}
|
|
||||||
// reset current stats
|
|
||||||
resourceStats.Cpu = 0
|
|
||||||
resourceStats.Mem = 0
|
|
||||||
resourceStats.Bandwidth = [2]uint64{0, 0}
|
|
||||||
// Store clean name (no type suffix)
|
|
||||||
resourceStats.Name = resource.Name
|
|
||||||
// Store resource ID (e.g. "qemu/100") in .Id (cbor key 7, json:"-")
|
|
||||||
resourceStats.Id = resource.ID
|
|
||||||
// Store type (e.g. "qemu" or "lxc") in .Image (cbor key 8, json:"-")
|
|
||||||
resourceStats.Type = resource.Type
|
|
||||||
// PVE limits (cbor-only, for pve_vms table)
|
|
||||||
resourceStats.MaxCPU = resource.MaxCPU
|
|
||||||
resourceStats.MaxMem = resource.MaxMem
|
|
||||||
resourceStats.Uptime = resource.Uptime
|
|
||||||
// prevent first run from sending all prev sent/recv bytes
|
|
||||||
total_sent := uint64(resource.NetOut)
|
|
||||||
total_recv := uint64(resource.NetIn)
|
|
||||||
var sent_delta, recv_delta float64
|
|
||||||
if initialized {
|
|
||||||
secondsElapsed := time.Since(resourceStats.PrevReadTime).Seconds()
|
|
||||||
if secondsElapsed > 0 {
|
|
||||||
sent_delta = float64(total_sent-resourceStats.PrevNet.Sent) / secondsElapsed
|
|
||||||
recv_delta = float64(total_recv-resourceStats.PrevNet.Recv) / secondsElapsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resourceStats.PrevNet.Sent = total_sent
|
|
||||||
resourceStats.PrevNet.Recv = total_recv
|
|
||||||
resourceStats.PrevReadTime = time.Now()
|
|
||||||
|
|
||||||
// Update final stats values
|
|
||||||
resourceStats.Cpu = twoDecimals(100.0 * resource.CPU * float64(resource.MaxCPU) / float64(pm.cpuCount))
|
|
||||||
resourceStats.Mem = bytesToMegabytes(float64(resource.Mem))
|
|
||||||
resourceStats.Bandwidth = [2]uint64{uint64(sent_delta), uint64(recv_delta)}
|
|
||||||
|
|
||||||
stats = append(stats, resourceStats)
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new PVE manager - may return nil if required environment variables are not set or if there is an error connecting to the API
|
|
||||||
func newPVEManager() *pveManager {
|
func newPVEManager() *pveManager {
|
||||||
url, exists := GetEnv("PROXMOX_URL")
|
url, exists := GetEnv("PROXMOX_URL")
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -145,14 +65,113 @@ func newPVEManager() *pveManager {
|
|||||||
nodeStatsMap: make(map[string]*container.PveNodeStats),
|
nodeStatsMap: make(map[string]*container.PveNodeStats),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve node cpu count
|
|
||||||
node, err := client.Node(context.Background(), nodeName)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Error connecting to Proxmox", "err", err)
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
pveManager.cpuCount = node.CPUInfo.CPUs
|
|
||||||
}
|
|
||||||
|
|
||||||
return &pveManager
|
return &pveManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureInitialized checks if the PVE manager is initialized and attempts to initialize it if not.
|
||||||
|
// It returns an error if initialization fails or if a retry is pending.
|
||||||
|
func (pm *pveManager) ensureInitialized(ctx context.Context) error {
|
||||||
|
if pm.client == nil {
|
||||||
|
return errors.New("PVE client not configured")
|
||||||
|
}
|
||||||
|
if pm.cpuCount > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(pm.lastInitTry) < 30*time.Second {
|
||||||
|
return errors.New("PVE initialization retry pending")
|
||||||
|
}
|
||||||
|
pm.lastInitTry = time.Now()
|
||||||
|
|
||||||
|
node, err := pm.client.Node(ctx, pm.nodeName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if node.CPUInfo.CPUs <= 0 {
|
||||||
|
return errors.New("node returned zero CPUs")
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.cpuCount = node.CPUInfo.CPUs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPVEStats returns stats for all running VMs/LXCs
|
||||||
|
func (pm *pveManager) getPVEStats() ([]*container.PveNodeStats, error) {
|
||||||
|
if err := pm.ensureInitialized(context.Background()); err != nil {
|
||||||
|
slog.Warn("Proxmox API unavailable", "err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cluster, err := pm.client.Cluster(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error getting cluster", "err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resources, err := cluster.Resources(context.Background(), "vm")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error getting resources", "err", err, "resources", resources)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
containersLength := len(resources)
|
||||||
|
resourceIds := make(map[string]struct{}, containersLength)
|
||||||
|
|
||||||
|
// only include running vms and lxcs on selected node
|
||||||
|
for _, resource := range resources {
|
||||||
|
if resource.Node == pm.nodeName && resource.Status == "running" {
|
||||||
|
resourceIds[resource.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove invalid container stats
|
||||||
|
for id := range pm.nodeStatsMap {
|
||||||
|
if _, exists := resourceIds[id]; !exists {
|
||||||
|
delete(pm.nodeStatsMap, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate stats
|
||||||
|
stats := make([]*container.PveNodeStats, 0, len(resourceIds))
|
||||||
|
for _, resource := range resources {
|
||||||
|
if _, exists := resourceIds[resource.ID]; !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resourceStats, initialized := pm.nodeStatsMap[resource.ID]
|
||||||
|
if !initialized {
|
||||||
|
resourceStats = &container.PveNodeStats{}
|
||||||
|
pm.nodeStatsMap[resource.ID] = resourceStats
|
||||||
|
}
|
||||||
|
resourceStats.Name = resource.Name
|
||||||
|
resourceStats.Id = resource.ID
|
||||||
|
resourceStats.Type = resource.Type
|
||||||
|
resourceStats.MaxCPU = resource.MaxCPU
|
||||||
|
resourceStats.MaxMem = resource.MaxMem
|
||||||
|
resourceStats.Uptime = resource.Uptime
|
||||||
|
resourceStats.DiskRead = resource.DiskRead
|
||||||
|
resourceStats.DiskWrite = resource.DiskWrite
|
||||||
|
resourceStats.Disk = resource.MaxDisk
|
||||||
|
|
||||||
|
// prevent first run from sending all prev sent/recv bytes
|
||||||
|
total_sent := resource.NetOut
|
||||||
|
total_recv := resource.NetIn
|
||||||
|
var sent_delta, recv_delta float64
|
||||||
|
if initialized {
|
||||||
|
secondsElapsed := time.Since(resourceStats.PrevReadTime).Seconds()
|
||||||
|
if secondsElapsed > 0 {
|
||||||
|
sent_delta = float64(total_sent-resourceStats.PrevNet.Sent) / secondsElapsed
|
||||||
|
recv_delta = float64(total_recv-resourceStats.PrevNet.Recv) / secondsElapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resourceStats.PrevNet.Sent = total_sent
|
||||||
|
resourceStats.PrevNet.Recv = total_recv
|
||||||
|
resourceStats.PrevReadTime = time.Now()
|
||||||
|
|
||||||
|
// Update final stats values
|
||||||
|
resourceStats.Cpu = twoDecimals(100.0 * resource.CPU * float64(resource.MaxCPU) / float64(pm.cpuCount))
|
||||||
|
resourceStats.Mem = bytesToMegabytes(float64(resource.Mem))
|
||||||
|
resourceStats.Bandwidth = [2]uint64{uint64(sent_delta), uint64(recv_delta)}
|
||||||
|
resourceStats.NetOut = total_sent
|
||||||
|
resourceStats.NetIn = total_recv
|
||||||
|
|
||||||
|
stats = append(stats, resourceStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|||||||
92
agent/pve_test.go
Normal file
92
agent/pve_test.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/henrygd/beszel/internal/entities/container"
|
||||||
|
"github.com/luthermonson/go-proxmox"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewPVEManagerDoesNotConnectAtStartup(t *testing.T) {
|
||||||
|
t.Setenv("BESZEL_AGENT_PROXMOX_URL", "https://127.0.0.1:1/api2/json")
|
||||||
|
t.Setenv("BESZEL_AGENT_PROXMOX_NODE", "pve")
|
||||||
|
t.Setenv("BESZEL_AGENT_PROXMOX_TOKENID", "root@pam!test")
|
||||||
|
t.Setenv("BESZEL_AGENT_PROXMOX_SECRET", "secret")
|
||||||
|
|
||||||
|
pm := newPVEManager()
|
||||||
|
require.NotNil(t, pm)
|
||||||
|
assert.Zero(t, pm.cpuCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPVEManagerRetriesInitialization(t *testing.T) {
|
||||||
|
var nodeRequests atomic.Int32
|
||||||
|
var clusterRequests atomic.Int32
|
||||||
|
|
||||||
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api2/json/nodes/pve/status":
|
||||||
|
nodeRequests.Add(1)
|
||||||
|
fmt.Fprint(w, `{"data":{"cpuinfo":{"cpus":8}}}`)
|
||||||
|
case "/api2/json/cluster/status":
|
||||||
|
fmt.Fprint(w, `{"data":[{"type":"cluster","name":"test-cluster","id":"test-cluster","version":1,"quorate":1}]}`)
|
||||||
|
case "/api2/json/cluster/resources":
|
||||||
|
clusterRequests.Add(1)
|
||||||
|
fmt.Fprint(w, `{"data":[{"id":"qemu/101","type":"qemu","node":"pve","status":"running","name":"vm-101","cpu":0.5,"maxcpu":4,"maxmem":4096,"mem":2048,"netin":1024,"netout":2048,"diskread":10,"diskwrite":20,"maxdisk":8192,"uptime":60}]}`)
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
pm := &pveManager{
|
||||||
|
client: proxmox.NewClient(server.URL+"/api2/json",
|
||||||
|
proxmox.WithHTTPClient(&http.Client{
|
||||||
|
Transport: &failOnceRoundTripper{
|
||||||
|
base: server.Client().Transport,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
proxmox.WithAPIToken("root@pam!test", "secret"),
|
||||||
|
),
|
||||||
|
nodeName: "pve",
|
||||||
|
nodeStatsMap: make(map[string]*container.PveNodeStats),
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := pm.getPVEStats()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, stats)
|
||||||
|
assert.Zero(t, pm.cpuCount)
|
||||||
|
|
||||||
|
pm.lastInitTry = time.Now().Add(-31 * time.Second)
|
||||||
|
stats, err = pm.getPVEStats()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, stats, 1)
|
||||||
|
assert.Equal(t, int32(1), nodeRequests.Load())
|
||||||
|
assert.Equal(t, int32(1), clusterRequests.Load())
|
||||||
|
assert.Equal(t, 8, pm.cpuCount)
|
||||||
|
assert.Equal(t, "qemu/101", stats[0].Id)
|
||||||
|
assert.Equal(t, 25.0, stats[0].Cpu)
|
||||||
|
assert.Equal(t, uint64(1024), stats[0].NetIn)
|
||||||
|
assert.Equal(t, uint64(2048), stats[0].NetOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
type failOnceRoundTripper struct {
|
||||||
|
base http.RoundTripper
|
||||||
|
failed atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt *failOnceRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
if req.URL.Path == "/api2/json/nodes/pve/status" && !rt.failed.Swap(true) {
|
||||||
|
return nil, errors.New("dial tcp 127.0.0.1:8006: connect: connection refused")
|
||||||
|
}
|
||||||
|
return rt.base.RoundTrip(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ http.RoundTripper = (*failOnceRoundTripper)(nil)
|
||||||
@@ -157,8 +157,13 @@ type PveNodeStats struct {
|
|||||||
|
|
||||||
// fields used for pve_vms table
|
// fields used for pve_vms table
|
||||||
|
|
||||||
MaxCPU uint64 `json:"-" cbor:"10,keyasint,omitzero"` // PVE: max vCPU count
|
MaxCPU uint64 `json:"-" cbor:"10,keyasint,omitzero"` // PVE: max vCPU count
|
||||||
MaxMem uint64 `json:"-" cbor:"11,keyasint,omitzero"` // PVE: max memory bytes
|
MaxMem uint64 `json:"-" cbor:"11,keyasint,omitzero"` // PVE: max memory bytes
|
||||||
Uptime uint64 `json:"-" cbor:"12,keyasint,omitzero"` // PVE: uptime in seconds
|
Uptime uint64 `json:"-" cbor:"12,keyasint,omitzero"` // PVE: uptime in seconds
|
||||||
Type string `json:"-" cbor:"13,keyasint,omitzero"` // PVE: resource type (e.g. "qemu" or "lxc")
|
Type string `json:"-" cbor:"13,keyasint,omitzero"` // PVE: resource type (e.g. "qemu" or "lxc")
|
||||||
|
DiskRead uint64 `json:"-" cbor:"14,keyasint,omitzero"` // PVE: cumulative disk read bytes
|
||||||
|
DiskWrite uint64 `json:"-" cbor:"15,keyasint,omitzero"` // PVE: cumulative disk write bytes
|
||||||
|
Disk uint64 `json:"-" cbor:"16,keyasint,omitzero"` // PVE: allocated disk size in bytes
|
||||||
|
NetOut uint64 `json:"-" cbor:"17,keyasint,omitzero"` // PVE: cumulative bytes sent by VM
|
||||||
|
NetIn uint64 `json:"-" cbor:"18,keyasint,omitzero"` // PVE: cumulative bytes received by VM
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ func createPVEVMRecords(app core.App, data []*container.PveNodeStats, systemId s
|
|||||||
valueStrings := make([]string, 0, len(data))
|
valueStrings := make([]string, 0, len(data))
|
||||||
for i, vm := range data {
|
for i, vm := range data {
|
||||||
suffix := fmt.Sprintf("%d", i)
|
suffix := fmt.Sprintf("%d", i)
|
||||||
valueStrings = append(valueStrings, fmt.Sprintf("({:id%[1]s}, {:system}, {:name%[1]s}, {:type%[1]s}, {:cpu%[1]s}, {:mem%[1]s}, {:net%[1]s}, {:maxcpu%[1]s}, {:maxmem%[1]s}, {:uptime%[1]s}, {:updated})", suffix))
|
valueStrings = append(valueStrings, fmt.Sprintf("({:id%[1]s}, {:system}, {:name%[1]s}, {:type%[1]s}, {:cpu%[1]s}, {:mem%[1]s}, {:netout%[1]s}, {:netin%[1]s}, {:maxcpu%[1]s}, {:maxmem%[1]s}, {:uptime%[1]s}, {:diskread%[1]s}, {:diskwrite%[1]s}, {:disk%[1]s}, {:updated})", suffix))
|
||||||
params["id"+suffix] = makeStableHashId(systemId, vm.Id)
|
params["id"+suffix] = makeStableHashId(systemId, vm.Id)
|
||||||
params["name"+suffix] = vm.Name
|
params["name"+suffix] = vm.Name
|
||||||
params["type"+suffix] = vm.Type // "qemu" or "lxc"
|
params["type"+suffix] = vm.Type // "qemu" or "lxc"
|
||||||
@@ -376,11 +376,14 @@ func createPVEVMRecords(app core.App, data []*container.PveNodeStats, systemId s
|
|||||||
params["maxcpu"+suffix] = vm.MaxCPU
|
params["maxcpu"+suffix] = vm.MaxCPU
|
||||||
params["maxmem"+suffix] = vm.MaxMem
|
params["maxmem"+suffix] = vm.MaxMem
|
||||||
params["uptime"+suffix] = vm.Uptime
|
params["uptime"+suffix] = vm.Uptime
|
||||||
netBytes := vm.Bandwidth[0] + vm.Bandwidth[1]
|
params["diskread"+suffix] = vm.DiskRead
|
||||||
params["net"+suffix] = netBytes
|
params["diskwrite"+suffix] = vm.DiskWrite
|
||||||
|
params["disk"+suffix] = vm.Disk
|
||||||
|
params["netout"+suffix] = vm.NetOut // cumulative bytes sent by VM
|
||||||
|
params["netin"+suffix] = vm.NetIn // cumulative bytes received by VM
|
||||||
}
|
}
|
||||||
queryString := fmt.Sprintf(
|
queryString := fmt.Sprintf(
|
||||||
"INSERT INTO pve_vms (id, system, name, type, cpu, mem, net, maxcpu, maxmem, uptime, updated) VALUES %s ON CONFLICT(id) DO UPDATE SET system=excluded.system, name=excluded.name, type=excluded.type, cpu=excluded.cpu, mem=excluded.mem, net=excluded.net, maxcpu=excluded.maxcpu, maxmem=excluded.maxmem, uptime=excluded.uptime, updated=excluded.updated",
|
"INSERT INTO pve_vms (id, system, name, type, cpu, mem, netout, netin, maxcpu, maxmem, uptime, diskread, diskwrite, disk, updated) VALUES %s ON CONFLICT(id) DO UPDATE SET system=excluded.system, name=excluded.name, type=excluded.type, cpu=excluded.cpu, mem=excluded.mem, netout=excluded.netout, netin=excluded.netin, maxcpu=excluded.maxcpu, maxmem=excluded.maxmem, uptime=excluded.uptime, diskread=excluded.diskread, diskwrite=excluded.diskwrite, disk=excluded.disk, updated=excluded.updated",
|
||||||
strings.Join(valueStrings, ","),
|
strings.Join(valueStrings, ","),
|
||||||
)
|
)
|
||||||
_, err := app.DB().NewQuery(queryString).Bind(params).Execute()
|
_, err := app.DB().NewQuery(queryString).Bind(params).Execute()
|
||||||
|
|||||||
@@ -1847,30 +1847,6 @@ func init() {
|
|||||||
"system": false,
|
"system": false,
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "pve_vms_mem001",
|
|
||||||
"max": null,
|
|
||||||
"min": 0,
|
|
||||||
"name": "mem",
|
|
||||||
"onlyInt": false,
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "pve_vms_net001",
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"name": "net",
|
|
||||||
"onlyInt": false,
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"id": "number1253106325",
|
"id": "number1253106325",
|
||||||
@@ -1883,6 +1859,18 @@ func init() {
|
|||||||
"system": false,
|
"system": false,
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "pve_vms_mem001",
|
||||||
|
"max": null,
|
||||||
|
"min": 0,
|
||||||
|
"name": "mem",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"id": "number1693954525",
|
"id": "number1693954525",
|
||||||
@@ -1895,6 +1883,66 @@ func init() {
|
|||||||
"system": false,
|
"system": false,
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number208985346",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "disk",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number4125810518",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "diskread",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number752404475",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "diskwrite",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number1880667380",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "netout",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number2702533949",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "netin",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"id": "number1563400775",
|
"id": "number1563400775",
|
||||||
@@ -1932,7 +1980,6 @@ func init() {
|
|||||||
"updateRule": null,
|
"updateRule": null,
|
||||||
"viewRule": null
|
"viewRule": null
|
||||||
}
|
}
|
||||||
|
|
||||||
]`
|
]`
|
||||||
|
|
||||||
err := app.ImportCollectionsByMarshaledJSON([]byte(jsonData), false)
|
err := app.ImportCollectionsByMarshaledJSON([]byte(jsonData), false)
|
||||||
@@ -3,9 +3,9 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { cn, decimalString, formatBytes, hourWithSeconds, toFixedFloat } from "@/lib/utils"
|
import { cn, decimalString, formatBytes, hourWithSeconds, toFixedFloat } from "@/lib/utils"
|
||||||
import type { PveVmRecord } from "@/types"
|
import type { PveVmRecord } from "@/types"
|
||||||
import {
|
import {
|
||||||
ArrowUpDownIcon,
|
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
CpuIcon,
|
CpuIcon,
|
||||||
|
HardDriveIcon,
|
||||||
MemoryStickIcon,
|
MemoryStickIcon,
|
||||||
MonitorIcon,
|
MonitorIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
@@ -42,7 +42,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
|||||||
accessorFn: (record) => record.name,
|
accessorFn: (record) => record.name,
|
||||||
header: ({ column }) => <HeaderButton column={column} name={t`Name`} Icon={MonitorIcon} />,
|
header: ({ column }) => <HeaderButton column={column} name={t`Name`} Icon={MonitorIcon} />,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
return <span className="ms-1.5 xl:w-48 block truncate">{getValue() as string}</span>
|
return <span className="ms-1 max-w-48 block truncate">{getValue() as string}</span>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
|||||||
header: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,
|
header: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const allSystems = useStore($allSystemsById)
|
const allSystems = useStore($allSystemsById)
|
||||||
return <span className="ms-1.5 xl:w-34 block truncate">{allSystems[getValue() as string]?.name ?? ""}</span>
|
return <span className="ms-1 max-w-34 block truncate">{allSystems[getValue() as string]?.name ?? ""}</span>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -68,7 +68,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
|||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const type = getValue() as string
|
const type = getValue() as string
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className="dark:border-white/12 ms-1.5">
|
<Badge variant="outline" className="dark:border-white/12 ms-1">
|
||||||
{type}
|
{type}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
@@ -81,7 +81,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
|||||||
header: ({ column }) => <HeaderButton column={column} name={t`CPU`} Icon={CpuIcon} />,
|
header: ({ column }) => <HeaderButton column={column} name={t`CPU`} Icon={CpuIcon} />,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const val = getValue() as number
|
const val = getValue() as number
|
||||||
return <span className="ms-1.5 tabular-nums">{`${decimalString(val, val >= 10 ? 1 : 2)}%`}</span>
|
return <span className="ms-1 tabular-nums">{`${decimalString(val, val >= 10 ? 1 : 2)}%`}</span>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -93,41 +93,86 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
|||||||
const val = getValue() as number
|
const val = getValue() as number
|
||||||
const formatted = formatBytes(val, false, undefined, true)
|
const formatted = formatBytes(val, false, undefined, true)
|
||||||
return (
|
return (
|
||||||
<span className="ms-1.5 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
<span className="ms-1 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "net",
|
id: "maxmem",
|
||||||
accessorFn: (record) => record.net,
|
accessorFn: (record) => record.maxmem,
|
||||||
|
header: ({ column }) => <HeaderButton column={column} name={t`Max`} Icon={MemoryStickIcon} />,
|
||||||
invertSorting: true,
|
invertSorting: true,
|
||||||
header: ({ column }) => <HeaderButton column={column} name={t`Net`} Icon={EthernetIcon} />,
|
cell: ({ getValue }) => {
|
||||||
|
// maxmem is stored in bytes; convert to MB for formatBytes
|
||||||
|
const formatted = formatBytes(getValue() as number, false, undefined, false)
|
||||||
|
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "disk",
|
||||||
|
accessorFn: (record) => record.disk,
|
||||||
|
invertSorting: true,
|
||||||
|
header: ({ column }) => <HeaderButton column={column} name={t`Disk`} Icon={HardDriveIcon} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const formatted = formatBytes(getValue() as number, false, undefined, false)
|
||||||
|
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "diskread",
|
||||||
|
accessorFn: (record) => record.diskread,
|
||||||
|
invertSorting: true,
|
||||||
|
header: ({ column }) => <HeaderButton column={column} name={t`Read`} Icon={HardDriveIcon} />,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const val = getValue() as number
|
const val = getValue() as number
|
||||||
const formatted = formatBytes(val, true, undefined, false)
|
const formatted = formatBytes(val, false, undefined, false)
|
||||||
|
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "diskwrite",
|
||||||
|
accessorFn: (record) => record.diskwrite,
|
||||||
|
invertSorting: true,
|
||||||
|
header: ({ column }) => <HeaderButton column={column} name={t`Write`} Icon={HardDriveIcon} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const val = getValue() as number
|
||||||
|
const formatted = formatBytes(val, false, undefined, false)
|
||||||
|
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "netin",
|
||||||
|
accessorFn: (record) => record.netin,
|
||||||
|
invertSorting: true,
|
||||||
|
header: ({ column }) => <HeaderButton column={column} name={t`Download`} Icon={EthernetIcon} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const val = getValue() as number
|
||||||
|
const formatted = formatBytes(val, false, undefined, false)
|
||||||
return (
|
return (
|
||||||
<span className="ms-1.5 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
<span className="ms-1 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "netout",
|
||||||
|
accessorFn: (record) => record.netout,
|
||||||
|
invertSorting: true,
|
||||||
|
header: ({ column }) => <HeaderButton column={column} name={t`Upload`} Icon={EthernetIcon} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const val = getValue() as number
|
||||||
|
const formatted = formatBytes(val, false, undefined, false)
|
||||||
|
return (
|
||||||
|
<span className="ms-1 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "maxcpu",
|
id: "maxcpu",
|
||||||
accessorFn: (record) => record.maxcpu,
|
accessorFn: (record) => record.maxcpu,
|
||||||
header: ({ column }) => <HeaderButton column={column} name={t`vCPUs`} Icon={CpuIcon} />,
|
header: ({ column }) => <HeaderButton column={column} name="vCPUs" Icon={CpuIcon} />,
|
||||||
invertSorting: true,
|
invertSorting: true,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
return <span className="ms-1.5 tabular-nums">{getValue() as number}</span>
|
return <span className="ms-1 tabular-nums">{getValue() as number}</span>
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "maxmem",
|
|
||||||
accessorFn: (record) => record.maxmem,
|
|
||||||
header: ({ column }) => <HeaderButton column={column} name={t`Max Mem`} Icon={MemoryStickIcon} />,
|
|
||||||
invertSorting: true,
|
|
||||||
cell: ({ getValue }) => {
|
|
||||||
// maxmem is stored in bytes; convert to MB for formatBytes
|
|
||||||
const formatted = formatBytes(getValue() as number, false, undefined, false)
|
|
||||||
return <span className="ms-1.5 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -136,7 +181,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
|||||||
invertSorting: true,
|
invertSorting: true,
|
||||||
header: ({ column }) => <HeaderButton column={column} name={t`Uptime`} Icon={TimerIcon} />,
|
header: ({ column }) => <HeaderButton column={column} name={t`Uptime`} Icon={TimerIcon} />,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
return <span className="ms-1.5 w-25 block truncate">{formatUptime(getValue() as number)}</span>
|
return <span className="ms-1">{formatUptime(getValue() as number)}</span>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -146,7 +191,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
|||||||
header: ({ column }) => <HeaderButton column={column} name={t`Updated`} Icon={ClockIcon} />,
|
header: ({ column }) => <HeaderButton column={column} name={t`Updated`} Icon={ClockIcon} />,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const timestamp = getValue() as number
|
const timestamp = getValue() as number
|
||||||
return <span className="ms-1.5 tabular-nums">{hourWithSeconds(new Date(timestamp).toISOString())}</span>
|
return <span className="ms-1 tabular-nums">{hourWithSeconds(new Date(timestamp).toISOString())}</span>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -164,7 +209,7 @@ function HeaderButton({ column, name, Icon }: { column: Column<PveVmRecord>; nam
|
|||||||
>
|
>
|
||||||
{Icon && <Icon className="size-4" />}
|
{Icon && <Icon className="size-4" />}
|
||||||
{name}
|
{name}
|
||||||
<ArrowUpDownIcon className="size-4" />
|
{/* <ArrowUpDownIcon className="size-4" /> */}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export default function PveTable({ systemId }: { systemId?: string }) {
|
|||||||
function fetchData(systemId?: string) {
|
function fetchData(systemId?: string) {
|
||||||
pb.collection<PveVmRecord>("pve_vms")
|
pb.collection<PveVmRecord>("pve_vms")
|
||||||
.getList(0, 2000, {
|
.getList(0, 2000, {
|
||||||
fields: "id,name,type,cpu,mem,net,maxcpu,maxmem,uptime,system,updated",
|
|
||||||
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
|
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
|
||||||
})
|
})
|
||||||
.then(({ items }) => {
|
.then(({ items }) => {
|
||||||
@@ -145,7 +144,7 @@ export default function PveTable({ systemId }: { systemId?: string }) {
|
|||||||
<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">
|
<CardTitle className="mb-2">
|
||||||
<Trans>All Proxmox VMs</Trans>
|
<Trans>Proxmox Resources</Trans>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="flex">
|
<CardDescription className="flex">
|
||||||
<Trans>CPU is percent of overall host CPU usage.</Trans>
|
<Trans>CPU is percent of overall host CPU usage.</Trans>
|
||||||
@@ -259,7 +258,11 @@ function PveVmSheet({
|
|||||||
|
|
||||||
const memFormatted = formatBytes(vm.mem, false, undefined, true)
|
const memFormatted = formatBytes(vm.mem, false, undefined, true)
|
||||||
const maxMemFormatted = formatBytes(vm.maxmem, false, undefined, false)
|
const maxMemFormatted = formatBytes(vm.maxmem, false, undefined, false)
|
||||||
const netFormatted = formatBytes(vm.net, true, undefined, false)
|
const netoutFormatted = formatBytes(vm.netout, false, undefined, false)
|
||||||
|
const netinFormatted = formatBytes(vm.netin, false, undefined, false)
|
||||||
|
const diskReadFormatted = formatBytes(vm.diskread, false, undefined, false)
|
||||||
|
const diskWriteFormatted = formatBytes(vm.diskwrite, false, undefined, false)
|
||||||
|
const diskFormatted = formatBytes(vm.disk, false, undefined, false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||||
@@ -294,9 +297,14 @@ function PveVmSheet({
|
|||||||
<dd className="tabular-nums">{`${decimalString(memFormatted.value, memFormatted.value >= 10 ? 1 : 2)} ${memFormatted.unit}`}</dd>
|
<dd className="tabular-nums">{`${decimalString(memFormatted.value, memFormatted.value >= 10 ? 1 : 2)} ${memFormatted.unit}`}</dd>
|
||||||
|
|
||||||
<dt className="text-muted-foreground">
|
<dt className="text-muted-foreground">
|
||||||
<Trans>Network</Trans>
|
<Trans>Upload</Trans>
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="tabular-nums">{`${decimalString(netFormatted.value, netFormatted.value >= 10 ? 1 : 2)} ${netFormatted.unit}`}</dd>
|
<dd className="tabular-nums">{`${decimalString(netoutFormatted.value, netoutFormatted.value >= 10 ? 1 : 2)} ${netoutFormatted.unit}`}</dd>
|
||||||
|
|
||||||
|
<dt className="text-muted-foreground">
|
||||||
|
<Trans>Download</Trans>
|
||||||
|
</dt>
|
||||||
|
<dd className="tabular-nums">{`${decimalString(netinFormatted.value, netinFormatted.value >= 10 ? 1 : 2)} ${netinFormatted.unit}`}</dd>
|
||||||
|
|
||||||
<dt className="text-muted-foreground">
|
<dt className="text-muted-foreground">
|
||||||
<Trans>vCPUs</Trans>
|
<Trans>vCPUs</Trans>
|
||||||
@@ -308,6 +316,21 @@ function PveVmSheet({
|
|||||||
</dt>
|
</dt>
|
||||||
<dd className="tabular-nums">{`${decimalString(maxMemFormatted.value, maxMemFormatted.value >= 10 ? 1 : 2)} ${maxMemFormatted.unit}`}</dd>
|
<dd className="tabular-nums">{`${decimalString(maxMemFormatted.value, maxMemFormatted.value >= 10 ? 1 : 2)} ${maxMemFormatted.unit}`}</dd>
|
||||||
|
|
||||||
|
<dt className="text-muted-foreground">
|
||||||
|
<Trans>Disk Read</Trans>
|
||||||
|
</dt>
|
||||||
|
<dd className="tabular-nums">{`${decimalString(diskReadFormatted.value, diskReadFormatted.value >= 10 ? 1 : 2)} ${diskReadFormatted.unit}`}</dd>
|
||||||
|
|
||||||
|
<dt className="text-muted-foreground">
|
||||||
|
<Trans>Disk Write</Trans>
|
||||||
|
</dt>
|
||||||
|
<dd className="tabular-nums">{`${decimalString(diskWriteFormatted.value, diskWriteFormatted.value >= 10 ? 1 : 2)} ${diskWriteFormatted.unit}`}</dd>
|
||||||
|
|
||||||
|
<dt className="text-muted-foreground">
|
||||||
|
<Trans>Disk Size</Trans>
|
||||||
|
</dt>
|
||||||
|
<dd className="tabular-nums">{`${decimalString(diskFormatted.value, diskFormatted.value >= 10 ? 1 : 2)} ${diskFormatted.unit}`}</dd>
|
||||||
|
|
||||||
<dt className="text-muted-foreground">
|
<dt className="text-muted-foreground">
|
||||||
<Trans>Uptime</Trans>
|
<Trans>Uptime</Trans>
|
||||||
</dt>
|
</dt>
|
||||||
|
|||||||
@@ -952,6 +952,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
<LazyContainersTable systemId={system.id} />
|
<LazyContainersTable systemId={system.id} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{pveData.length > 0 && <LazyPveTable systemId={system.id} />}
|
||||||
|
|
||||||
{isLinux && compareSemVer(chartData.agentVersion, parseSemVer("0.16.0")) >= 0 && (
|
{isLinux && compareSemVer(chartData.agentVersion, parseSemVer("0.16.0")) >= 0 && (
|
||||||
<LazySystemdTable systemId={system.id} />
|
<LazySystemdTable systemId={system.id} />
|
||||||
)}
|
)}
|
||||||
@@ -1126,6 +1128,17 @@ function LazyContainersTable({ systemId }: { systemId: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PveTable = lazy(() => import("../pve-table/pve-table"))
|
||||||
|
|
||||||
|
function LazyPveTable({ systemId }: { systemId: string }) {
|
||||||
|
const { isIntersecting, ref } = useIntersectionObserver({ rootMargin: "90px" })
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={cn(isIntersecting && "contents")}>
|
||||||
|
{isIntersecting && <PveTable systemId={systemId} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SmartTable = lazy(() => import("./system/smart-table"))
|
const SmartTable = lazy(() => import("./system/smart-table"))
|
||||||
|
|
||||||
function LazySmartTable({ systemId }: { systemId: string }) {
|
function LazySmartTable({ systemId }: { systemId: string }) {
|
||||||
|
|||||||
238
internal/site/src/types.d.ts
vendored
238
internal/site/src/types.d.ts
vendored
@@ -285,14 +285,22 @@ export interface PveVmRecord extends RecordModel {
|
|||||||
cpu: number
|
cpu: number
|
||||||
/** Memory used (MB) */
|
/** Memory used (MB) */
|
||||||
mem: number
|
mem: number
|
||||||
/** Network bandwidth (bytes/s, combined send+recv) */
|
/** Total upload (bytes, sent by VM) */
|
||||||
net: number
|
netout: number
|
||||||
|
/** Total download (bytes, received by VM) */
|
||||||
|
netin: number
|
||||||
/** Max vCPU count */
|
/** Max vCPU count */
|
||||||
maxcpu: number
|
maxcpu: number
|
||||||
/** Max memory (bytes) */
|
/** Max memory (bytes) */
|
||||||
maxmem: number
|
maxmem: number
|
||||||
/** Uptime (seconds) */
|
/** Uptime (seconds) */
|
||||||
uptime: number
|
uptime: number
|
||||||
|
/** Cumulative disk read (bytes) */
|
||||||
|
diskread: number
|
||||||
|
/** Cumulative disk write (bytes) */
|
||||||
|
diskwrite: number
|
||||||
|
/** Allocated disk size (bytes) */
|
||||||
|
disk: number
|
||||||
/** Unix timestamp (ms) */
|
/** Unix timestamp (ms) */
|
||||||
updated: number
|
updated: number
|
||||||
}
|
}
|
||||||
@@ -447,116 +455,116 @@ export interface SystemdRecord extends RecordModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemdServiceDetails {
|
export interface SystemdServiceDetails {
|
||||||
AccessSELinuxContext: string;
|
AccessSELinuxContext: string
|
||||||
ActivationDetails: any[];
|
ActivationDetails: any[]
|
||||||
ActiveEnterTimestamp: number;
|
ActiveEnterTimestamp: number
|
||||||
ActiveEnterTimestampMonotonic: number;
|
ActiveEnterTimestampMonotonic: number
|
||||||
ActiveExitTimestamp: number;
|
ActiveExitTimestamp: number
|
||||||
ActiveExitTimestampMonotonic: number;
|
ActiveExitTimestampMonotonic: number
|
||||||
ActiveState: string;
|
ActiveState: string
|
||||||
After: string[];
|
After: string[]
|
||||||
AllowIsolate: boolean;
|
AllowIsolate: boolean
|
||||||
AssertResult: boolean;
|
AssertResult: boolean
|
||||||
AssertTimestamp: number;
|
AssertTimestamp: number
|
||||||
AssertTimestampMonotonic: number;
|
AssertTimestampMonotonic: number
|
||||||
Asserts: any[];
|
Asserts: any[]
|
||||||
Before: string[];
|
Before: string[]
|
||||||
BindsTo: any[];
|
BindsTo: any[]
|
||||||
BoundBy: any[];
|
BoundBy: any[]
|
||||||
CPUUsageNSec: number;
|
CPUUsageNSec: number
|
||||||
CanClean: any[];
|
CanClean: any[]
|
||||||
CanFreeze: boolean;
|
CanFreeze: boolean
|
||||||
CanIsolate: boolean;
|
CanIsolate: boolean
|
||||||
CanLiveMount: boolean;
|
CanLiveMount: boolean
|
||||||
CanReload: boolean;
|
CanReload: boolean
|
||||||
CanStart: boolean;
|
CanStart: boolean
|
||||||
CanStop: boolean;
|
CanStop: boolean
|
||||||
CollectMode: string;
|
CollectMode: string
|
||||||
ConditionResult: boolean;
|
ConditionResult: boolean
|
||||||
ConditionTimestamp: number;
|
ConditionTimestamp: number
|
||||||
ConditionTimestampMonotonic: number;
|
ConditionTimestampMonotonic: number
|
||||||
Conditions: any[];
|
Conditions: any[]
|
||||||
ConflictedBy: any[];
|
ConflictedBy: any[]
|
||||||
Conflicts: string[];
|
Conflicts: string[]
|
||||||
ConsistsOf: any[];
|
ConsistsOf: any[]
|
||||||
DebugInvocation: boolean;
|
DebugInvocation: boolean
|
||||||
DefaultDependencies: boolean;
|
DefaultDependencies: boolean
|
||||||
Description: string;
|
Description: string
|
||||||
Documentation: string[];
|
Documentation: string[]
|
||||||
DropInPaths: any[];
|
DropInPaths: any[]
|
||||||
ExecMainPID: number;
|
ExecMainPID: number
|
||||||
FailureAction: string;
|
FailureAction: string
|
||||||
FailureActionExitStatus: number;
|
FailureActionExitStatus: number
|
||||||
Following: string;
|
Following: string
|
||||||
FragmentPath: string;
|
FragmentPath: string
|
||||||
FreezerState: string;
|
FreezerState: string
|
||||||
Id: string;
|
Id: string
|
||||||
IgnoreOnIsolate: boolean;
|
IgnoreOnIsolate: boolean
|
||||||
InactiveEnterTimestamp: number;
|
InactiveEnterTimestamp: number
|
||||||
InactiveEnterTimestampMonotonic: number;
|
InactiveEnterTimestampMonotonic: number
|
||||||
InactiveExitTimestamp: number;
|
InactiveExitTimestamp: number
|
||||||
InactiveExitTimestampMonotonic: number;
|
InactiveExitTimestampMonotonic: number
|
||||||
InvocationID: string;
|
InvocationID: string
|
||||||
Job: Array<number | string>;
|
Job: Array<number | string>
|
||||||
JobRunningTimeoutUSec: number;
|
JobRunningTimeoutUSec: number
|
||||||
JobTimeoutAction: string;
|
JobTimeoutAction: string
|
||||||
JobTimeoutRebootArgument: string;
|
JobTimeoutRebootArgument: string
|
||||||
JobTimeoutUSec: number;
|
JobTimeoutUSec: number
|
||||||
JoinsNamespaceOf: any[];
|
JoinsNamespaceOf: any[]
|
||||||
LoadError: string[];
|
LoadError: string[]
|
||||||
LoadState: string;
|
LoadState: string
|
||||||
MainPID: number;
|
MainPID: number
|
||||||
Markers: any[];
|
Markers: any[]
|
||||||
MemoryCurrent: number;
|
MemoryCurrent: number
|
||||||
MemoryLimit: number;
|
MemoryLimit: number
|
||||||
MemoryPeak: number;
|
MemoryPeak: number
|
||||||
NRestarts: number;
|
NRestarts: number
|
||||||
Names: string[];
|
Names: string[]
|
||||||
NeedDaemonReload: boolean;
|
NeedDaemonReload: boolean
|
||||||
OnFailure: any[];
|
OnFailure: any[]
|
||||||
OnFailureJobMode: string;
|
OnFailureJobMode: string
|
||||||
OnFailureOf: any[];
|
OnFailureOf: any[]
|
||||||
OnSuccess: any[];
|
OnSuccess: any[]
|
||||||
OnSuccessJobMode: string;
|
OnSuccessJobMode: string
|
||||||
OnSuccessOf: any[];
|
OnSuccessOf: any[]
|
||||||
PartOf: any[];
|
PartOf: any[]
|
||||||
Perpetual: boolean;
|
Perpetual: boolean
|
||||||
PropagatesReloadTo: any[];
|
PropagatesReloadTo: any[]
|
||||||
PropagatesStopTo: any[];
|
PropagatesStopTo: any[]
|
||||||
RebootArgument: string;
|
RebootArgument: string
|
||||||
Refs: any[];
|
Refs: any[]
|
||||||
RefuseManualStart: boolean;
|
RefuseManualStart: boolean
|
||||||
RefuseManualStop: boolean;
|
RefuseManualStop: boolean
|
||||||
ReloadPropagatedFrom: any[];
|
ReloadPropagatedFrom: any[]
|
||||||
RequiredBy: any[];
|
RequiredBy: any[]
|
||||||
Requires: string[];
|
Requires: string[]
|
||||||
RequiresMountsFor: any[];
|
RequiresMountsFor: any[]
|
||||||
Requisite: any[];
|
Requisite: any[]
|
||||||
RequisiteOf: any[];
|
RequisiteOf: any[]
|
||||||
Result: string;
|
Result: string
|
||||||
SliceOf: any[];
|
SliceOf: any[]
|
||||||
SourcePath: string;
|
SourcePath: string
|
||||||
StartLimitAction: string;
|
StartLimitAction: string
|
||||||
StartLimitBurst: number;
|
StartLimitBurst: number
|
||||||
StartLimitIntervalUSec: number;
|
StartLimitIntervalUSec: number
|
||||||
StateChangeTimestamp: number;
|
StateChangeTimestamp: number
|
||||||
StateChangeTimestampMonotonic: number;
|
StateChangeTimestampMonotonic: number
|
||||||
StopPropagatedFrom: any[];
|
StopPropagatedFrom: any[]
|
||||||
StopWhenUnneeded: boolean;
|
StopWhenUnneeded: boolean
|
||||||
SubState: string;
|
SubState: string
|
||||||
SuccessAction: string;
|
SuccessAction: string
|
||||||
SuccessActionExitStatus: number;
|
SuccessActionExitStatus: number
|
||||||
SurviveFinalKillSignal: boolean;
|
SurviveFinalKillSignal: boolean
|
||||||
TasksCurrent: number;
|
TasksCurrent: number
|
||||||
TasksMax: number;
|
TasksMax: number
|
||||||
Transient: boolean;
|
Transient: boolean
|
||||||
TriggeredBy: string[];
|
TriggeredBy: string[]
|
||||||
Triggers: any[];
|
Triggers: any[]
|
||||||
UnitFilePreset: string;
|
UnitFilePreset: string
|
||||||
UnitFileState: string;
|
UnitFileState: string
|
||||||
UpheldBy: any[];
|
UpheldBy: any[]
|
||||||
Upholds: any[];
|
Upholds: any[]
|
||||||
WantedBy: any[];
|
WantedBy: any[]
|
||||||
Wants: string[];
|
Wants: string[]
|
||||||
WantsMountsFor: any[];
|
WantsMountsFor: any[]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user