From 55f10ce36cdf71594198393b115c16e706496f2b Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 7 Dec 2025 23:07:45 +0000 Subject: [PATCH] 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. --- frontend-modern/src/hooks/useResources.ts | 315 ++++++++++++++++++++++ frontend-modern/src/types/api.ts | 4 + frontend-modern/src/types/resource.ts | 194 +++++++++++++ 3 files changed, 513 insertions(+) create mode 100644 frontend-modern/src/hooks/useResources.ts create mode 100644 frontend-modern/src/types/resource.ts diff --git a/frontend-modern/src/hooks/useResources.ts b/frontend-modern/src/hooks/useResources.ts new file mode 100644 index 0000000..0f77855 --- /dev/null +++ b/frontend-modern/src/hooks/useResources.ts @@ -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 + * {r =>
{r.name}
}
+ * + * // Get only infrastructure (nodes, hosts) + * {r =>
{r.name}
}
+ * + * // Get filtered workloads + * const vms = filtered({ types: ['vm'], statuses: ['running'] }); + * {r =>
{r.name}
}
+ * ``` + */ + +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; + + /** Infrastructure resources only (nodes, hosts, docker-hosts) */ + infra: Accessor; + + /** Workload resources only (vms, containers, docker-containers) */ + workloads: Accessor; + + /** 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>; + + /** Top consumers by CPU */ + topByCpu: (limit?: number) => Resource[]; + + /** Top consumers by memory */ + topByMemory: (limit?: number) => Resource[]; + + /** Whether resources are available */ + hasResources: Accessor; +} + +/** + * 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(() => { + return wsStore.state.resources ?? []; + }); + + // Pre-filtered memos for common use cases + const infra = createMemo(() => { + return resources().filter(isInfrastructure); + }); + + const workloads = createMemo(() => { + return resources().filter(isWorkload); + }); + + const hasResources = createMemo(() => { + return resources().length > 0; + }); + + // Status counts + const statusCounts = createMemo>(() => { + const counts: Record = { + 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 }; diff --git a/frontend-modern/src/types/api.ts b/frontend-modern/src/types/api.ts index 5f10f10..bb8685b 100644 --- a/frontend-modern/src/types/api.ts +++ b/frontend-modern/src/types/api.ts @@ -1,5 +1,7 @@ // Properly typed TypeScript interfaces for Pulse API +import type { Resource } from './resource'; + export interface State { nodes: Node[]; vms: VM[]; @@ -25,6 +27,8 @@ export interface State { recentlyResolved: ResolvedAlert[]; lastUpdate: string; temperatureMonitoringEnabled?: boolean; + // Unified resources (new data model - eventually replaces legacy arrays above) + resources?: Resource[]; } export interface RemovedDockerHost { diff --git a/frontend-modern/src/types/resource.ts b/frontend-modern/src/types/resource.ts new file mode 100644 index 0000000..59dbf8a --- /dev/null +++ b/frontend-modern/src/types/resource.ts @@ -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; + lastSeen: number; // Unix milliseconds + alerts?: ResourceAlert[]; + + // Identity for deduplication + identity?: ResourceIdentity; + + // Platform-specific data (varies by type) + platformData?: Record; +} + +/** + * 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; +}