This commit is contained in:
henrygd
2026-03-05 16:10:19 -05:00
parent 1243a7bd8d
commit d9e3c4678a
7 changed files with 316 additions and 180 deletions

View File

@@ -3,9 +3,9 @@ import { Button } from "@/components/ui/button"
import { cn, decimalString, formatBytes, hourWithSeconds, toFixedFloat } from "@/lib/utils"
import type { PveVmRecord } from "@/types"
import {
ArrowUpDownIcon,
ClockIcon,
CpuIcon,
HardDriveIcon,
MemoryStickIcon,
MonitorIcon,
ServerIcon,
@@ -42,7 +42,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
accessorFn: (record) => record.name,
header: ({ column }) => <HeaderButton column={column} name={t`Name`} Icon={MonitorIcon} />,
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} />,
cell: ({ getValue }) => {
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 }) => {
const type = getValue() as string
return (
<Badge variant="outline" className="dark:border-white/12 ms-1.5">
<Badge variant="outline" className="dark:border-white/12 ms-1">
{type}
</Badge>
)
@@ -81,7 +81,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
header: ({ column }) => <HeaderButton column={column} name={t`CPU`} Icon={CpuIcon} />,
cell: ({ getValue }) => {
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 formatted = formatBytes(val, false, undefined, true)
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",
accessorFn: (record) => record.net,
id: "maxmem",
accessorFn: (record) => record.maxmem,
header: ({ column }) => <HeaderButton column={column} name={t`Max`} Icon={MemoryStickIcon} />,
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 }) => {
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 (
<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",
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,
cell: ({ getValue }) => {
return <span className="ms-1.5 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>
return <span className="ms-1 tabular-nums">{getValue() as number}</span>
},
},
{
@@ -136,7 +181,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
invertSorting: true,
header: ({ column }) => <HeaderButton column={column} name={t`Uptime`} Icon={TimerIcon} />,
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} />,
cell: ({ getValue }) => {
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" />}
{name}
<ArrowUpDownIcon className="size-4" />
{/* <ArrowUpDownIcon className="size-4" /> */}
</Button>
)
}

View File

