import { Component, inject, signal, effect, OnDestroy } from '@angular/core'; import { RouterLink } from '@angular/router'; import { WebSocketService } from '../../core/services/websocket.service'; import { IContainerStats } from '../../core/types/api.types'; import { CardComponent, CardHeaderComponent, CardTitleComponent, CardDescriptionComponent, CardContentComponent, } from '../../ui/card/card.component'; interface IServiceStats { name: string; stats: IContainerStats; timestamp: number; } interface IAggregatedStats { totalCpuPercent: number; totalMemoryUsed: number; totalMemoryLimit: number; memoryPercent: number; networkRxRate: number; networkTxRate: number; serviceCount: number; topCpuServices: { name: string; value: number }[]; topMemoryServices: { name: string; value: number }[]; } @Component({ selector: 'app-resource-usage-card', standalone: true, host: { class: 'block' }, imports: [ RouterLink, CardComponent, CardHeaderComponent, CardTitleComponent, CardDescriptionComponent, CardContentComponent, ], template: `
Resource Usage Aggregated across {{ aggregated().serviceCount }} services
View All
@if (aggregated().serviceCount === 0) {
No running services
} @else {
CPU {{ aggregated().totalCpuPercent.toFixed(1) }}%
Memory {{ formatBytes(aggregated().totalMemoryUsed) }} / {{ formatBytes(aggregated().totalMemoryLimit) }}
Network
{{ formatBytesRate(aggregated().networkRxRate) }} {{ formatBytesRate(aggregated().networkTxRate) }}
@if (aggregated().topCpuServices.length > 0 || aggregated().topMemoryServices.length > 0) {
Top consumers
@for (svc of aggregated().topCpuServices.slice(0, 2); track svc.name) { {{ svc.name }}: {{ svc.value.toFixed(1) }}% CPU } @for (svc of aggregated().topMemoryServices.slice(0, 2); track svc.name) { {{ svc.name }}: {{ formatBytes(svc.value) }} }
} }
`, }) export class ResourceUsageCardComponent implements OnDestroy { private ws = inject(WebSocketService); // Store stats per service private serviceStats = new Map(); private cleanupInterval: any; // Expose Math for template Math = Math; aggregated = signal({ totalCpuPercent: 0, totalMemoryUsed: 0, totalMemoryLimit: 0, memoryPercent: 0, networkRxRate: 0, networkTxRate: 0, serviceCount: 0, topCpuServices: [], topMemoryServices: [], }); constructor() { // Listen for stats updates effect(() => { const update = this.ws.statsUpdate(); if (update) { this.serviceStats.set(update.serviceName, { name: update.serviceName, stats: update.stats, timestamp: update.timestamp, }); this.recalculateAggregated(); } }); // Clean up stale entries every 30 seconds this.cleanupInterval = setInterval(() => { const now = Date.now(); const staleThreshold = 60000; // 60 seconds let changed = false; for (const [name, entry] of this.serviceStats.entries()) { if (now - entry.timestamp > staleThreshold) { this.serviceStats.delete(name); changed = true; } } if (changed) { this.recalculateAggregated(); } }, 30000); } ngOnDestroy(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } } private recalculateAggregated(): void { const entries = Array.from(this.serviceStats.values()); if (entries.length === 0) { this.aggregated.set({ totalCpuPercent: 0, totalMemoryUsed: 0, totalMemoryLimit: 0, memoryPercent: 0, networkRxRate: 0, networkTxRate: 0, serviceCount: 0, topCpuServices: [], topMemoryServices: [], }); return; } let totalCpu = 0; let totalMemUsed = 0; let totalMemLimit = 0; let totalNetRx = 0; let totalNetTx = 0; for (const entry of entries) { totalCpu += entry.stats.cpuPercent; totalMemUsed += entry.stats.memoryUsed; totalMemLimit += entry.stats.memoryLimit; totalNetRx += entry.stats.networkRx; totalNetTx += entry.stats.networkTx; } // Sort by CPU usage for top consumers const sortedByCpu = [...entries] .filter(e => e.stats.cpuPercent > 0) .sort((a, b) => b.stats.cpuPercent - a.stats.cpuPercent) .slice(0, 3) .map(e => ({ name: e.name, value: e.stats.cpuPercent })); // Sort by memory usage for top consumers const sortedByMem = [...entries] .filter(e => e.stats.memoryUsed > 0) .sort((a, b) => b.stats.memoryUsed - a.stats.memoryUsed) .slice(0, 3) .map(e => ({ name: e.name, value: e.stats.memoryUsed })); this.aggregated.set({ totalCpuPercent: totalCpu, totalMemoryUsed: totalMemUsed, totalMemoryLimit: totalMemLimit, memoryPercent: totalMemLimit > 0 ? (totalMemUsed / totalMemLimit) * 100 : 0, networkRxRate: totalNetRx, networkTxRate: totalNetTx, serviceCount: entries.length, topCpuServices: sortedByCpu, topMemoryServices: sortedByMem, }); } formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } formatBytesRate(bytes: number): string { return this.formatBytes(bytes) + '/s'; } }