mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-14 08:51:49 +02:00
Compare commits
6 Commits
v0.16.0
...
e1067fa1a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1067fa1a3 | ||
|
|
0a3eb898ae | ||
|
|
6c33e9dc93 | ||
|
|
f8ed6ce705 | ||
|
|
f64478b75e | ||
|
|
854a3697d7 |
@@ -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
|
// skip updating systemd services if cache time is not the default 60sec interval
|
||||||
if a.systemdManager != nil && cacheTimeMs == 60_000 && a.systemdManager.hasFreshStats {
|
if a.systemdManager != nil && cacheTimeMs == 60_000 {
|
||||||
data.SystemdServices = a.systemdManager.getServiceStats(nil, false)
|
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)
|
data.Stats.ExtraFs = make(map[string]*system.FsStats)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type systemdManager struct {
|
|||||||
serviceStatsMap map[string]*systemd.Service
|
serviceStatsMap map[string]*systemd.Service
|
||||||
isRunning bool
|
isRunning bool
|
||||||
hasFreshStats bool
|
hasFreshStats bool
|
||||||
|
patterns []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSystemdManager creates a new systemdManager.
|
// newSystemdManager creates a new systemdManager.
|
||||||
@@ -39,6 +40,7 @@ func newSystemdManager() (*systemdManager, error) {
|
|||||||
|
|
||||||
manager := &systemdManager{
|
manager := &systemdManager{
|
||||||
serviceStatsMap: make(map[string]*systemd.Service),
|
serviceStatsMap: make(map[string]*systemd.Service),
|
||||||
|
patterns: getServicePatterns(),
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.startWorker(conn)
|
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.
|
// getServiceStats collects statistics for all running systemd services.
|
||||||
func (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh bool) []*systemd.Service {
|
func (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh bool) []*systemd.Service {
|
||||||
// start := time.Now()
|
// start := time.Now()
|
||||||
@@ -91,7 +111,7 @@ func (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh bool) []*syst
|
|||||||
defer conn.Close()
|
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 {
|
if err != nil {
|
||||||
slog.Error("Error listing systemd service units", "err", err)
|
slog.Error("Error listing systemd service units", "err", err)
|
||||||
return nil
|
return nil
|
||||||
@@ -227,3 +247,26 @@ func unescapeServiceName(name string) string {
|
|||||||
}
|
}
|
||||||
return unescaped
|
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
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ type Info struct {
|
|||||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||||
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
||||||
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
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
|
// Final data structure to return to the hub
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export default memo(function ContainerChart({
|
|||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
direction="ltr"
|
direction="ltr"
|
||||||
|
domain={[0, 'dataMax']}
|
||||||
orientation={chartData.orientation}
|
orientation={chartData.orientation}
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
width={yAxisWidth}
|
width={yAxisWidth}
|
||||||
|
|||||||
@@ -2,14 +2,17 @@
|
|||||||
import { Trans, useLingui } from "@lingui/react/macro"
|
import { Trans, useLingui } from "@lingui/react/macro"
|
||||||
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
|
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import { useStore } from "@nanostores/react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import Slider from "@/components/ui/slider"
|
||||||
import { HourFormat, Unit } from "@/lib/enums"
|
import { HourFormat, Unit } from "@/lib/enums"
|
||||||
import { dynamicActivate } from "@/lib/i18n"
|
import { dynamicActivate } from "@/lib/i18n"
|
||||||
import languages from "@/lib/languages"
|
import languages from "@/lib/languages"
|
||||||
|
import { $userSettings } from "@/lib/stores"
|
||||||
import { chartTimeData, currentHour12 } from "@/lib/utils"
|
import { chartTimeData, currentHour12 } from "@/lib/utils"
|
||||||
import type { UserSettings } from "@/types"
|
import type { UserSettings } from "@/types"
|
||||||
import { saveSettings } from "./layout"
|
import { saveSettings } from "./layout"
|
||||||
@@ -17,6 +20,8 @@ import { saveSettings } from "./layout"
|
|||||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const { i18n } = useLingui()
|
const { i18n } = useLingui()
|
||||||
|
const currentUserSettings = useStore($userSettings)
|
||||||
|
const layoutWidth = currentUserSettings.layoutWidth ?? 1480
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -73,6 +78,27 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<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="grid gap-2">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<h3 className="mb-1 text-lg font-medium">
|
<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),
|
sortingFn: (a, b) => a.original.device.localeCompare(b.original.device),
|
||||||
header: ({ column }) => <HeaderButton column={column} name={t`Device`} Icon={HardDrive} />,
|
header: ({ column }) => <HeaderButton column={column} name={t`Device`} Icon={HardDrive} />,
|
||||||
cell: ({ row }) => (
|
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,
|
PenBoxIcon,
|
||||||
PlayCircleIcon,
|
PlayCircleIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
|
TerminalSquareIcon,
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
WifiIcon,
|
WifiIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
@@ -69,7 +70,7 @@ const STATUS_COLORS = {
|
|||||||
* @param viewMode - "table" or "grid"
|
* @param viewMode - "table" or "grid"
|
||||||
* @returns - Column definitions for the systems table
|
* @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 [
|
return [
|
||||||
{
|
{
|
||||||
// size: 200,
|
// size: 200,
|
||||||
@@ -134,7 +135,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: ({ info }) => info.cpu,
|
accessorFn: ({ info }) => info.cpu || undefined,
|
||||||
id: "cpu",
|
id: "cpu",
|
||||||
name: () => t`CPU`,
|
name: () => t`CPU`,
|
||||||
cell: TableCellWithMeter,
|
cell: TableCellWithMeter,
|
||||||
@@ -143,7 +144,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// accessorKey: "info.mp",
|
// accessorKey: "info.mp",
|
||||||
accessorFn: ({ info }) => info.mp,
|
accessorFn: ({ info }) => info.mp || undefined,
|
||||||
id: "memory",
|
id: "memory",
|
||||||
name: () => t`Memory`,
|
name: () => t`Memory`,
|
||||||
cell: TableCellWithMeter,
|
cell: TableCellWithMeter,
|
||||||
@@ -151,7 +152,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: ({ info }) => info.dp,
|
accessorFn: ({ info }) => info.dp || undefined,
|
||||||
id: "disk",
|
id: "disk",
|
||||||
name: () => t`Disk`,
|
name: () => t`Disk`,
|
||||||
cell: DiskCellWithMultiple,
|
cell: DiskCellWithMultiple,
|
||||||
@@ -159,7 +160,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: ({ info }) => info.g,
|
accessorFn: ({ info }) => info.g || undefined,
|
||||||
id: "gpu",
|
id: "gpu",
|
||||||
name: () => "GPU",
|
name: () => "GPU",
|
||||||
cell: TableCellWithMeter,
|
cell: TableCellWithMeter,
|
||||||
@@ -172,9 +173,9 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
const sum = info.la?.reduce((acc, curr) => acc + curr, 0)
|
const sum = info.la?.reduce((acc, curr) => acc + curr, 0)
|
||||||
// TODO: remove this in future release in favor of la array
|
// TODO: remove this in future release in favor of la array
|
||||||
if (!sum) {
|
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" }),
|
name: () => t({ message: "Load Avg", comment: "Short label for load average" }),
|
||||||
size: 0,
|
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",
|
id: "net",
|
||||||
name: () => t`Net`,
|
name: () => t`Net`,
|
||||||
size: 0,
|
size: 0,
|
||||||
@@ -229,10 +230,10 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
|||||||
if (sys.status === SystemStatus.Paused) {
|
if (sys.status === SystemStatus.Paused) {
|
||||||
return null
|
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 (
|
return (
|
||||||
<span className="tabular-nums whitespace-nowrap">
|
<span className="tabular-nums whitespace-nowrap">
|
||||||
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
|
{decimalString(value , value >= 100 ? 1 : 2)} {unit}
|
||||||
</span>
|
</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,
|
accessorFn: ({ info }) => info.v,
|
||||||
id: "agent",
|
id: "agent",
|
||||||
name: () => t`Agent`,
|
name: () => t`Agent`,
|
||||||
// invertSorting: true,
|
|
||||||
size: 50,
|
size: 50,
|
||||||
Icon: WifiIcon,
|
Icon: WifiIcon,
|
||||||
hideSort: true,
|
hideSort: true,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import type { SystemRecord } from "@/types"
|
|||||||
import AlertButton from "../alerts/alert-button"
|
import AlertButton from "../alerts/alert-button"
|
||||||
import { $router, Link } from "../router"
|
import { $router, Link } from "../router"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
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 ViewMode = "table" | "grid"
|
||||||
type StatusFilter = "all" | SystemRecord["status"]
|
type StatusFilter = "all" | SystemRecord["status"]
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
--table-header: hsl(225, 6%, 97%);
|
--table-header: hsl(225, 6%, 97%);
|
||||||
--chart-saturation: 65%;
|
--chart-saturation: 65%;
|
||||||
--chart-lightness: 50%;
|
--chart-lightness: 50%;
|
||||||
|
--container: 1480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -149,7 +150,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility container {
|
@utility container {
|
||||||
@apply max-w-370 mx-auto px-4;
|
max-width: var(--container);
|
||||||
|
@apply mx-auto px-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility link {
|
@utility link {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { Toaster } from "@/components/ui/toaster.tsx"
|
|||||||
import { alertManager } from "@/lib/alerts"
|
import { alertManager } from "@/lib/alerts"
|
||||||
import { pb, updateUserSettings } from "@/lib/api.ts"
|
import { pb, updateUserSettings } from "@/lib/api.ts"
|
||||||
import { dynamicActivate, getLocale } from "@/lib/i18n"
|
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"
|
import * as systemsManager from "@/lib/systemsManager.ts"
|
||||||
|
|
||||||
const LoginPage = lazy(() => import("@/components/login/login.tsx"))
|
const LoginPage = lazy(() => import("@/components/login/login.tsx"))
|
||||||
@@ -71,6 +71,7 @@ const Layout = () => {
|
|||||||
const authenticated = useStore($authenticated)
|
const authenticated = useStore($authenticated)
|
||||||
const copyContent = useStore($copyContent)
|
const copyContent = useStore($copyContent)
|
||||||
const direction = useStore($direction)
|
const direction = useStore($direction)
|
||||||
|
const userSettings = useStore($userSettings)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.dir = direction
|
document.documentElement.dir = direction
|
||||||
@@ -96,7 +97,7 @@ const Layout = () => {
|
|||||||
<LoginPage />
|
<LoginPage />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div style={{"--container": `${userSettings.layoutWidth ?? 1480}px`} as React.CSSProperties}>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +109,7 @@ const Layout = () => {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DirectionProvider>
|
</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
|
ct?: ConnectionType
|
||||||
/** extra filesystem percentages */
|
/** extra filesystem percentages */
|
||||||
efs?: Record<string, number>
|
efs?: Record<string, number>
|
||||||
|
/** services [totalServices, numFailedServices] */
|
||||||
|
sv?: [number, number]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemStats {
|
export interface SystemStats {
|
||||||
@@ -279,6 +281,7 @@ export interface UserSettings {
|
|||||||
colorWarn?: number
|
colorWarn?: number
|
||||||
colorCrit?: number
|
colorCrit?: number
|
||||||
hourFormat?: HourFormat
|
hourFormat?: HourFormat
|
||||||
|
layoutWidth?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartDataContainer = {
|
type ChartDataContainer = {
|
||||||
|
|||||||
Reference in New Issue
Block a user