@@ -46,7 +46,6 @@ export default function PveTable({ systemId }: { systemId?: string }) {
function fetchData(systemId?: string) {
pb.collection<PveVmRecord>("pve_vms")
.getList(0, 2000, {
fields: "id,name,type,cpu,mem,net,maxcpu,maxmem,uptime,system,updated",
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
})
.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="px-2 sm:px-1">
<CardTitle className="mb-2">
<Trans>All Proxmox VMs</Trans>
<Trans>Proxmox Resources</Trans>
</CardTitle>
<CardDescription className="flex">
<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 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 (
<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>
<dt className="text-muted-foreground">
<Trans>Network</Trans>
<Trans>Upload</Trans>
</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">
<Trans>vCPUs</Trans>
@@ -308,6 +316,21 @@ function PveVmSheet({
</dt>
<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">
<Trans>Uptime</Trans>
</dt>

View File

@@ -285,14 +285,22 @@ export interface PveVmRecord extends RecordModel {
cpu: number
/** Memory used (MB) */
mem: number
/** Network bandwidth (bytes/s, combined send+recv) */
net: number
/** Total upload (bytes, sent by VM) */
netout: number
/** Total download (bytes, received by VM) */
netin: number
/** Max vCPU count */
maxcpu: number
/** Max memory (bytes) */
maxmem: number
/** Uptime (seconds) */
uptime: number
/** Cumulative disk read (bytes) */
diskread: number
/** Cumulative disk write (bytes) */
diskwrite: number
/** Allocated disk size (bytes) */
disk: number
/** Unix timestamp (ms) */
updated: number
}
@@ -447,116 +455,116 @@ export interface SystemdRecord extends RecordModel {
}
export interface SystemdServiceDetails {
AccessSELinuxContext: string;
ActivationDetails: any[];
ActiveEnterTimestamp: number;
ActiveEnterTimestampMonotonic: number;
ActiveExitTimestamp: number;
ActiveExitTimestampMonotonic: number;
ActiveState: string;
After: string[];
AllowIsolate: boolean;
AssertResult: boolean;
AssertTimestamp: number;
AssertTimestampMonotonic: number;
Asserts: any[];
Before: string[];
BindsTo: any[];
BoundBy: any[];
CPUUsageNSec: number;
CanClean: any[];
CanFreeze: boolean;
CanIsolate: boolean;
CanLiveMount: boolean;
CanReload: boolean;
CanStart: boolean;
CanStop: boolean;
CollectMode: string;
ConditionResult: boolean;
ConditionTimestamp: number;
ConditionTimestampMonotonic: number;
Conditions: any[];
ConflictedBy: any[];
Conflicts: string[];
ConsistsOf: any[];
DebugInvocation: boolean;
DefaultDependencies: boolean;
Description: string;
Documentation: string[];
DropInPaths: any[];
ExecMainPID: number;
FailureAction: string;
FailureActionExitStatus: number;
Following: string;
FragmentPath: string;
FreezerState: string;
Id: string;
IgnoreOnIsolate: boolean;
InactiveEnterTimestamp: number;
InactiveEnterTimestampMonotonic: number;
InactiveExitTimestamp: number;
InactiveExitTimestampMonotonic: number;
InvocationID: string;
Job: Array<number | string>;
JobRunningTimeoutUSec: number;
JobTimeoutAction: string;
JobTimeoutRebootArgument: string;
JobTimeoutUSec: number;
JoinsNamespaceOf: any[];
LoadError: string[];
LoadState: string;
MainPID: number;
Markers: any[];
MemoryCurrent: number;
MemoryLimit: number;
MemoryPeak: number;
NRestarts: number;
Names: string[];
NeedDaemonReload: boolean;
OnFailure: any[];
OnFailureJobMode: string;
OnFailureOf: any[];
OnSuccess: any[];
OnSuccessJobMode: string;
OnSuccessOf: any[];
PartOf: any[];
Perpetual: boolean;
PropagatesReloadTo: any[];
PropagatesStopTo: any[];
RebootArgument: string;
Refs: any[];
RefuseManualStart: boolean;
RefuseManualStop: boolean;
ReloadPropagatedFrom: any[];
RequiredBy: any[];
Requires: string[];
RequiresMountsFor: any[];
Requisite: any[];
RequisiteOf: any[];
Result: string;
SliceOf: any[];
SourcePath: string;
StartLimitAction: string;
StartLimitBurst: number;
StartLimitIntervalUSec: number;
StateChangeTimestamp: number;
StateChangeTimestampMonotonic: number;
StopPropagatedFrom: any[];
StopWhenUnneeded: boolean;
SubState: string;
SuccessAction: string;
SuccessActionExitStatus: number;
SurviveFinalKillSignal: boolean;
TasksCurrent: number;
TasksMax: number;
Transient: boolean;
TriggeredBy: string[];
Triggers: any[];
UnitFilePreset: string;
UnitFileState: string;
UpheldBy: any[];
Upholds: any[];
WantedBy: any[];
Wants: string[];
WantsMountsFor: any[];
}
AccessSELinuxContext: string
ActivationDetails: any[]
ActiveEnterTimestamp: number
ActiveEnterTimestampMonotonic: number
ActiveExitTimestamp: number
ActiveExitTimestampMonotonic: number
ActiveState: string
After: string[]
AllowIsolate: boolean
AssertResult: boolean
AssertTimestamp: number
AssertTimestampMonotonic: number
Asserts: any[]
Before: string[]
BindsTo: any[]
BoundBy: any[]
CPUUsageNSec: number
CanClean: any[]
CanFreeze: boolean
CanIsolate: boolean
CanLiveMount: boolean
CanReload: boolean
CanStart: boolean
CanStop: boolean
CollectMode: string
ConditionResult: boolean
ConditionTimestamp: number
ConditionTimestampMonotonic: number
Conditions: any[]
ConflictedBy: any[]
Conflicts: string[]
ConsistsOf: any[]
DebugInvocation: boolean
DefaultDependencies: boolean
Description: string
Documentation: string[]
DropInPaths: any[]
ExecMainPID: number
FailureAction: string
FailureActionExitStatus: number
Following: string
FragmentPath: string
FreezerState: string
Id: string
IgnoreOnIsolate: boolean
InactiveEnterTimestamp: number
InactiveEnterTimestampMonotonic: number
InactiveExitTimestamp: number
InactiveExitTimestampMonotonic: number
InvocationID: string
Job: Array<number | string>
JobRunningTimeoutUSec: number
JobTimeoutAction: string
JobTimeoutRebootArgument: string
JobTimeoutUSec: number
JoinsNamespaceOf: any[]
LoadError: string[]
LoadState: string
MainPID: number
Markers: any[]
MemoryCurrent: number
MemoryLimit: number
MemoryPeak: number
NRestarts: number
Names: string[]
NeedDaemonReload: boolean
OnFailure: any[]
OnFailureJobMode: string
OnFailureOf: any[]
OnSuccess: any[]
OnSuccessJobMode: string
OnSuccessOf: any[]
PartOf: any[]
Perpetual: boolean
PropagatesReloadTo: any[]
PropagatesStopTo: any[]
RebootArgument: string
Refs: any[]
RefuseManualStart: boolean
RefuseManualStop: boolean
ReloadPropagatedFrom: any[]
RequiredBy: any[]
Requires: string[]
RequiresMountsFor: any[]
Requisite: any[]
RequisiteOf: any[]
Result: string
SliceOf: any[]
SourcePath: string
StartLimitAction: string
StartLimitBurst: number
StartLimitIntervalUSec: number
StateChangeTimestamp: number
StateChangeTimestampMonotonic: number
StopPropagatedFrom: any[]
StopWhenUnneeded: boolean
SubState: string
SuccessAction: string
SuccessActionExitStatus: number
SurviveFinalKillSignal: boolean
TasksCurrent: number
TasksMax: number
Transient: boolean
TriggeredBy: string[]
Triggers: any[]
UnitFilePreset: string
UnitFileState: string
UpheldBy: any[]
Upholds: any[]
WantedBy: any[]
Wants: string[]
WantsMountsFor: any[]
}