mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-21 06:53:29 +00:00
feat(frontend): show more mobile operational columns
Contract-Neutral: Phone-width responsive column presentation only; no API, ownership, route, or subsystem contract changed.
This commit is contained in:
parent
61bc4b25eb
commit
642d1ec5b0
9 changed files with 125 additions and 90 deletions
|
|
@ -1,32 +1,24 @@
|
|||
{
|
||||
"version": 1,
|
||||
"base_sha": "6ca27721ef6c00836bab7e169654692bbd1ed16e",
|
||||
"verified_at": "2026-08-18T15:17:29Z",
|
||||
"base_sha": "61bc4b25eb506848eee4303bbf888e71b57a1c17",
|
||||
"verified_at": "2026-08-18T15:50:10Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/App.tsx",
|
||||
"frontend-modern/src/AppLayout.tsx",
|
||||
"frontend-modern/src/components/shared/MobileNavBar.tsx",
|
||||
"frontend-modern/src/components/shared/Subtabs.tsx",
|
||||
"frontend-modern/src/components/shared/SummaryRowActionButton.tsx",
|
||||
"frontend-modern/src/components/shared/mobileNavBarModel.ts",
|
||||
"frontend-modern/src/components/Workloads/GuestRow.tsx",
|
||||
"frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx",
|
||||
"frontend-modern/src/components/Workloads/guestRowModel.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx",
|
||||
"frontend-modern/src/index.css"
|
||||
"frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/App.tsx": "2f8ec39abd969c9abc995105eccb9513a83dca9744d8c003d5b5e8cf0a69eec5",
|
||||
"frontend-modern/src/AppLayout.tsx": "9c4ffef00d4d09f76b744fdb43c961c09436b2a1620607e48721eed3a848f44e",
|
||||
"frontend-modern/src/components/shared/MobileNavBar.tsx": "a39ff906f6be2f35f3c6894615811e6e0566d068d7ac5ec89f14c0cb01d40751",
|
||||
"frontend-modern/src/components/shared/Subtabs.tsx": "277d29d71d710382bb72cbe80791133bd9093e7529d525319d42e52adf7668ad",
|
||||
"frontend-modern/src/components/shared/SummaryRowActionButton.tsx": "527c6226dd12a7b2e37167787db50c62c7de414658a163f6c316077bfaa2a862",
|
||||
"frontend-modern/src/components/shared/mobileNavBarModel.ts": "fd6614f3cb8d90776fba51c04b887f152e2e88952f149b9b5eacc1e848dccd14",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "7b8a9a56e4b31833d18c713a692a75148d786233a7a16c2339167517f7ad78a0",
|
||||
"frontend-modern/src/index.css": "6e775ca1cba645cac91b1fffc6c9680fe898d24d29398919e095870d8559f8fa"
|
||||
"frontend-modern/src/components/Workloads/GuestRow.tsx": "a8cf5d799db11989dd055ad119a66ac2bee52f003c0c28009bc0f47b9fc5386e",
|
||||
"frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx": "7e64ac79b0fb3bf6c5784201c880ae5812a6203609b634e0ed02f7af513f3858",
|
||||
"frontend-modern/src/components/Workloads/guestRowModel.tsx": "cb5165bc48fe29bf227a1fdcae3ecc11fcf93d3c761f3d283d2a5e67a95de620",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "59651f69ad19690750fc6e2235c9ed57d6b86c016b2d753eb2f60f9b752be62e",
|
||||
"frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts": "bd00307cdbf654f4b7968fddd114877e313974879f43734cecb67e2b5f4a909e"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox/overview",
|
||||
"/alerts/overview",
|
||||
"/actions"
|
||||
"/proxmox/overview"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
|
|
@ -36,28 +28,20 @@
|
|||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 320,
|
||||
"height": 740
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Desktop Alerts overview with the full navigation shell and loaded content",
|
||||
"Phone Proxmox overview with 37-pixel node and workload rows",
|
||||
"Phone Proxmox overview with the redundant Nodes card title removed",
|
||||
"Phone Proxmox filter controls in closed and expanded states",
|
||||
"Phone Proxmox node details in collapsed and expanded states",
|
||||
"Phone Actions page with compact page header and 44-pixel navigation rail",
|
||||
"Phone Alerts overview with compact page header"
|
||||
"Narrow Proxmox overview with node and workload tables fully loaded",
|
||||
"Node rows showing CPU, memory, aggregate disk, temperature, and age",
|
||||
"Workload rows showing type, ID, CPU, memory, aggregate disk, and age"
|
||||
],
|
||||
"interactions": [
|
||||
"Opened and closed the Proxmox filter controls at 390 by 844",
|
||||
"Expanded and collapsed a Proxmox node detail row at 390 by 844",
|
||||
"Confirmed twelve host and workload rows remained visible in a 320 by 740 viewport",
|
||||
"Confirmed document width matched viewport width at 390 and 320 pixels",
|
||||
"Confirmed shared compact headers and navigation remained overflow-free on Actions and Alerts",
|
||||
"Confirmed the desktop mobile rail remained hidden and Alerts retained its desktop geometry at 1440 by 900",
|
||||
"Confirmed final browser diagnostics contained no warnings or errors"
|
||||
"Reloaded the active Proxmox overview after each responsive column change",
|
||||
"Confirmed host headers render Node, CPU, Mem, Disk, Temp, and Age",
|
||||
"Confirmed workload headers render Name, Type, ID, CPU, Mem, Disk, and Age",
|
||||
"Confirmed multi-disk workloads use a readable aggregate percentage at phone width",
|
||||
"Confirmed both tables fit the 390-pixel viewport without document-level horizontal overflow",
|
||||
"Confirmed the full desktop node and workload column sets remain visible without horizontal overflow at 1440 by 900",
|
||||
"Captured the final narrow mobile rendering as pulse-mobile-final-density.png"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,9 @@ export function GuestRow(props: GuestRowProps) {
|
|||
const cpuPercent = createMemo(() => getWorkloadCPUPercent(props.guest.cpu) ?? 0);
|
||||
const metricDisplayMode = createMemo(() => props.metricDisplayMode ?? 'bars');
|
||||
const isSparklineMode = createMemo(() => metricDisplayMode() === 'sparklines');
|
||||
const usesCompactTableLayout = createMemo(
|
||||
() => props.workloadTableLayoutMode === 'phone' || props.workloadTableLayoutMode === 'mobile',
|
||||
);
|
||||
const telemetryAvailable = (
|
||||
metric: keyof NonNullable<WorkloadGuest['telemetryAvailability']>,
|
||||
): boolean => props.guest.telemetryAvailability?.[metric] ?? true;
|
||||
|
|
@ -309,7 +312,7 @@ export function GuestRow(props: GuestRowProps) {
|
|||
|
||||
{/* Info - merged identifier (VMID / image / namespace) for mixed-type views */}
|
||||
<Show when={isColVisible('info')}>
|
||||
<td class="px-1.5 sm:px-2 py-0.5 align-middle">
|
||||
<td class="px-0.5 py-0.5 align-middle sm:px-2">
|
||||
<div class="flex justify-center text-xs text-muted whitespace-nowrap">
|
||||
<Show
|
||||
when={infoValue()}
|
||||
|
|
@ -471,6 +474,7 @@ export function GuestRow(props: GuestRowProps) {
|
|||
}
|
||||
>
|
||||
<StackedDiskBar
|
||||
mode={usesCompactTableLayout() ? 'aggregate' : undefined}
|
||||
disks={props.guest.disks}
|
||||
aggregateDisk={props.guest.disk}
|
||||
anomaly={diskAnomaly()}
|
||||
|
|
@ -517,7 +521,10 @@ export function GuestRow(props: GuestRowProps) {
|
|||
<span
|
||||
class={`text-xs whitespace-nowrap ${props.guest.uptime > 0 && props.guest.uptime < 3600 ? 'text-orange-500' : 'text-muted'}`}
|
||||
>
|
||||
<Show when={isMobile()} fallback={formatUptime(props.guest.uptime)}>
|
||||
<Show
|
||||
when={isMobile() || usesCompactTableLayout()}
|
||||
fallback={formatUptime(props.guest.uptime)}
|
||||
>
|
||||
{formatUptime(props.guest.uptime, true)}
|
||||
</Show>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ export const getWorkloadColumnHeaderLabel = (
|
|||
): string => {
|
||||
if (compact && columnId === 'availability') return 'Up';
|
||||
if (compact && columnId === 'memory') return 'Mem';
|
||||
if (compact && columnId === 'info') return 'ID';
|
||||
if (compact && columnId === 'uptime') return 'Age';
|
||||
return columnId === 'memory' && memoryDisplayBasis === 'host'
|
||||
? `${defaultLabel} · Host`
|
||||
: defaultLabel;
|
||||
|
|
@ -67,12 +69,16 @@ export function WorkloadTableHeader(props: WorkloadTableHeaderProps) {
|
|||
const sortKeyForCol = col.sortKey as WorkloadSortKey | undefined;
|
||||
const isSortable = !!sortKeyForCol;
|
||||
const isSorted = () => sortKeyForCol && props.sortKey() === sortKeyForCol;
|
||||
const usesCompactHeader = () =>
|
||||
props.isMobile() ||
|
||||
props.workloadTableLayoutMode() === 'phone' ||
|
||||
props.workloadTableLayoutMode() === 'mobile';
|
||||
const label = () =>
|
||||
getWorkloadColumnHeaderLabel(
|
||||
col.id,
|
||||
col.label,
|
||||
props.workloadMemoryDisplayBasis(),
|
||||
props.isMobile(),
|
||||
usesCompactHeader(),
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -97,7 +103,7 @@ export function WorkloadTableHeader(props: WorkloadTableHeaderProps) {
|
|||
}
|
||||
>
|
||||
<div class={`flex min-h-[14px] items-center gap-0.5 ${alignClasses().flexJustify}`}>
|
||||
{col.icon ? (
|
||||
{col.icon && !(usesCompactHeader() && col.id === 'uptime') ? (
|
||||
<>
|
||||
<span class="flex items-center" aria-hidden="true">
|
||||
{col.icon}
|
||||
|
|
|
|||
|
|
@ -1091,14 +1091,14 @@ describe('GUEST_COLUMNS', () => {
|
|||
|
||||
it('derives mobile overrides from the canonical guest column model', () => {
|
||||
expect(getGuestColumnStyle('name', true)).toEqual({
|
||||
width: '62%',
|
||||
'max-width': '62%',
|
||||
width: '33%',
|
||||
'max-width': '33%',
|
||||
});
|
||||
expect(getGuestColumnStyle('cpu', true)).toEqual({
|
||||
width: '15%',
|
||||
'max-width': '15%',
|
||||
width: '11%',
|
||||
'max-width': '11%',
|
||||
});
|
||||
expect(getGuestColumnWidthStyle('name', true)).toEqual({ width: '62%' });
|
||||
expect(getGuestColumnWidthStyle('name', true)).toEqual({ width: '33%' });
|
||||
expect(getGuestColumnWidthStyle('diskIo', true)).toEqual({ width: '170px' });
|
||||
});
|
||||
|
||||
|
|
@ -1112,15 +1112,22 @@ describe('GUEST_COLUMNS', () => {
|
|||
expect(phoneColumns.map((column) => column.id)).toEqual([
|
||||
'name',
|
||||
'availability',
|
||||
'type',
|
||||
'info',
|
||||
'cpu',
|
||||
'memory',
|
||||
'disk',
|
||||
'uptime',
|
||||
]);
|
||||
expect(mobileColumns.map((column) => column.id)).toEqual([
|
||||
'name',
|
||||
'availability',
|
||||
'type',
|
||||
'info',
|
||||
'cpu',
|
||||
'memory',
|
||||
'disk',
|
||||
'uptime',
|
||||
]);
|
||||
expect(tabletColumns.map((column) => column.id)).toEqual([
|
||||
'name',
|
||||
|
|
@ -1130,6 +1137,7 @@ describe('GUEST_COLUMNS', () => {
|
|||
'cpu',
|
||||
'memory',
|
||||
'disk',
|
||||
'uptime',
|
||||
]);
|
||||
expect(compactColumns.map((column) => column.id)).toEqual([
|
||||
'name',
|
||||
|
|
|
|||
|
|
@ -18,5 +18,7 @@ describe('getWorkloadColumnHeaderLabel', () => {
|
|||
it('uses compact labels for narrow operational columns', () => {
|
||||
expect(getWorkloadColumnHeaderLabel('availability', 'Avail', 'guest', true)).toBe('Up');
|
||||
expect(getWorkloadColumnHeaderLabel('memory', 'Mem', 'host', true)).toBe('Mem');
|
||||
expect(getWorkloadColumnHeaderLabel('info', 'Info', 'guest', true)).toBe('ID');
|
||||
expect(getWorkloadColumnHeaderLabel('uptime', 'Uptime', 'guest', true)).toBe('Age');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,15 +42,14 @@ const WORKLOAD_COLUMN_MIN_LAYOUT: Record<string, WorkloadTableLayoutMode> = {
|
|||
runtime: 'tablet',
|
||||
cpu: 'phone',
|
||||
memory: 'phone',
|
||||
// A phone-width row needs enough identity space to distinguish similarly
|
||||
// named workloads. Disk remains available in the row details and returns in
|
||||
// the wider mobile layout; keeping it beside CPU and memory on phones reduced the
|
||||
// name to a handful of characters once the status and backup cues rendered.
|
||||
disk: 'mobile',
|
||||
type: 'tablet',
|
||||
info: 'tablet',
|
||||
// Phone rows retain the compact operational scan: workload kind, all three
|
||||
// capacity metrics, and age. The percentage model below preserves a useful
|
||||
// identity track without hiding those signals behind row expansion.
|
||||
disk: 'phone',
|
||||
type: 'phone',
|
||||
info: 'phone',
|
||||
vmid: 'tablet',
|
||||
uptime: 'compact',
|
||||
uptime: 'phone',
|
||||
backup: 'compact',
|
||||
image: 'compact',
|
||||
namespace: 'compact',
|
||||
|
|
@ -445,20 +444,24 @@ const GUEST_COLUMN_RESPONSIVE_WEIGHTS: Record<
|
|||
Record<string, number>
|
||||
> = {
|
||||
phone: {
|
||||
// Identity is the primary mobile task. Availability stays visible because
|
||||
// it can disagree with runtime health, while CPU and memory retain useful
|
||||
// bar widths. Disk rejoins this set at the wider-mobile breakpoint above.
|
||||
name: 62,
|
||||
availability: 8,
|
||||
cpu: 15,
|
||||
memory: 15,
|
||||
name: 33,
|
||||
availability: 5,
|
||||
type: 7,
|
||||
info: 9,
|
||||
cpu: 11,
|
||||
memory: 11,
|
||||
disk: 11,
|
||||
uptime: 13,
|
||||
},
|
||||
mobile: {
|
||||
name: 40,
|
||||
availability: 8,
|
||||
cpu: 17,
|
||||
memory: 18,
|
||||
disk: 17,
|
||||
name: 33,
|
||||
availability: 5,
|
||||
type: 7,
|
||||
info: 9,
|
||||
cpu: 11,
|
||||
memory: 11,
|
||||
disk: 11,
|
||||
uptime: 13,
|
||||
},
|
||||
tablet: {
|
||||
name: 30,
|
||||
|
|
|
|||
|
|
@ -74,12 +74,15 @@ import {
|
|||
// metricDisplayMode signal so the hosts table swaps to MetricMiniSparkline
|
||||
// whenever the user flips the toggle.
|
||||
|
||||
const formatNodeUptime = (seconds: number | undefined): { label: string; warn: boolean } => {
|
||||
const formatNodeUptime = (
|
||||
seconds: number | undefined,
|
||||
compact = false,
|
||||
): { label: string; warn: boolean } => {
|
||||
if (!seconds || seconds <= 0) return { label: '—', warn: false };
|
||||
// Full "26d 4h" precision matches v5 and the guest rows below; <1h keeps
|
||||
// the v5 "recently restarted" highlight.
|
||||
return {
|
||||
label: formatPlatformTableUptimeValue(seconds, { compact: false }),
|
||||
label: formatPlatformTableUptimeValue(seconds, { compact }),
|
||||
warn: seconds < 3_600,
|
||||
};
|
||||
};
|
||||
|
|
@ -260,7 +263,10 @@ export const ProxmoxNodesTable: Component<{
|
|||
<PlatformSortableTableHead kind={column.kind} sort={sort} sortKey={column.id}>
|
||||
{(layoutMode() === 'phone' || layoutMode() === 'mobile') && column.id === 'memory'
|
||||
? 'Mem'
|
||||
: column.label}
|
||||
: (layoutMode() === 'phone' || layoutMode() === 'mobile') &&
|
||||
column.id === 'uptime'
|
||||
? 'Age'
|
||||
: column.label}
|
||||
</PlatformSortableTableHead>
|
||||
)}
|
||||
</For>
|
||||
|
|
@ -312,7 +318,11 @@ export const ProxmoxNodesTable: Component<{
|
|||
return 'Online';
|
||||
});
|
||||
const isOnline = createMemo(() => availabilityLabel() === 'Online');
|
||||
const uptime = () => formatNodeUptime(isOnline() ? node.uptime : undefined);
|
||||
const usesCompactMetrics = () =>
|
||||
layoutMode() === 'phone' || layoutMode() === 'mobile';
|
||||
const uptime = () =>
|
||||
formatNodeUptime(isOnline() ? node.uptime : undefined, usesCompactMetrics());
|
||||
const fullUptime = () => formatNodeUptime(isOnline() ? node.uptime : undefined).label;
|
||||
const metricsKey = () => buildMetricKeyForUnifiedResource(node);
|
||||
const temperature = () => node.temperature;
|
||||
const alertResourceIds = () => hostOverrideIdCandidates(node);
|
||||
|
|
@ -442,6 +452,7 @@ export const ProxmoxNodesTable: Component<{
|
|||
? 'text-orange-600 dark:text-orange-400'
|
||||
: 'text-base-content'
|
||||
}`}
|
||||
title={usesCompactMetrics() ? fullUptime() : undefined}
|
||||
>
|
||||
{uptime().label}
|
||||
</TableCell>
|
||||
|
|
@ -527,7 +538,11 @@ export const ProxmoxNodesTable: Component<{
|
|||
fallback={
|
||||
<StackedDiskBar
|
||||
mode={
|
||||
(node.agent?.disks?.length ?? 0) > 1 ? 'vertical-bars' : undefined
|
||||
usesCompactMetrics()
|
||||
? 'aggregate'
|
||||
: (node.agent?.disks?.length ?? 0) > 1
|
||||
? 'vertical-bars'
|
||||
: undefined
|
||||
}
|
||||
disks={normalizeDiskArray(node.agent?.disks)}
|
||||
aggregateDisk={aggregateDisk()}
|
||||
|
|
|
|||
|
|
@ -8,25 +8,30 @@ import {
|
|||
} from '../proxmoxHostTableModel';
|
||||
|
||||
describe('proxmoxHostTableModel', () => {
|
||||
it('reserves phone space for an identifying node label and the two fastest health signals', () => {
|
||||
it('shows the full operational metric track at phone width', () => {
|
||||
const columns = getProxmoxHostVisibleColumnsForLayout('phone');
|
||||
const ids = columns.map((column) => column.id);
|
||||
|
||||
expect(ids).toEqual(['node', 'cpu', 'memory']);
|
||||
expect(ids).toEqual(['node', 'cpu', 'memory', 'disk', 'temp', 'uptime']);
|
||||
expect(getProxmoxHostTableMinWidthClass('phone')).toBe('min-w-[0px]');
|
||||
expect(getProxmoxHostColumnWidthStyle('node', 'phone', ids)).toEqual({ width: '60%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('node', 'phone', ids)).toEqual({ width: '32%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('disk', 'phone', ids)).toEqual({ width: '13%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('temp', 'phone', ids)).toEqual({ width: '15%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('uptime', 'phone', ids)).toEqual({ width: '14%' });
|
||||
});
|
||||
|
||||
it('prioritizes live utilization columns in the mobile host table', () => {
|
||||
const columns = getProxmoxHostVisibleColumnsForLayout('mobile');
|
||||
const ids = columns.map((column) => column.id);
|
||||
|
||||
expect(ids).toEqual(['node', 'cpu', 'memory', 'disk']);
|
||||
expect(ids).toEqual(['node', 'cpu', 'memory', 'disk', 'temp', 'uptime']);
|
||||
expect(getProxmoxHostTableMinWidthClass('mobile')).toBe('min-w-[0px]');
|
||||
expect(getProxmoxHostColumnWidthStyle('node', 'mobile', ids)).toEqual({ width: '40%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('cpu', 'mobile', ids)).toEqual({ width: '20%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('memory', 'mobile', ids)).toEqual({ width: '20%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('disk', 'mobile', ids)).toEqual({ width: '20%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('node', 'mobile', ids)).toEqual({ width: '32%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('cpu', 'mobile', ids)).toEqual({ width: '13%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('memory', 'mobile', ids)).toEqual({ width: '13%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('disk', 'mobile', ids)).toEqual({ width: '13%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('temp', 'mobile', ids)).toEqual({ width: '15%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('uptime', 'mobile', ids)).toEqual({ width: '14%' });
|
||||
});
|
||||
|
||||
it('adds operational context before inventory metadata on tablet', () => {
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ const HOST_COLUMN_MIN_LAYOUT: Record<ProxmoxHostTableColumnId, WorkloadTableLayo
|
|||
node: 'phone',
|
||||
cpu: 'phone',
|
||||
memory: 'phone',
|
||||
disk: 'mobile',
|
||||
temp: 'tablet',
|
||||
uptime: 'tablet',
|
||||
disk: 'phone',
|
||||
temp: 'phone',
|
||||
uptime: 'phone',
|
||||
cluster: 'tablet',
|
||||
vms: 'compact',
|
||||
cts: 'compact',
|
||||
|
|
@ -58,15 +58,20 @@ const HOST_COLUMN_RESPONSIVE_WEIGHTS: Record<
|
|||
Partial<Record<ProxmoxHostTableColumnId, number>>
|
||||
> = {
|
||||
phone: {
|
||||
node: 60,
|
||||
cpu: 20,
|
||||
memory: 20,
|
||||
node: 32,
|
||||
cpu: 13,
|
||||
memory: 13,
|
||||
disk: 13,
|
||||
temp: 15,
|
||||
uptime: 14,
|
||||
},
|
||||
mobile: {
|
||||
node: 40,
|
||||
cpu: 20,
|
||||
memory: 20,
|
||||
disk: 20,
|
||||
node: 32,
|
||||
cpu: 13,
|
||||
memory: 13,
|
||||
disk: 13,
|
||||
temp: 15,
|
||||
uptime: 14,
|
||||
},
|
||||
tablet: {
|
||||
node: 22,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue