phase 2: frontend types and useResources hook
- Created resource.ts with TypeScript types for unified Resource model - ResourceType, PlatformType, SourceType, ResourceStatus enums - Resource interface matching backend ResourceFrontend - Helper functions: isInfrastructure, isWorkload, getDisplayName, etc. - ResourceFilter interface for complex filtering - Updated api.ts State interface to include optional resources array - Created useResources hook for accessing unified resources - Reactive access via getGlobalWebSocketStore - Pre-computed memos for infra, workloads, statusCounts - Filtering methods: byType, byPlatform, filtered - Query helpers: get, children, topByCpu, topByMemory - Created useResourcesAsLegacy helper for migration - Converts resources to legacy VM/Container formats - Enables gradual component migration This provides the foundation for migrating frontend pages to use the unified resource model.
This commit is contained in:
parent
cdd2249e00
commit
55f10ce36c
3 changed files with 513 additions and 0 deletions
315
frontend-modern/src/hooks/useResources.ts
Normal file
315
frontend-modern/src/hooks/useResources.ts
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
/**
|
||||||
|
* useResources Hook
|
||||||
|
*
|
||||||
|
* Provides reactive access to unified resources from the WebSocket state.
|
||||||
|
* This is the primary way for frontend components to access resource data
|
||||||
|
* as we migrate away from legacy arrays (nodes, vms, containers, etc.).
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
* ```tsx
|
||||||
|
* const { resources, infra, workloads, filtered } = useResources();
|
||||||
|
*
|
||||||
|
* // Get all resources
|
||||||
|
* <For each={resources()}>{r => <div>{r.name}</div>}</For>
|
||||||
|
*
|
||||||
|
* // Get only infrastructure (nodes, hosts)
|
||||||
|
* <For each={infra()}>{r => <div>{r.name}</div>}</For>
|
||||||
|
*
|
||||||
|
* // Get filtered workloads
|
||||||
|
* const vms = filtered({ types: ['vm'], statuses: ['running'] });
|
||||||
|
* <For each={vms}>{r => <div>{r.name}</div>}</For>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createMemo, type Accessor } from 'solid-js';
|
||||||
|
import { getGlobalWebSocketStore } from '@/stores/websocket-global';
|
||||||
|
import type {
|
||||||
|
Resource,
|
||||||
|
ResourceType,
|
||||||
|
PlatformType,
|
||||||
|
ResourceStatus,
|
||||||
|
ResourceFilter,
|
||||||
|
} from '@/types/resource';
|
||||||
|
import {
|
||||||
|
isInfrastructure,
|
||||||
|
isWorkload,
|
||||||
|
getDisplayName,
|
||||||
|
getCpuPercent,
|
||||||
|
getMemoryPercent,
|
||||||
|
getDiskPercent,
|
||||||
|
} from '@/types/resource';
|
||||||
|
|
||||||
|
export interface UseResourcesReturn {
|
||||||
|
/** All unified resources */
|
||||||
|
resources: Accessor<Resource[]>;
|
||||||
|
|
||||||
|
/** Infrastructure resources only (nodes, hosts, docker-hosts) */
|
||||||
|
infra: Accessor<Resource[]>;
|
||||||
|
|
||||||
|
/** Workload resources only (vms, containers, docker-containers) */
|
||||||
|
workloads: Accessor<Resource[]>;
|
||||||
|
|
||||||
|
/** Resources by type */
|
||||||
|
byType: (type: ResourceType) => Resource[];
|
||||||
|
|
||||||
|
/** Resources by platform */
|
||||||
|
byPlatform: (platform: PlatformType) => Resource[];
|
||||||
|
|
||||||
|
/** Filter resources with multiple criteria */
|
||||||
|
filtered: (filter: ResourceFilter) => Resource[];
|
||||||
|
|
||||||
|
/** Get a single resource by ID */
|
||||||
|
get: (id: string) => Resource | undefined;
|
||||||
|
|
||||||
|
/** Get children of a resource (e.g., VMs of a node) */
|
||||||
|
children: (parentId: string) => Resource[];
|
||||||
|
|
||||||
|
/** Count of resources by status */
|
||||||
|
statusCounts: Accessor<Record<ResourceStatus, number>>;
|
||||||
|
|
||||||
|
/** Top consumers by CPU */
|
||||||
|
topByCpu: (limit?: number) => Resource[];
|
||||||
|
|
||||||
|
/** Top consumers by memory */
|
||||||
|
topByMemory: (limit?: number) => Resource[];
|
||||||
|
|
||||||
|
/** Whether resources are available */
|
||||||
|
hasResources: Accessor<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for accessing unified resources with reactive filtering.
|
||||||
|
*/
|
||||||
|
export function useResources(): UseResourcesReturn {
|
||||||
|
// Get the WebSocket store instance
|
||||||
|
const wsStore = getGlobalWebSocketStore();
|
||||||
|
|
||||||
|
// All resources from WebSocket state
|
||||||
|
const resources = createMemo<Resource[]>(() => {
|
||||||
|
return wsStore.state.resources ?? [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pre-filtered memos for common use cases
|
||||||
|
const infra = createMemo<Resource[]>(() => {
|
||||||
|
return resources().filter(isInfrastructure);
|
||||||
|
});
|
||||||
|
|
||||||
|
const workloads = createMemo<Resource[]>(() => {
|
||||||
|
return resources().filter(isWorkload);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasResources = createMemo<boolean>(() => {
|
||||||
|
return resources().length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status counts
|
||||||
|
const statusCounts = createMemo<Record<ResourceStatus, number>>(() => {
|
||||||
|
const counts: Record<ResourceStatus, number> = {
|
||||||
|
online: 0,
|
||||||
|
offline: 0,
|
||||||
|
running: 0,
|
||||||
|
stopped: 0,
|
||||||
|
degraded: 0,
|
||||||
|
paused: 0,
|
||||||
|
unknown: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const r of resources()) {
|
||||||
|
if (r.status in counts) {
|
||||||
|
counts[r.status as ResourceStatus]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter by type
|
||||||
|
const byType = (type: ResourceType): Resource[] => {
|
||||||
|
return resources().filter(r => r.type === type);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter by platform
|
||||||
|
const byPlatform = (platform: PlatformType): Resource[] => {
|
||||||
|
return resources().filter(r => r.platformType === platform);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Complex filtering
|
||||||
|
const filtered = (filter: ResourceFilter): Resource[] => {
|
||||||
|
let result = resources();
|
||||||
|
|
||||||
|
// Filter by types
|
||||||
|
if (filter.types && filter.types.length > 0) {
|
||||||
|
result = result.filter(r => filter.types!.includes(r.type));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by platforms
|
||||||
|
if (filter.platforms && filter.platforms.length > 0) {
|
||||||
|
result = result.filter(r => filter.platforms!.includes(r.platformType));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by statuses
|
||||||
|
if (filter.statuses && filter.statuses.length > 0) {
|
||||||
|
result = result.filter(r => filter.statuses!.includes(r.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by parent
|
||||||
|
if (filter.parentId) {
|
||||||
|
result = result.filter(r => r.parentId === filter.parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by cluster
|
||||||
|
if (filter.clusterId) {
|
||||||
|
result = result.filter(r => r.clusterId === filter.clusterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by alerts
|
||||||
|
if (filter.hasAlerts) {
|
||||||
|
result = result.filter(r => r.alerts && r.alerts.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search filter (name, displayName)
|
||||||
|
if (filter.search && filter.search.trim()) {
|
||||||
|
const term = filter.search.toLowerCase().trim();
|
||||||
|
result = result.filter(r => {
|
||||||
|
const name = getDisplayName(r).toLowerCase();
|
||||||
|
const id = r.id.toLowerCase();
|
||||||
|
return name.includes(term) || id.includes(term);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get single resource by ID
|
||||||
|
const get = (id: string): Resource | undefined => {
|
||||||
|
return resources().find(r => r.id === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get children of a parent
|
||||||
|
const children = (parentId: string): Resource[] => {
|
||||||
|
return resources().filter(r => r.parentId === parentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Top by CPU
|
||||||
|
const topByCpu = (limit: number = 10): Resource[] => {
|
||||||
|
return [...resources()]
|
||||||
|
.filter(r => r.cpu && r.cpu.current > 0)
|
||||||
|
.sort((a, b) => getCpuPercent(b) - getCpuPercent(a))
|
||||||
|
.slice(0, limit);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Top by memory
|
||||||
|
const topByMemory = (limit: number = 10): Resource[] => {
|
||||||
|
return [...resources()]
|
||||||
|
.filter(r => r.memory)
|
||||||
|
.sort((a, b) => getMemoryPercent(b) - getMemoryPercent(a))
|
||||||
|
.slice(0, limit);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
resources,
|
||||||
|
infra,
|
||||||
|
workloads,
|
||||||
|
byType,
|
||||||
|
byPlatform,
|
||||||
|
filtered,
|
||||||
|
get,
|
||||||
|
children,
|
||||||
|
statusCounts,
|
||||||
|
topByCpu,
|
||||||
|
topByMemory,
|
||||||
|
hasResources,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper hook for resource-to-legacy-type conversion.
|
||||||
|
* This helps during migration when components still expect legacy types.
|
||||||
|
*/
|
||||||
|
export function useResourcesAsLegacy() {
|
||||||
|
const { resources, byType } = useResources();
|
||||||
|
|
||||||
|
// Convert resources to legacy VM format
|
||||||
|
const asVMs = createMemo(() => {
|
||||||
|
return byType('vm').map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
vmid: parseInt(r.id.split('-').pop() ?? '0', 10),
|
||||||
|
name: r.name,
|
||||||
|
node: r.parentId ?? '',
|
||||||
|
instance: r.platformId,
|
||||||
|
status: r.status === 'running' ? 'running' : 'stopped',
|
||||||
|
type: 'qemu',
|
||||||
|
cpu: r.cpu?.current ?? 0,
|
||||||
|
cpus: 1, // Not available in unified model
|
||||||
|
memory: r.memory ? {
|
||||||
|
total: r.memory.total ?? 0,
|
||||||
|
used: r.memory.used ?? 0,
|
||||||
|
free: r.memory.free ?? 0,
|
||||||
|
usage: r.memory.current,
|
||||||
|
} : { total: 0, used: 0, free: 0, usage: 0 },
|
||||||
|
disk: r.disk ? {
|
||||||
|
total: r.disk.total ?? 0,
|
||||||
|
used: r.disk.used ?? 0,
|
||||||
|
free: r.disk.free ?? 0,
|
||||||
|
usage: r.disk.current,
|
||||||
|
} : { total: 0, used: 0, free: 0, usage: 0 },
|
||||||
|
networkIn: r.network?.rxBytes ?? 0,
|
||||||
|
networkOut: r.network?.txBytes ?? 0,
|
||||||
|
diskRead: 0,
|
||||||
|
diskWrite: 0,
|
||||||
|
uptime: r.uptime ?? 0,
|
||||||
|
template: false,
|
||||||
|
lastBackup: 0,
|
||||||
|
tags: r.tags ?? [],
|
||||||
|
lock: '',
|
||||||
|
lastSeen: new Date(r.lastSeen).toISOString(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert resources to legacy Container format
|
||||||
|
const asContainers = createMemo(() => {
|
||||||
|
return byType('container').map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
vmid: parseInt(r.id.split('-').pop() ?? '0', 10),
|
||||||
|
name: r.name,
|
||||||
|
node: r.parentId ?? '',
|
||||||
|
instance: r.platformId,
|
||||||
|
status: r.status === 'running' ? 'running' : 'stopped',
|
||||||
|
type: 'lxc',
|
||||||
|
cpu: r.cpu?.current ?? 0,
|
||||||
|
cpus: 1,
|
||||||
|
memory: r.memory ? {
|
||||||
|
total: r.memory.total ?? 0,
|
||||||
|
used: r.memory.used ?? 0,
|
||||||
|
free: r.memory.free ?? 0,
|
||||||
|
usage: r.memory.current,
|
||||||
|
} : { total: 0, used: 0, free: 0, usage: 0 },
|
||||||
|
disk: r.disk ? {
|
||||||
|
total: r.disk.total ?? 0,
|
||||||
|
used: r.disk.used ?? 0,
|
||||||
|
free: r.disk.free ?? 0,
|
||||||
|
usage: r.disk.current,
|
||||||
|
} : { total: 0, used: 0, free: 0, usage: 0 },
|
||||||
|
networkIn: r.network?.rxBytes ?? 0,
|
||||||
|
networkOut: r.network?.txBytes ?? 0,
|
||||||
|
diskRead: 0,
|
||||||
|
diskWrite: 0,
|
||||||
|
uptime: r.uptime ?? 0,
|
||||||
|
template: false,
|
||||||
|
lastBackup: 0,
|
||||||
|
tags: r.tags ?? [],
|
||||||
|
lock: '',
|
||||||
|
lastSeen: new Date(r.lastSeen).toISOString(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
resources,
|
||||||
|
asVMs,
|
||||||
|
asContainers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export types and helpers
|
||||||
|
export type { Resource, ResourceType, PlatformType, ResourceStatus, ResourceFilter };
|
||||||
|
export { isInfrastructure, isWorkload, getDisplayName, getCpuPercent, getMemoryPercent, getDiskPercent };
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// Properly typed TypeScript interfaces for Pulse API
|
// Properly typed TypeScript interfaces for Pulse API
|
||||||
|
|
||||||
|
import type { Resource } from './resource';
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
vms: VM[];
|
vms: VM[];
|
||||||
|
|
@ -25,6 +27,8 @@ export interface State {
|
||||||
recentlyResolved: ResolvedAlert[];
|
recentlyResolved: ResolvedAlert[];
|
||||||
lastUpdate: string;
|
lastUpdate: string;
|
||||||
temperatureMonitoringEnabled?: boolean;
|
temperatureMonitoringEnabled?: boolean;
|
||||||
|
// Unified resources (new data model - eventually replaces legacy arrays above)
|
||||||
|
resources?: Resource[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemovedDockerHost {
|
export interface RemovedDockerHost {
|
||||||
|
|
|
||||||
194
frontend-modern/src/types/resource.ts
Normal file
194
frontend-modern/src/types/resource.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
/**
|
||||||
|
* Unified Resource Types
|
||||||
|
*
|
||||||
|
* These types define the unified resource model that normalizes all monitored
|
||||||
|
* entities (VMs, containers, hosts, etc.) into a common structure.
|
||||||
|
*
|
||||||
|
* The frontend receives these via WebSocket state.resources[].
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Resource types - what kind of entity is being monitored
|
||||||
|
export type ResourceType =
|
||||||
|
| 'node' // Proxmox VE node
|
||||||
|
| 'host' // Standalone host (via host-agent)
|
||||||
|
| 'docker-host' // Docker/Podman host
|
||||||
|
| 'k8s-node' // Kubernetes node
|
||||||
|
| 'truenas' // TrueNAS system
|
||||||
|
| 'vm' // Proxmox VM
|
||||||
|
| 'container' // LXC container
|
||||||
|
| 'docker-container' // Docker container
|
||||||
|
| 'pod' // Kubernetes pod
|
||||||
|
| 'jail' // BSD jail
|
||||||
|
| 'docker-service' // Docker Swarm service
|
||||||
|
| 'k8s-deployment' // Kubernetes deployment
|
||||||
|
| 'k8s-service' // Kubernetes service
|
||||||
|
| 'storage' // Storage resource
|
||||||
|
| 'datastore' // PBS datastore
|
||||||
|
| 'pool' // ZFS/Ceph pool
|
||||||
|
| 'dataset' // ZFS dataset
|
||||||
|
| 'pbs' // Proxmox Backup Server
|
||||||
|
| 'pmg'; // Proxmox Mail Gateway
|
||||||
|
|
||||||
|
// Platform types - which system the resource comes from
|
||||||
|
export type PlatformType =
|
||||||
|
| 'proxmox-pve'
|
||||||
|
| 'proxmox-pbs'
|
||||||
|
| 'proxmox-pmg'
|
||||||
|
| 'docker'
|
||||||
|
| 'kubernetes'
|
||||||
|
| 'truenas'
|
||||||
|
| 'host-agent';
|
||||||
|
|
||||||
|
// Source types - how data is collected
|
||||||
|
export type SourceType =
|
||||||
|
| 'api' // Data from polling an API
|
||||||
|
| 'agent' // Data pushed from agent
|
||||||
|
| 'hybrid'; // Both sources, agent preferred
|
||||||
|
|
||||||
|
// Resource status - operational state
|
||||||
|
export type ResourceStatus =
|
||||||
|
| 'online'
|
||||||
|
| 'offline'
|
||||||
|
| 'running'
|
||||||
|
| 'stopped'
|
||||||
|
| 'degraded'
|
||||||
|
| 'paused'
|
||||||
|
| 'unknown';
|
||||||
|
|
||||||
|
// Metric value with optional limits
|
||||||
|
export interface ResourceMetric {
|
||||||
|
current: number; // Current value (percentage or bytes)
|
||||||
|
total?: number; // Total capacity (bytes) - null for percentages
|
||||||
|
used?: number; // Used amount (bytes)
|
||||||
|
free?: number; // Free amount (bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network I/O metrics
|
||||||
|
export interface ResourceNetwork {
|
||||||
|
rxBytes: number; // Total bytes received
|
||||||
|
txBytes: number; // Total bytes transmitted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert associated with a resource
|
||||||
|
export interface ResourceAlert {
|
||||||
|
id: string;
|
||||||
|
type: string; // cpu, memory, disk, temperature, etc.
|
||||||
|
level: string; // warning, critical
|
||||||
|
message: string;
|
||||||
|
value: number;
|
||||||
|
threshold: number;
|
||||||
|
startTime: number; // Unix milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identity information for deduplication
|
||||||
|
export interface ResourceIdentity {
|
||||||
|
hostname?: string;
|
||||||
|
machineId?: string;
|
||||||
|
ips?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The core unified Resource type.
|
||||||
|
* This is what the frontend receives from WebSocket state.resources[].
|
||||||
|
*/
|
||||||
|
export interface Resource {
|
||||||
|
// Identity
|
||||||
|
id: string;
|
||||||
|
type: ResourceType;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
|
||||||
|
// Platform/Source
|
||||||
|
platformId: string;
|
||||||
|
platformType: PlatformType;
|
||||||
|
sourceType: SourceType;
|
||||||
|
|
||||||
|
// Hierarchy
|
||||||
|
parentId?: string; // Parent resource (e.g., VM -> Node)
|
||||||
|
clusterId?: string; // Cluster membership
|
||||||
|
|
||||||
|
// Universal Metrics
|
||||||
|
status: ResourceStatus;
|
||||||
|
cpu?: ResourceMetric;
|
||||||
|
memory?: ResourceMetric;
|
||||||
|
disk?: ResourceMetric;
|
||||||
|
network?: ResourceNetwork;
|
||||||
|
temperature?: number;
|
||||||
|
uptime?: number; // Seconds
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
tags?: string[];
|
||||||
|
labels?: Record<string, string>;
|
||||||
|
lastSeen: number; // Unix milliseconds
|
||||||
|
alerts?: ResourceAlert[];
|
||||||
|
|
||||||
|
// Identity for deduplication
|
||||||
|
identity?: ResourceIdentity;
|
||||||
|
|
||||||
|
// Platform-specific data (varies by type)
|
||||||
|
platformData?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper type guards
|
||||||
|
*/
|
||||||
|
export function isInfrastructure(r: Resource): boolean {
|
||||||
|
return ['node', 'host', 'docker-host', 'k8s-node', 'truenas'].includes(r.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWorkload(r: Resource): boolean {
|
||||||
|
return ['vm', 'container', 'docker-container', 'pod', 'jail'].includes(r.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStorage(r: Resource): boolean {
|
||||||
|
return ['storage', 'datastore', 'pool', 'dataset'].includes(r.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource filtering options
|
||||||
|
*/
|
||||||
|
export interface ResourceFilter {
|
||||||
|
types?: ResourceType[];
|
||||||
|
platforms?: PlatformType[];
|
||||||
|
statuses?: ResourceStatus[];
|
||||||
|
parentId?: string;
|
||||||
|
clusterId?: string;
|
||||||
|
hasAlerts?: boolean;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get effective display name
|
||||||
|
*/
|
||||||
|
export function getDisplayName(r: Resource): string {
|
||||||
|
return r.displayName || r.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get CPU percentage
|
||||||
|
*/
|
||||||
|
export function getCpuPercent(r: Resource): number {
|
||||||
|
return r.cpu?.current ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get memory percentage
|
||||||
|
*/
|
||||||
|
export function getMemoryPercent(r: Resource): number {
|
||||||
|
if (!r.memory) return 0;
|
||||||
|
if (r.memory.total && r.memory.used) {
|
||||||
|
return (r.memory.used / r.memory.total) * 100;
|
||||||
|
}
|
||||||
|
return r.memory.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get disk percentage
|
||||||
|
*/
|
||||||
|
export function getDiskPercent(r: Resource): number {
|
||||||
|
if (!r.disk) return 0;
|
||||||
|
if (r.disk.total && r.disk.used) {
|
||||||
|
return (r.disk.used / r.disk.total) * 100;
|
||||||
|
}
|
||||||
|
return r.disk.current;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue