mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-24 14:36:17 +01:00
Compare commits
13 Commits
v0.16.0
...
3650482b09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3650482b09 | ||
|
|
79adfd2c0d | ||
|
|
779dcc62aa | ||
|
|
abe39c1a0a | ||
|
|
bd41ad813c | ||
|
|
77fe63fb63 | ||
|
|
f61ba202d8 | ||
|
|
e1067fa1a3 | ||
|
|
0a3eb898ae | ||
|
|
6c33e9dc93 | ||
|
|
f8ed6ce705 | ||
|
|
f64478b75e | ||
|
|
854a3697d7 |
17
.github/workflows/inactivity-actions.yml
vendored
17
.github/workflows/inactivity-actions.yml
vendored
@@ -10,12 +10,25 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
lock-inactive:
|
||||
name: Lock Inactive Issues
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: klaasnicolaas/action-inactivity-lock@v1.1.3
|
||||
id: lock
|
||||
with:
|
||||
days-inactive-issues: 14
|
||||
lock-reason-issues: ""
|
||||
# Action can not skip PRs, set it to 100 years to cover it.
|
||||
days-inactive-prs: 36524
|
||||
lock-reason-prs: ""
|
||||
|
||||
close-stale:
|
||||
name: Close Stale Issues
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Close Stale Issues
|
||||
uses: actions/stale@v9
|
||||
uses: actions/stale@v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -32,6 +45,8 @@ jobs:
|
||||
# Timing
|
||||
days-before-issue-stale: 14
|
||||
days-before-issue-close: 7
|
||||
# Action can not skip PRs, set it to 100 years to cover it.
|
||||
days-before-pr-stale: 36524
|
||||
|
||||
# Labels
|
||||
stale-issue-label: 'stale'
|
||||
|
||||
@@ -161,8 +161,15 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
||||
}
|
||||
|
||||
// skip updating systemd services if cache time is not the default 60sec interval
|
||||
if a.systemdManager != nil && cacheTimeMs == 60_000 && a.systemdManager.hasFreshStats {
|
||||
data.SystemdServices = a.systemdManager.getServiceStats(nil, false)
|
||||
if a.systemdManager != nil && cacheTimeMs == 60_000 {
|
||||
totalCount := uint16(a.systemdManager.getServiceStatsCount())
|
||||
if totalCount > 0 {
|
||||
numFailed := a.systemdManager.getFailedServiceCount()
|
||||
data.Info.Services = []uint16{totalCount, numFailed}
|
||||
}
|
||||
if a.systemdManager.hasFreshStats {
|
||||
data.SystemdServices = a.systemdManager.getServiceStats(nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
data.Stats.ExtraFs = make(map[string]*system.FsStats)
|
||||
|
||||
@@ -95,6 +95,9 @@ func (a *Agent) initializeDiskInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the appropriate root mount point for this system
|
||||
rootMountPoint := a.getRootMountPoint()
|
||||
|
||||
// Use FILESYSTEM env var to find root filesystem
|
||||
if filesystem != "" {
|
||||
for _, p := range partitions {
|
||||
@@ -138,7 +141,7 @@ func (a *Agent) initializeDiskInfo() {
|
||||
for _, p := range partitions {
|
||||
// fmt.Println(p.Device, p.Mountpoint)
|
||||
// Binary root fallback or docker root fallback
|
||||
if !hasRoot && (p.Mountpoint == "/" || (p.Mountpoint == "/etc/hosts" && strings.HasPrefix(p.Device, "/dev"))) {
|
||||
if !hasRoot && (p.Mountpoint == rootMountPoint || (p.Mountpoint == "/etc/hosts" && strings.HasPrefix(p.Device, "/dev"))) {
|
||||
fs, match := findIoDevice(filepath.Base(p.Device), diskIoCounters, a.fsStats)
|
||||
if match {
|
||||
addFsStat(fs, p.Mountpoint, true)
|
||||
@@ -174,8 +177,8 @@ func (a *Agent) initializeDiskInfo() {
|
||||
// If no root filesystem set, use fallback
|
||||
if !hasRoot {
|
||||
rootDevice, _ := findIoDevice(filepath.Base(filesystem), diskIoCounters, a.fsStats)
|
||||
slog.Info("Root disk", "mountpoint", "/", "io", rootDevice)
|
||||
a.fsStats[rootDevice] = &system.FsStats{Root: true, Mountpoint: "/"}
|
||||
slog.Info("Root disk", "mountpoint", rootMountPoint, "io", rootDevice)
|
||||
a.fsStats[rootDevice] = &system.FsStats{Root: true, Mountpoint: rootMountPoint}
|
||||
}
|
||||
|
||||
a.initializeDiskIoStats(diskIoCounters)
|
||||
@@ -312,3 +315,32 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getRootMountPoint returns the appropriate root mount point for the system
|
||||
// For immutable systems like Fedora Silverblue, it returns /sysroot instead of /
|
||||
func (a *Agent) getRootMountPoint() string {
|
||||
// 1. Check if /etc/os-release contains indicators of an immutable system
|
||||
if osReleaseContent, err := os.ReadFile("/etc/os-release"); err == nil {
|
||||
content := string(osReleaseContent)
|
||||
if strings.Contains(content, "fedora") && strings.Contains(content, "silverblue") ||
|
||||
strings.Contains(content, "coreos") ||
|
||||
strings.Contains(content, "flatcar") ||
|
||||
strings.Contains(content, "rhel-atomic") ||
|
||||
strings.Contains(content, "centos-atomic") {
|
||||
// Verify that /sysroot exists before returning it
|
||||
if _, err := os.Stat("/sysroot"); err == nil {
|
||||
return "/sysroot"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check if /run/ostree is present (ostree-based systems like Silverblue)
|
||||
if _, err := os.Stat("/run/ostree"); err == nil {
|
||||
// Verify that /sysroot exists before returning it
|
||||
if _, err := os.Stat("/sysroot"); err == nil {
|
||||
return "/sysroot"
|
||||
}
|
||||
}
|
||||
|
||||
return "/"
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type systemdManager struct {
|
||||
serviceStatsMap map[string]*systemd.Service
|
||||
isRunning bool
|
||||
hasFreshStats bool
|
||||
patterns []string
|
||||
}
|
||||
|
||||
// newSystemdManager creates a new systemdManager.
|
||||
@@ -39,6 +40,7 @@ func newSystemdManager() (*systemdManager, error) {
|
||||
|
||||
manager := &systemdManager{
|
||||
serviceStatsMap: make(map[string]*systemd.Service),
|
||||
patterns: getServicePatterns(),
|
||||
}
|
||||
|
||||
manager.startWorker(conn)
|
||||
@@ -62,6 +64,24 @@ func (sm *systemdManager) startWorker(conn *dbus.Conn) {
|
||||
}()
|
||||
}
|
||||
|
||||
// getServiceStatsCount returns the number of systemd services.
|
||||
func (sm *systemdManager) getServiceStatsCount() int {
|
||||
return len(sm.serviceStatsMap)
|
||||
}
|
||||
|
||||
// getFailedServiceCount returns the number of systemd services in a failed state.
|
||||
func (sm *systemdManager) getFailedServiceCount() uint16 {
|
||||
sm.Lock()
|
||||
defer sm.Unlock()
|
||||
count := uint16(0)
|
||||
for _, service := range sm.serviceStatsMap {
|
||||
if service.State == systemd.StatusFailed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// getServiceStats collects statistics for all running systemd services.
|
||||
func (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh bool) []*systemd.Service {
|
||||
// start := time.Now()
|
||||
@@ -91,7 +111,7 @@ func (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh bool) []*syst
|
||||
defer conn.Close()
|
||||
}
|
||||
|
||||
units, err := conn.ListUnitsByPatternsContext(context.Background(), []string{"loaded"}, []string{"*.service"})
|
||||
units, err := conn.ListUnitsByPatternsContext(context.Background(), []string{"loaded"}, sm.patterns)
|
||||
if err != nil {
|
||||
slog.Error("Error listing systemd service units", "err", err)
|
||||
return nil
|
||||
@@ -227,3 +247,26 @@ func unescapeServiceName(name string) string {
|
||||
}
|
||||
return unescaped
|
||||
}
|
||||
|
||||
// getServicePatterns returns the list of service patterns to match.
|
||||
// It reads from the SERVICE_PATTERNS environment variable if set,
|
||||
// otherwise defaults to "*service".
|
||||
func getServicePatterns() []string {
|
||||
patterns := []string{}
|
||||
if envPatterns, _ := GetEnv("SERVICE_PATTERNS"); envPatterns != "" {
|
||||
for pattern := range strings.SplitSeq(envPatterns, ",") {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(pattern, ".service") {
|
||||
pattern += ".service"
|
||||
}
|
||||
patterns = append(patterns, pattern)
|
||||
}
|
||||
}
|
||||
if len(patterns) == 0 {
|
||||
patterns = []string{"*.service"}
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -46,3 +47,112 @@ func TestUnescapeServiceNameInvalid(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetServicePatterns(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefixedEnv string
|
||||
unprefixedEnv string
|
||||
expected []string
|
||||
cleanupEnvVars bool
|
||||
}{
|
||||
{
|
||||
name: "default when no env var set",
|
||||
prefixedEnv: "",
|
||||
unprefixedEnv: "",
|
||||
expected: []string{"*.service"},
|
||||
cleanupEnvVars: true,
|
||||
},
|
||||
{
|
||||
name: "single pattern with prefixed env",
|
||||
prefixedEnv: "nginx",
|
||||
unprefixedEnv: "",
|
||||
expected: []string{"nginx.service"},
|
||||
cleanupEnvVars: true,
|
||||
},
|
||||
{
|
||||
name: "single pattern with unprefixed env",
|
||||
prefixedEnv: "",
|
||||
unprefixedEnv: "nginx",
|
||||
expected: []string{"nginx.service"},
|
||||
cleanupEnvVars: true,
|
||||
},
|
||||
{
|
||||
name: "prefixed env takes precedence",
|
||||
prefixedEnv: "nginx",
|
||||
unprefixedEnv: "apache",
|
||||
expected: []string{"nginx.service"},
|
||||
cleanupEnvVars: true,
|
||||
},
|
||||
{
|
||||
name: "multiple patterns",
|
||||
prefixedEnv: "nginx,apache,postgresql",
|
||||
unprefixedEnv: "",
|
||||
expected: []string{"nginx.service", "apache.service", "postgresql.service"},
|
||||
cleanupEnvVars: true,
|
||||
},
|
||||
{
|
||||
name: "patterns with .service suffix",
|
||||
prefixedEnv: "nginx.service,apache.service",
|
||||
unprefixedEnv: "",
|
||||
expected: []string{"nginx.service", "apache.service"},
|
||||
cleanupEnvVars: true,
|
||||
},
|
||||
{
|
||||
name: "mixed patterns with and without suffix",
|
||||
prefixedEnv: "nginx.service,apache,postgresql.service",
|
||||
unprefixedEnv: "",
|
||||
expected: []string{"nginx.service", "apache.service", "postgresql.service"},
|
||||
cleanupEnvVars: true,
|
||||
},
|
||||
{
|
||||
name: "patterns with whitespace",
|
||||
prefixedEnv: " nginx , apache , postgresql ",
|
||||
unprefixedEnv: "",
|
||||
expected: []string{"nginx.service", "apache.service", "postgresql.service"},
|
||||
cleanupEnvVars: true,
|
||||
},
|
||||
{
|
||||
name: "empty patterns are skipped",
|
||||
prefixedEnv: "nginx,,apache, ,postgresql",
|
||||
unprefixedEnv: "",
|
||||
expected: []string{"nginx.service", "apache.service", "postgresql.service"},
|
||||
cleanupEnvVars: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard pattern",
|
||||
prefixedEnv: "*nginx*,*apache*",
|
||||
unprefixedEnv: "",
|
||||
expected: []string{"*nginx*.service", "*apache*.service"},
|
||||
cleanupEnvVars: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Clean up any existing env vars
|
||||
os.Unsetenv("BESZEL_AGENT_SERVICE_PATTERNS")
|
||||
os.Unsetenv("SERVICE_PATTERNS")
|
||||
|
||||
// Set up environment variables
|
||||
if tt.prefixedEnv != "" {
|
||||
os.Setenv("BESZEL_AGENT_SERVICE_PATTERNS", tt.prefixedEnv)
|
||||
}
|
||||
if tt.unprefixedEnv != "" {
|
||||
os.Setenv("SERVICE_PATTERNS", tt.unprefixedEnv)
|
||||
}
|
||||
|
||||
// Run the function
|
||||
result := getServicePatterns()
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, tt.expected, result, "Patterns should match expected values")
|
||||
|
||||
// Cleanup
|
||||
if tt.cleanupEnvVars {
|
||||
os.Unsetenv("BESZEL_AGENT_SERVICE_PATTERNS")
|
||||
os.Unsetenv("SERVICE_PATTERNS")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,6 @@ var supportsTitle = map[string]struct{}{
|
||||
"ifttt": {},
|
||||
"join": {},
|
||||
"lark": {},
|
||||
"matrix": {},
|
||||
"ntfy": {},
|
||||
"opsgenie": {},
|
||||
"pushbullet": {},
|
||||
|
||||
@@ -147,6 +147,7 @@ type Info struct {
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
||||
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||
Services []uint16 `json:"sv,omitempty" cbor:"22,keyasint,omitempty"` // [totalServices, numFailedServices]
|
||||
}
|
||||
|
||||
// Final data structure to return to the hub
|
||||
|
||||
1008
internal/site/bun.lock
Normal file
1008
internal/site/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -124,6 +124,7 @@ export default memo(function ContainerChart({
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis
|
||||
direction="ltr"
|
||||
domain={[0, 'dataMax']}
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
width={yAxisWidth}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
type VisibilityState,
|
||||
@@ -40,7 +41,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { alertInfo } from "@/lib/alerts"
|
||||
import { pb } from "@/lib/api"
|
||||
import { cn, formatDuration, formatShortDate } from "@/lib/utils"
|
||||
import { cn, formatDuration, formatShortDate, useBrowserStorage } from "@/lib/utils"
|
||||
import type { AlertsHistoryRecord } from "@/types"
|
||||
import { alertsHistoryColumns } from "../../alerts-history-columns"
|
||||
|
||||
@@ -66,6 +67,12 @@ export default function AlertsHistoryDataTable() {
|
||||
const [globalFilter, setGlobalFilter] = useState("")
|
||||
const { toast } = useToast()
|
||||
const [deleteOpen, setDeleteDialogOpen] = useState(false)
|
||||
|
||||
// Store pagination preference in local storage
|
||||
const [pagination, setPagination] = useBrowserStorage<PaginationState>("ah-pagination", {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined
|
||||
@@ -136,12 +143,14 @@ export default function AlertsHistoryDataTable() {
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onPaginationChange: setPagination,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
globalFilter,
|
||||
pagination,
|
||||
},
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
globalFilterFn: (row, _columnId, filterValue) => {
|
||||
@@ -318,10 +327,10 @@ export default function AlertsHistoryDataTable() {
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value))
|
||||
table.setPageSize(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[4.8em]" id="rows-per-page">
|
||||
<SelectTrigger className="w-18" id="rows-per-page">
|
||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import Slider from "@/components/ui/slider"
|
||||
import { HourFormat, Unit } from "@/lib/enums"
|
||||
import { dynamicActivate } from "@/lib/i18n"
|
||||
import languages from "@/lib/languages"
|
||||
import { $userSettings } from "@/lib/stores"
|
||||
import { chartTimeData, currentHour12 } from "@/lib/utils"
|
||||
import type { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
@@ -17,6 +20,8 @@ import { saveSettings } from "./layout"
|
||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { i18n } = useLingui()
|
||||
const currentUserSettings = useStore($userSettings)
|
||||
const layoutWidth = currentUserSettings.layoutWidth ?? 1480
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
@@ -73,6 +78,27 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
</Select>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid gap-2">
|
||||
<div className="mb-2">
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
<Trans>Layout width</Trans>
|
||||
</h3>
|
||||
<Label htmlFor="layoutWidth" className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>Adjust the width of the main layout</Trans> ({layoutWidth}px)
|
||||
</Label>
|
||||
</div>
|
||||
<Slider
|
||||
id="layoutWidth"
|
||||
name="layoutWidth"
|
||||
value={[layoutWidth]}
|
||||
onValueChange={(val) => $userSettings.setKey("layoutWidth", val[0])}
|
||||
min={1000}
|
||||
max={2000}
|
||||
step={10}
|
||||
className="w-full mb-1"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid gap-2">
|
||||
<div className="mb-2">
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
|
||||
@@ -118,7 +118,9 @@ export const columns: ColumnDef<DiskInfo>[] = [
|
||||
sortingFn: (a, b) => a.original.device.localeCompare(b.original.device),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Device`} Icon={HardDrive} />,
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium ms-1.5">{row.getValue("device")}</div>
|
||||
<div className="font-medium max-w-50 truncate ms-1.5" title={row.getValue("device")}>
|
||||
{row.getValue("device")}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
PenBoxIcon,
|
||||
PlayCircleIcon,
|
||||
ServerIcon,
|
||||
TerminalSquareIcon,
|
||||
Trash2Icon,
|
||||
WifiIcon,
|
||||
} from "lucide-react"
|
||||
@@ -69,7 +70,7 @@ const STATUS_COLORS = {
|
||||
* @param viewMode - "table" or "grid"
|
||||
* @returns - Column definitions for the systems table
|
||||
*/
|
||||
export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<SystemRecord>[] {
|
||||
export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<SystemRecord>[] {
|
||||
return [
|
||||
{
|
||||
// size: 200,
|
||||
@@ -134,7 +135,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: ({ info }) => info.cpu,
|
||||
accessorFn: ({ info }) => info.cpu || undefined,
|
||||
id: "cpu",
|
||||
name: () => t`CPU`,
|
||||
cell: TableCellWithMeter,
|
||||
@@ -143,7 +144,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
},
|
||||
{
|
||||
// accessorKey: "info.mp",
|
||||
accessorFn: ({ info }) => info.mp,
|
||||
accessorFn: ({ info }) => info.mp || undefined,
|
||||
id: "memory",
|
||||
name: () => t`Memory`,
|
||||
cell: TableCellWithMeter,
|
||||
@@ -151,7 +152,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: ({ info }) => info.dp,
|
||||
accessorFn: ({ info }) => info.dp || undefined,
|
||||
id: "disk",
|
||||
name: () => t`Disk`,
|
||||
cell: DiskCellWithMultiple,
|
||||
@@ -159,7 +160,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: ({ info }) => info.g,
|
||||
accessorFn: ({ info }) => info.g || undefined,
|
||||
id: "gpu",
|
||||
name: () => "GPU",
|
||||
cell: TableCellWithMeter,
|
||||
@@ -172,9 +173,9 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
const sum = info.la?.reduce((acc, curr) => acc + curr, 0)
|
||||
// TODO: remove this in future release in favor of la array
|
||||
if (!sum) {
|
||||
return (info.l1 ?? 0) + (info.l5 ?? 0) + (info.l15 ?? 0)
|
||||
return (info.l1 ?? 0) + (info.l5 ?? 0) + (info.l15 ?? 0) || undefined
|
||||
}
|
||||
return sum
|
||||
return sum || undefined
|
||||
},
|
||||
name: () => t({ message: "Load Avg", comment: "Short label for load average" }),
|
||||
size: 0,
|
||||
@@ -217,7 +218,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024,
|
||||
accessorFn: ({ info }) => (info.bb || (info.b || 0) * 1024 * 1024) || undefined,
|
||||
id: "net",
|
||||
name: () => t`Net`,
|
||||
size: 0,
|
||||
@@ -229,10 +230,10 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
if (sys.status === SystemStatus.Paused) {
|
||||
return null
|
||||
}
|
||||
const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, false)
|
||||
const { value, unit } = formatBytes((info.getValue() || 0) as number, true, userSettings.unitNet, false)
|
||||
return (
|
||||
<span className="tabular-nums whitespace-nowrap">
|
||||
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
|
||||
{decimalString(value , value >= 100 ? 1 : 2)} {unit}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
@@ -259,11 +260,46 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorFn: ({ info }) => info.sv?.[0],
|
||||
id: "services",
|
||||
name: () => t`Services`,
|
||||
size: 50,
|
||||
Icon: TerminalSquareIcon,
|
||||
header: sortableHeader,
|
||||
hideSort: true,
|
||||
sortingFn: (a, b) => {
|
||||
// sort priorities: 1) failed services, 2) total services
|
||||
const [totalCountA, numFailedA] = a.original.info.sv ?? [0, 0]
|
||||
const [totalCountB, numFailedB] = b.original.info.sv ?? [0, 0]
|
||||
if (numFailedA !== numFailedB) {
|
||||
return numFailedA - numFailedB
|
||||
}
|
||||
return totalCountA - totalCountB
|
||||
},
|
||||
cell(info) {
|
||||
const sys = info.row.original
|
||||
const [totalCount, numFailed] = sys.info.sv ?? [0, 0]
|
||||
if (sys.status !== SystemStatus.Up || totalCount === 0) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<span className="tabular-nums whitespace-nowrap flex gap-1.5 items-center">
|
||||
<span
|
||||
className={cn("block size-2 rounded-full", {
|
||||
[STATUS_COLORS[SystemStatus.Down]]: numFailed > 0,
|
||||
[STATUS_COLORS[SystemStatus.Up]]: numFailed === 0,
|
||||
})}
|
||||
/>
|
||||
{totalCount} <span className="text-muted-foreground text-sm -ms-0.5">({t`Failed`.toLowerCase()}: {numFailed})</span>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorFn: ({ info }) => info.v,
|
||||
id: "agent",
|
||||
name: () => t`Agent`,
|
||||
// invertSorting: true,
|
||||
size: 50,
|
||||
Icon: WifiIcon,
|
||||
hideSort: true,
|
||||
|
||||
@@ -47,7 +47,7 @@ import type { SystemRecord } from "@/types"
|
||||
import AlertButton from "../alerts/alert-button"
|
||||
import { $router, Link } from "../router"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
|
||||
import { SystemsTableColumns, ActionsButton, IndicatorDot } from "./systems-table-columns"
|
||||
|
||||
type ViewMode = "table" | "grid"
|
||||
type StatusFilter = "all" | SystemRecord["status"]
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
--table-header: hsl(225, 6%, 97%);
|
||||
--chart-saturation: 65%;
|
||||
--chart-lightness: 50%;
|
||||
--container: 1480px;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -149,7 +150,8 @@
|
||||
}
|
||||
|
||||
@utility container {
|
||||
@apply max-w-370 mx-auto px-4;
|
||||
max-width: var(--container);
|
||||
@apply mx-auto px-4;
|
||||
}
|
||||
|
||||
@utility link {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Toaster } from "@/components/ui/toaster.tsx"
|
||||
import { alertManager } from "@/lib/alerts"
|
||||
import { pb, updateUserSettings } from "@/lib/api.ts"
|
||||
import { dynamicActivate, getLocale } from "@/lib/i18n"
|
||||
import { $authenticated, $copyContent, $direction, $publicKey } from "@/lib/stores.ts"
|
||||
import { $authenticated, $copyContent, $direction, $publicKey, $userSettings } from "@/lib/stores.ts"
|
||||
import * as systemsManager from "@/lib/systemsManager.ts"
|
||||
|
||||
const LoginPage = lazy(() => import("@/components/login/login.tsx"))
|
||||
@@ -71,6 +71,7 @@ const Layout = () => {
|
||||
const authenticated = useStore($authenticated)
|
||||
const copyContent = useStore($copyContent)
|
||||
const direction = useStore($direction)
|
||||
const userSettings = useStore($userSettings)
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.dir = direction
|
||||
@@ -96,7 +97,7 @@ const Layout = () => {
|
||||
<LoginPage />
|
||||
</Suspense>
|
||||
) : (
|
||||
<>
|
||||
<div style={{"--container": `${userSettings.layoutWidth ?? 1480}px`} as React.CSSProperties}>
|
||||
<div className="container">
|
||||
<Navbar />
|
||||
</div>
|
||||
@@ -108,7 +109,7 @@ const Layout = () => {
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</DirectionProvider>
|
||||
)
|
||||
|
||||
3
internal/site/src/types.d.ts
vendored
3
internal/site/src/types.d.ts
vendored
@@ -79,6 +79,8 @@ export interface SystemInfo {
|
||||
ct?: ConnectionType
|
||||
/** extra filesystem percentages */
|
||||
efs?: Record<string, number>
|
||||
/** services [totalServices, numFailedServices] */
|
||||
sv?: [number, number]
|
||||
}
|
||||
|
||||
export interface SystemStats {
|
||||
@@ -279,6 +281,7 @@ export interface UserSettings {
|
||||
colorWarn?: number
|
||||
colorCrit?: number
|
||||
hourFormat?: HourFormat
|
||||
layoutWidth?: number
|
||||
}
|
||||
|
||||
type ChartDataContainer = {
|
||||
|
||||
Reference in New Issue
Block a user