mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-22 01:17:48 +02:00
Compare commits
4 Commits
755-xpu-sm
...
c74e7430ef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c74e7430ef | ||
|
|
2467bbc0f0 | ||
|
|
ea665e02da | ||
|
|
358e05d544 |
@@ -4,7 +4,6 @@ import (
|
|||||||
"beszel/internal/entities/system"
|
"beszel/internal/entities/system"
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/csv"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -22,13 +21,11 @@ const (
|
|||||||
nvidiaSmiCmd = "nvidia-smi"
|
nvidiaSmiCmd = "nvidia-smi"
|
||||||
rocmSmiCmd = "rocm-smi"
|
rocmSmiCmd = "rocm-smi"
|
||||||
tegraStatsCmd = "tegrastats"
|
tegraStatsCmd = "tegrastats"
|
||||||
xpuSmiCmd = "xpu-smi"
|
|
||||||
|
|
||||||
// Polling intervals
|
// Polling intervals
|
||||||
nvidiaSmiInterval = "4" // in seconds
|
nvidiaSmiInterval = "4" // in seconds
|
||||||
tegraStatsInterval = "3700" // in milliseconds
|
tegraStatsInterval = "3700" // in milliseconds
|
||||||
rocmSmiInterval = 4300 * time.Millisecond
|
rocmSmiInterval = 4300 * time.Millisecond
|
||||||
xpuSmiInterval = 4
|
|
||||||
|
|
||||||
// Command retry and timeout constants
|
// Command retry and timeout constants
|
||||||
retryWaitTime = 5 * time.Second
|
retryWaitTime = 5 * time.Second
|
||||||
@@ -44,11 +41,10 @@ const (
|
|||||||
// GPUManager manages data collection for GPUs (either Nvidia or AMD)
|
// GPUManager manages data collection for GPUs (either Nvidia or AMD)
|
||||||
type GPUManager struct {
|
type GPUManager struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
nvidiaSmi bool
|
nvidiaSmi bool
|
||||||
rocmSmi bool
|
rocmSmi bool
|
||||||
tegrastats bool
|
tegrastats bool
|
||||||
intelXpuSmi bool
|
GpuDataMap map[string]*system.GPUData
|
||||||
GpuDataMap map[string]*system.GPUData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RocmSmiJson represents the JSON structure of rocm-smi output
|
// RocmSmiJson represents the JSON structure of rocm-smi output
|
||||||
@@ -164,59 +160,6 @@ func (gm *GPUManager) getJetsonParser() func(output []byte) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gm *GPUManager) parseIntelData(output []byte) bool {
|
|
||||||
gm.Lock()
|
|
||||||
defer gm.Unlock()
|
|
||||||
reader := csv.NewReader(bytes.NewReader(output))
|
|
||||||
records, err := reader.ReadAll()
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn("Failed to parse Intel GPU data", "err", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
header := []string{"Timestamp", "DeviceId", "GPU Power (W)", "GPU Frequency (MHz)", "GPU Memory Utilization (%)", "GPU Memory Used (MiB)"}
|
|
||||||
gpuData := &system.GPUData{Name: "GPU"}
|
|
||||||
gm.GpuDataMap["0"] = gpuData
|
|
||||||
|
|
||||||
for _, record := range records {
|
|
||||||
if strings.Join(record, ",") == strings.Join(header, ",") {
|
|
||||||
slog.Debug("Skipping header", "header", record)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var memoryUtilization *float64
|
|
||||||
var memoryUsed *float64
|
|
||||||
for i, field := range header {
|
|
||||||
if field == "Timestamp" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stripped := strings.TrimSpace(record[i])
|
|
||||||
value, err := strconv.ParseFloat(stripped, 64)
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn("Failed to parse field", "field", field, "value", stripped, "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch field {
|
|
||||||
case "GPU Power (W)":
|
|
||||||
gpuData.Power += value
|
|
||||||
case "GPU Frequency (MHz)":
|
|
||||||
gpuData.Usage += value
|
|
||||||
case "GPU Memory Utilization (%)":
|
|
||||||
memoryUtilization = &value
|
|
||||||
case "GPU Memory Used (MiB)":
|
|
||||||
memoryUsed = &value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if memoryUtilization != nil && memoryUsed != nil {
|
|
||||||
gpuData.MemoryUsed = *memoryUsed
|
|
||||||
gpuData.MemoryTotal = (*memoryUsed / *memoryUtilization) * 100 // convert to total memory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gpuData.Count++
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseNvidiaData parses the output of nvidia-smi and updates the GPUData map
|
// parseNvidiaData parses the output of nvidia-smi and updates the GPUData map
|
||||||
func (gm *GPUManager) parseNvidiaData(output []byte) bool {
|
func (gm *GPUManager) parseNvidiaData(output []byte) bool {
|
||||||
gm.Lock()
|
gm.Lock()
|
||||||
@@ -335,14 +278,10 @@ func (gm *GPUManager) detectGPUs() error {
|
|||||||
gm.tegrastats = true
|
gm.tegrastats = true
|
||||||
gm.nvidiaSmi = false
|
gm.nvidiaSmi = false
|
||||||
}
|
}
|
||||||
fmt.Println("Looking for gpus")
|
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats {
|
||||||
if _, err := exec.LookPath(xpuSmiCmd); err == nil {
|
|
||||||
gm.intelXpuSmi = true
|
|
||||||
}
|
|
||||||
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats || gm.intelXpuSmi {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("no GPU found - install nvidia-smi, rocm-smi, intel_gpu_top, or tegrastats")
|
return fmt.Errorf("no GPU found - install nvidia-smi, rocm-smi, or tegrastats")
|
||||||
}
|
}
|
||||||
|
|
||||||
// startCollector starts the appropriate GPU data collector based on the command
|
// startCollector starts the appropriate GPU data collector based on the command
|
||||||
@@ -379,10 +318,6 @@ func (gm *GPUManager) startCollector(command string) {
|
|||||||
time.Sleep(rocmSmiInterval)
|
time.Sleep(rocmSmiInterval)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
case xpuSmiCmd:
|
|
||||||
collector.cmdArgs = []string{"dump", "-d", "-1", "-m", "1,2,5,18", "-i", strconv.Itoa(xpuSmiInterval)}
|
|
||||||
collector.parse = gm.parseIntelData
|
|
||||||
go collector.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,9 +338,6 @@ func NewGPUManager() (*GPUManager, error) {
|
|||||||
if gm.tegrastats {
|
if gm.tegrastats {
|
||||||
gm.startCollector(tegraStatsCmd)
|
gm.startCollector(tegraStatsCmd)
|
||||||
}
|
}
|
||||||
if gm.intelXpuSmi {
|
|
||||||
gm.startCollector(xpuSmiCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &gm, nil
|
return &gm, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,22 @@ import (
|
|||||||
func (a *Agent) initializeSystemInfo() {
|
func (a *Agent) initializeSystemInfo() {
|
||||||
a.systemInfo.AgentVersion = beszel.Version
|
a.systemInfo.AgentVersion = beszel.Version
|
||||||
a.systemInfo.Hostname, _ = os.Hostname()
|
a.systemInfo.Hostname, _ = os.Hostname()
|
||||||
a.systemInfo.KernelVersion, _ = host.KernelVersion()
|
|
||||||
|
platform, _, version, _ := host.PlatformInformation()
|
||||||
|
|
||||||
|
if platform == "darwin" {
|
||||||
|
a.systemInfo.KernelVersion = version
|
||||||
|
a.systemInfo.Os = system.Darwin
|
||||||
|
} else if strings.Contains(platform, "indows") {
|
||||||
|
a.systemInfo.KernelVersion = strings.Replace(platform, "Microsoft ", "", 1) + " " + version
|
||||||
|
a.systemInfo.Os = system.Windows
|
||||||
|
} else {
|
||||||
|
a.systemInfo.Os = system.Linux
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.systemInfo.KernelVersion == "" {
|
||||||
|
a.systemInfo.KernelVersion, _ = host.KernelVersion()
|
||||||
|
}
|
||||||
|
|
||||||
// cpu model
|
// cpu model
|
||||||
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
||||||
|
|||||||
@@ -64,6 +64,14 @@ type NetIoStats struct {
|
|||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Os uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
Linux Os = iota
|
||||||
|
Darwin
|
||||||
|
Windows
|
||||||
|
)
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Hostname string `json:"h"`
|
Hostname string `json:"h"`
|
||||||
KernelVersion string `json:"k,omitempty"`
|
KernelVersion string `json:"k,omitempty"`
|
||||||
@@ -79,6 +87,7 @@ type Info struct {
|
|||||||
Podman bool `json:"p,omitempty"`
|
Podman bool `json:"p,omitempty"`
|
||||||
GpuPct float64 `json:"g,omitempty"`
|
GpuPct float64 `json:"g,omitempty"`
|
||||||
DashboardTemp float64 `json:"dt,omitempty"`
|
DashboardTemp float64 `json:"dt,omitempty"`
|
||||||
|
Os Os `json:"os"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final data structure to return to the hub
|
// Final data structure to return to the hub
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -19,11 +19,12 @@ import { $publicKey, pb } from "@/lib/stores"
|
|||||||
import { cn, copyToClipboard, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
|
import { cn, copyToClipboard, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
|
||||||
import { i18n } from "@lingui/core"
|
import { i18n } from "@lingui/core"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { ChevronDownIcon, Copy, PlusIcon } from "lucide-react"
|
import { ChevronDownIcon, Copy, ExternalLinkIcon, PlusIcon } from "lucide-react"
|
||||||
import { memo, useRef, useState } from "react"
|
import { memo, useRef, useState } from "react"
|
||||||
import { basePath, navigate } from "./router"
|
import { basePath, navigate } from "./router"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"
|
||||||
import { SystemRecord } from "@/types"
|
import { SystemRecord } from "@/types"
|
||||||
|
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "./ui/icons"
|
||||||
|
|
||||||
export function AddSystemButton({ className }: { className?: string }) {
|
export function AddSystemButton({ className }: { className?: string }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
@@ -72,15 +73,22 @@ function copyDockerRun(port = "45876", publicKey: string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyInstallCommand(port = "45876", publicKey: string) {
|
function copyLinuxCommand(port = "45876", publicKey: string, brew = false) {
|
||||||
let cmd = `curl -sL https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/scripts/install-agent.sh -o install-agent.sh && chmod +x install-agent.sh && ./install-agent.sh -p ${port} -k "${publicKey}"`
|
let cmd = `curl -sL https://get.beszel.dev${
|
||||||
// add china mirrors flag if zh-CN
|
brew ? "/brew" : ""
|
||||||
|
} -o /tmp/install-agent.sh && chmod +x /tmp/install-agent.sh && /tmp/install-agent.sh -p ${port} -k "${publicKey}"`
|
||||||
if ((i18n.locale + navigator.language).includes("zh-CN")) {
|
if ((i18n.locale + navigator.language).includes("zh-CN")) {
|
||||||
cmd += ` --china-mirrors`
|
cmd += ` --china-mirrors`
|
||||||
}
|
}
|
||||||
copyToClipboard(cmd)
|
copyToClipboard(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyWindowsCommand(port = "45876", publicKey: string) {
|
||||||
|
copyToClipboard(
|
||||||
|
`Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser; & iwr -useb https://get.beszel.dev -OutFile "$env:TEMP\install-agent.ps1"; & "$env:TEMP\install-agent.ps1" -Key "${publicKey}" -Port ${port}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SystemDialog component for adding or editing a system.
|
* SystemDialog component for adding or editing a system.
|
||||||
* @param {Object} props - The component props.
|
* @param {Object} props - The component props.
|
||||||
@@ -197,7 +205,7 @@ export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean
|
|||||||
className="absolute end-0 top-0"
|
className="absolute end-0 top-0"
|
||||||
onClick={() => copyToClipboard(publicKey)}
|
onClick={() => copyToClipboard(publicKey)}
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4 " />
|
<Copy className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -215,17 +223,39 @@ export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean
|
|||||||
<CopyButton
|
<CopyButton
|
||||||
text={t`Copy` + " docker compose"}
|
text={t`Copy` + " docker compose"}
|
||||||
onClick={() => copyDockerCompose(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
onClick={() => copyDockerCompose(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
||||||
dropdownText={t`Copy` + " docker run"}
|
icon={<DockerIcon className="size-4 -me-0.5" />}
|
||||||
dropdownOnClick={() => copyDockerRun(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
dropdownItems={[
|
||||||
|
{
|
||||||
|
text: t`Copy` + " docker run",
|
||||||
|
onClick: () => copyDockerRun(isUnixSocket ? hostValue : port.current?.value, publicKey),
|
||||||
|
icons: [<DockerIcon className="size-4" />],
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
{/* Binary */}
|
{/* Binary */}
|
||||||
<TabsContent value="binary" className="contents">
|
<TabsContent value="binary" className="contents">
|
||||||
<CopyButton
|
<CopyButton
|
||||||
text={t`Copy Linux command`}
|
text={t`Copy Linux command`}
|
||||||
onClick={() => copyInstallCommand(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
icon={<TuxIcon className="size-4" />}
|
||||||
dropdownText={t`Manual setup instructions`}
|
onClick={() => copyLinuxCommand(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
||||||
dropdownUrl="https://beszel.dev/guide/agent-installation#binary"
|
dropdownItems={[
|
||||||
|
{
|
||||||
|
text: t`Copy Homebrew command`,
|
||||||
|
onClick: () => copyLinuxCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, true),
|
||||||
|
icons: [<AppleIcon className="size-4" />, <TuxIcon className="w-4 h-4" />],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t`Copy Windows command`,
|
||||||
|
onClick: () => copyWindowsCommand(isUnixSocket ? hostValue : port.current?.value, publicKey),
|
||||||
|
icons: [<WindowsIcon className="size-4" />],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t`Manual setup instructions`,
|
||||||
|
url: "https://beszel.dev/guide/agent-installation#binary",
|
||||||
|
icons: [<ExternalLinkIcon className="size-4" />],
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
{/* Save */}
|
{/* Save */}
|
||||||
@@ -237,19 +267,30 @@ export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
interface DropdownItem {
|
||||||
|
text: string
|
||||||
|
onClick?: () => void
|
||||||
|
url?: string
|
||||||
|
icons?: React.ReactNode[]
|
||||||
|
}
|
||||||
|
|
||||||
interface CopyButtonProps {
|
interface CopyButtonProps {
|
||||||
text: string
|
text: string
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
dropdownText: string
|
dropdownItems: DropdownItem[]
|
||||||
dropdownOnClick?: () => void
|
icon?: React.ReactNode
|
||||||
dropdownUrl?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CopyButton = memo((props: CopyButtonProps) => {
|
const CopyButton = memo((props: CopyButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-0 rounded-lg">
|
<div className="flex gap-0 rounded-lg">
|
||||||
<Button type="button" variant="outline" onClick={props.onClick} className="rounded-e-none dark:border-e-0 grow">
|
<Button
|
||||||
{props.text}
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={props.onClick}
|
||||||
|
className="rounded-e-none dark:border-e-0 grow flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{props.text} {props.icon}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="w-px h-full bg-muted"></div>
|
<div className="w-px h-full bg-muted"></div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -259,15 +300,24 @@ const CopyButton = memo((props: CopyButtonProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
{props.dropdownUrl ? (
|
{props.dropdownItems.map((item, index) => (
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem key={index} asChild={!!item.url}>
|
||||||
<a href={props.dropdownUrl} className="cursor-pointer" target="_blank" rel="noopener noreferrer">
|
{item.url ? (
|
||||||
{props.dropdownText}
|
<a
|
||||||
</a>
|
href={item.url}
|
||||||
|
className="cursor-pointer flex items-center gap-1.5"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{item.text} {item.icons?.map((icon) => icon)}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div onClick={item.onClick} className="cursor-pointer flex items-center gap-1.5">
|
||||||
|
{item.text} {item.icons?.map((icon) => icon)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : (
|
))}
|
||||||
<DropdownMenuItem onClick={props.dropdownOnClick} className="cursor-pointer">{props.dropdownText}</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,16 +16,17 @@ import { useStore } from "@nanostores/react"
|
|||||||
import { $containerFilter } from "@/lib/stores"
|
import { $containerFilter } from "@/lib/stores"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { Separator } from "../ui/separator"
|
import { Separator } from "../ui/separator"
|
||||||
|
import { ChartType } from "@/lib/enums"
|
||||||
|
|
||||||
export default memo(function ContainerChart({
|
export default memo(function ContainerChart({
|
||||||
dataKey,
|
dataKey,
|
||||||
chartData,
|
chartData,
|
||||||
chartName,
|
chartType,
|
||||||
unit = "%",
|
unit = "%",
|
||||||
}: {
|
}: {
|
||||||
dataKey: string
|
dataKey: string
|
||||||
chartData: ChartData
|
chartData: ChartData
|
||||||
chartName: string
|
chartType: ChartType
|
||||||
unit?: string
|
unit?: string
|
||||||
}) {
|
}) {
|
||||||
const filter = useStore($containerFilter)
|
const filter = useStore($containerFilter)
|
||||||
@@ -33,7 +34,7 @@ export default memo(function ContainerChart({
|
|||||||
|
|
||||||
const { containerData } = chartData
|
const { containerData } = chartData
|
||||||
|
|
||||||
const isNetChart = chartName === "net"
|
const isNetChart = chartType === ChartType.Network
|
||||||
|
|
||||||
const chartConfig = useMemo(() => {
|
const chartConfig = useMemo(() => {
|
||||||
let config = {} as Record<
|
let config = {} as Record<
|
||||||
@@ -81,7 +82,7 @@ export default memo(function ContainerChart({
|
|||||||
tickFormatter: (value: any) => string
|
tickFormatter: (value: any) => string
|
||||||
}
|
}
|
||||||
// tick formatter
|
// tick formatter
|
||||||
if (chartName === "cpu") {
|
if (chartType === ChartType.CPU) {
|
||||||
obj.tickFormatter = (value) => {
|
obj.tickFormatter = (value) => {
|
||||||
const val = toFixedWithoutTrailingZeros(value, 2) + unit
|
const val = toFixedWithoutTrailingZeros(value, 2) + unit
|
||||||
return updateYAxisWidth(val)
|
return updateYAxisWidth(val)
|
||||||
@@ -111,6 +112,11 @@ export default memo(function ContainerChart({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (chartType === ChartType.Memory) {
|
||||||
|
obj.toolTipFormatter = (item: any) => {
|
||||||
|
const { v, u } = getSizeAndUnit(item.value, false)
|
||||||
|
return updateYAxisWidth(toFixedFloat(v, 2) + u)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
obj.toolTipFormatter = (item: any) => decimalString(item.value) + unit
|
obj.toolTipFormatter = (item: any) => decimalString(item.value) + unit
|
||||||
}
|
}
|
||||||
@@ -157,6 +163,7 @@ export default memo(function ContainerChart({
|
|||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
|
truncate={true}
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
itemSorter={(a, b) => b.value - a.value}
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { t } from "@lingui/core/macro"
|
|||||||
import { Plural, Trans } from "@lingui/react/macro"
|
import { Plural, Trans } from "@lingui/react/macro"
|
||||||
import { $systems, pb, $chartTime, $containerFilter, $userSettings, $direction, $maxValues } from "@/lib/stores"
|
import { $systems, pb, $chartTime, $containerFilter, $userSettings, $direction, $maxValues } from "@/lib/stores"
|
||||||
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
|
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
|
||||||
|
import { ChartType, Os } from "@/lib/enums"
|
||||||
import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
@@ -22,7 +23,7 @@ import { Separator } from "../ui/separator"
|
|||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
||||||
import { Button } from "../ui/button"
|
import { Button } from "../ui/button"
|
||||||
import { Input } from "../ui/input"
|
import { Input } from "../ui/input"
|
||||||
import { ChartAverage, ChartMax, Rows, TuxIcon, WindowsIcon } from "../ui/icons"
|
import { ChartAverage, ChartMax, Rows, TuxIcon, WindowsIcon, AppleIcon } from "../ui/icons"
|
||||||
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
|
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
||||||
import { timeTicks } from "d3-time"
|
import { timeTicks } from "d3-time"
|
||||||
@@ -251,12 +252,23 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
if (!system.info) {
|
if (!system.info) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let version = system.info.k ?? ""
|
|
||||||
const buildIndex = version.indexOf(" Build")
|
const osInfo = {
|
||||||
const isWindows = buildIndex !== -1
|
[Os.Linux]: {
|
||||||
if (isWindows) {
|
Icon: TuxIcon,
|
||||||
version = version.substring(0, buildIndex)
|
value: system.info.k,
|
||||||
|
label: t({ comment: "Linux kernel", message: "Kernel" }),
|
||||||
|
},
|
||||||
|
[Os.Darwin]: {
|
||||||
|
Icon: AppleIcon,
|
||||||
|
value: `macOS ${system.info.k}`,
|
||||||
|
},
|
||||||
|
[Os.Windows]: {
|
||||||
|
Icon: WindowsIcon,
|
||||||
|
value: system.info.k,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
let uptime: React.ReactNode
|
let uptime: React.ReactNode
|
||||||
if (system.info.u < 172800) {
|
if (system.info.u < 172800) {
|
||||||
const hours = Math.trunc(system.info.u / 3600)
|
const hours = Math.trunc(system.info.u / 3600)
|
||||||
@@ -274,11 +286,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
hide: system.info.h === system.host || system.info.h === system.name,
|
hide: system.info.h === system.host || system.info.h === system.name,
|
||||||
},
|
},
|
||||||
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
|
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
|
||||||
{
|
osInfo[system.info.os ?? Os.Linux],
|
||||||
value: version,
|
|
||||||
Icon: isWindows ? WindowsIcon : TuxIcon,
|
|
||||||
label: isWindows ? t`Windows build` : t({ comment: "Linux kernel", message: "Kernel" }),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ""})`,
|
value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ""})`,
|
||||||
Icon: CpuIcon,
|
Icon: CpuIcon,
|
||||||
@@ -312,7 +320,13 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const handleKeyUp = (e: KeyboardEvent) => {
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
if (
|
||||||
|
e.target instanceof HTMLInputElement ||
|
||||||
|
e.target instanceof HTMLTextAreaElement ||
|
||||||
|
e.shiftKey ||
|
||||||
|
e.ctrlKey ||
|
||||||
|
e.metaKey
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const currentIndex = systems.findIndex((s) => s.name === name)
|
const currentIndex = systems.findIndex((s) => s.name === name)
|
||||||
@@ -456,7 +470,7 @@ 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" chartName="cpu" />
|
<ContainerChart chartData={chartData} dataKey="c" chartType={ChartType.CPU} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -477,7 +491,7 @@ 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} chartName="mem" dataKey="m" unit=" MB" />
|
<ContainerChart chartData={chartData} dataKey="m" chartType={ChartType.Memory} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -519,7 +533,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
cornerEl={containerFilterBar}
|
cornerEl={containerFilterBar}
|
||||||
>
|
>
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<ContainerChart chartData={chartData} chartName="net" dataKey="n" />
|
<ContainerChart chartData={chartData} chartType={ChartType.Network} dataKey="n" />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
unit?: string
|
unit?: string
|
||||||
filter?: string
|
filter?: string
|
||||||
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
||||||
|
truncate?: boolean
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
@@ -119,6 +120,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
filter,
|
filter,
|
||||||
itemSorter,
|
itemSorter,
|
||||||
contentFormatter: content = undefined,
|
contentFormatter: content = undefined,
|
||||||
|
truncate = false,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@@ -214,10 +216,15 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
nestLabel ? "items-end" : "items-center"
|
nestLabel ? "items-end" : "items-center"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="grid gap-1.5">
|
{nestLabel ? tooltipLabel : null}
|
||||||
{nestLabel ? tooltipLabel : null}
|
<span
|
||||||
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
|
className={cn(
|
||||||
</div>
|
"text-muted-foreground",
|
||||||
|
truncate ? "max-w-40 truncate leading-normal -my-1" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{itemConfig?.label || item.name}
|
||||||
|
</span>
|
||||||
{item.value !== undefined && (
|
{item.value !== undefined && (
|
||||||
<span className="font-medium tabular-nums text-foreground">
|
<span className="font-medium tabular-nums text-foreground">
|
||||||
{content && typeof content === "function"
|
{content && typeof content === "function"
|
||||||
|
|||||||
@@ -12,21 +12,42 @@ export function TuxIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// meteor icons (MIT) https://github.com/zkreations/icons/blob/main/LICENSE
|
// icon park (Apache 2.0) https://github.com/bytedance/IconPark/blob/master/LICENSE
|
||||||
export function WindowsIcon(props: SVGProps<SVGSVGElement>) {
|
export function WindowsIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg viewBox="0 0 24 24" {...props}>
|
<svg {...props} viewBox="0 0 48 48">
|
||||||
<path
|
<path
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeWidth="3.8"
|
||||||
strokeWidth="2"
|
d="m6.8 11 12.9-1.7v12.1h-13zm18-2.2 16.4-2v14.6H25zm0 18.6 16.4.4v13.4L25 38.6zm-18-.8 12.9.3v10.9l-13-2.2z"
|
||||||
d="M2 12h20m-11.3 8.3V3.7M2 5l20-3v20L2 19Z"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// teenyicons (MIT) https://github.com/teenyicons/teenyicons/blob/master/LICENSE
|
||||||
|
export function AppleIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 20 20" {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M14.1 4.7a5 5 0 0 1 3.8 2c-3.3 1.9-2.8 6.7.6 8L17.2 17c-.8 1.3-2 2.9-3.5 2.9-1.2 0-1.6-.9-3.3-.8s-2.2.8-3.5.8c-1.4 0-2.5-1.5-3.4-2.7-2.3-3.6-2.5-7.9-1.1-10 1-1.7 2.6-2.6 4.1-2.6 1.6 0 2.6.8 3.8.8 1.3 0 2-.8 3.8-.8M13.7 0c.2 1.2-.3 2.4-1 3.2a4 4 0 0 1-3 1.6c-.2-1.2.3-2.3 1-3.2.7-.8 2-1.5 3-1.6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ion icons (MIT) https://github.com/ionic-team/ionicons/blob/main/LICENSE
|
||||||
|
export function DockerIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} viewBox="0 0 512 512" fill="currentColor">
|
||||||
|
<path d="M507 211c-1-1-14-11-42-11a133 133 0 0 0-21 2c-6-36-36-54-37-55l-7-4-5 7a102 102 0 0 0-13 30c-5 21-2 40 8 57-12 7-33 9-37 9H16a16 16 0 0 0-16 16 241 241 0 0 0 15 87c11 30 29 53 51 67 25 15 66 24 113 24a344 344 0 0 0 62-6 257 257 0 0 0 82-29 224 224 0 0 0 55-46c27-30 43-64 55-94h4c30 0 48-12 58-22a63 63 0 0 0 15-22l2-6Z" />
|
||||||
|
<path d="M47 236h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4H47a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m63 0h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m63 0h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m62 0h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m-125-57h45a4 4 0 0 0 4-4v-41a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v41a4 4 0 0 0 4 4m63 0h45a4 4 0 0 0 4-4v-41a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v41a4 4 0 0 0 4 4m62 0h45a4 4 0 0 0 4-4v-41a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v41a4 4 0 0 0 4 4m0-58h45a4 4 0 0 0 4-4V76a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m63 116h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// MingCute Apache License 2.0 https://github.com/Richard9394/MingCute
|
// MingCute Apache License 2.0 https://github.com/Richard9394/MingCute
|
||||||
export function Rows(props: SVGProps<SVGSVGElement>) {
|
export function Rows(props: SVGProps<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
13
beszel/site/src/lib/enums.ts
Normal file
13
beszel/site/src/lib/enums.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export enum Os {
|
||||||
|
Linux = 0,
|
||||||
|
Darwin,
|
||||||
|
Windows,
|
||||||
|
// FreeBSD,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChartType {
|
||||||
|
Memory,
|
||||||
|
Disk,
|
||||||
|
Network,
|
||||||
|
CPU,
|
||||||
|
}
|
||||||
3
beszel/site/src/types.d.ts
vendored
3
beszel/site/src/types.d.ts
vendored
@@ -1,4 +1,5 @@
|
|||||||
import { RecordModel } from "pocketbase"
|
import { RecordModel } from "pocketbase"
|
||||||
|
import { Os } from "./lib/enums"
|
||||||
|
|
||||||
// global window properties
|
// global window properties
|
||||||
declare global {
|
declare global {
|
||||||
@@ -48,6 +49,8 @@ export interface SystemInfo {
|
|||||||
g?: number
|
g?: number
|
||||||
/** dashboard display temperature */
|
/** dashboard display temperature */
|
||||||
dt?: number
|
dt?: number
|
||||||
|
/** operating system */
|
||||||
|
os?: Os
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemStats {
|
export interface SystemStats {
|
||||||
|
|||||||
Reference in New Issue
Block a user