feat: Enhance container stats monitoring and UI integration with new ContainerStatsComponent
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Component, inject, signal, OnInit, OnDestroy, ViewChild, ElementRef, effect } from '@angular/core';
|
||||
import { Component, inject, signal, computed, OnInit, OnDestroy, ViewChild, ElementRef, effect } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ApiService } from '../../core/services/api.service';
|
||||
@@ -6,6 +6,7 @@ import { ToastService } from '../../core/services/toast.service';
|
||||
import { LogStreamService } from '../../core/services/log-stream.service';
|
||||
import { WebSocketService } from '../../core/services/websocket.service';
|
||||
import { IService, IServiceUpdate, IPlatformResource, IContainerStats, IMetric } from '../../core/types/api.types';
|
||||
import { ContainerStatsComponent } from '../../shared/components/container-stats/container-stats.component';
|
||||
import {
|
||||
CardComponent,
|
||||
CardHeaderComponent,
|
||||
@@ -51,6 +52,7 @@ import {
|
||||
DialogTitleComponent,
|
||||
DialogDescriptionComponent,
|
||||
DialogFooterComponent,
|
||||
ContainerStatsComponent,
|
||||
],
|
||||
template: `
|
||||
<div class="space-y-6">
|
||||
@@ -192,62 +194,8 @@ import {
|
||||
</ui-card>
|
||||
|
||||
<!-- Resource Stats (only shown when service is running) -->
|
||||
@if (service()!.status === 'running' && stats()) {
|
||||
<ui-card>
|
||||
<ui-card-header class="flex flex-col space-y-1.5">
|
||||
<ui-card-title>Resource Usage</ui-card-title>
|
||||
<ui-card-description>
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||
</span>
|
||||
Live stats
|
||||
</span>
|
||||
</ui-card-description>
|
||||
</ui-card-header>
|
||||
<ui-card-content>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- CPU -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">CPU</span>
|
||||
<span class="font-medium">{{ formatPercent(stats()!.cpuPercent) }}</span>
|
||||
</div>
|
||||
<div class="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div class="h-full bg-primary transition-all duration-300" [style.width.%]="stats()!.cpuPercent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Memory</span>
|
||||
<span class="font-medium">{{ formatBytes(stats()!.memoryUsed) }} / {{ formatBytes(stats()!.memoryLimit) }}</span>
|
||||
</div>
|
||||
<div class="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div class="h-full bg-primary transition-all duration-300" [style.width.%]="stats()!.memoryPercent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network RX -->
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Network In</span>
|
||||
<span class="font-medium">{{ formatBytes(stats()!.networkRx) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network TX -->
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Network Out</span>
|
||||
<span class="font-medium">{{ formatBytes(stats()!.networkTx) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ui-card-content>
|
||||
</ui-card>
|
||||
@if (service()!.status === 'running') {
|
||||
<app-container-stats [stats]="stats()" [showLiveIndicator]="true" />
|
||||
}
|
||||
|
||||
<!-- Environment Variables -->
|
||||
@@ -352,6 +300,38 @@ import {
|
||||
</ui-card-content>
|
||||
</ui-card>
|
||||
}
|
||||
|
||||
<!-- External Registry Info (shown when NOT using Onebox Registry) -->
|
||||
@if (!service()!.useOneboxRegistry) {
|
||||
<ui-card>
|
||||
<ui-card-header class="flex flex-col space-y-1.5">
|
||||
<ui-card-title>Image Source</ui-card-title>
|
||||
<ui-card-description>External container registry</ui-card-description>
|
||||
</ui-card-header>
|
||||
<ui-card-content>
|
||||
<dl class="space-y-4">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-muted-foreground">Registry</dt>
|
||||
<dd class="text-sm">{{ imageInfo().registry }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-muted-foreground">Repository</dt>
|
||||
<dd class="text-sm font-mono">{{ imageInfo().repository }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-muted-foreground">Tag</dt>
|
||||
<dd class="text-sm">
|
||||
<ui-badge variant="outline">{{ imageInfo().tag }}</ui-badge>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pt-2 border-t">
|
||||
<dt class="text-sm font-medium text-muted-foreground">Full Image Reference</dt>
|
||||
<dd class="text-sm font-mono text-muted-foreground break-all">{{ service()!.image }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</ui-card-content>
|
||||
</ui-card>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Logs Section -->
|
||||
@@ -463,6 +443,13 @@ export class ServiceDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
editForm: IServiceUpdate = {};
|
||||
|
||||
// Computed signal for parsed image information (external registries)
|
||||
imageInfo = computed(() => {
|
||||
const svc = this.service();
|
||||
if (!svc) return { registry: '', repository: '', tag: '' };
|
||||
return this.parseImageInfo(svc.image, svc.registry);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Auto-scroll when new logs arrive
|
||||
effect(() => {
|
||||
@@ -597,22 +584,41 @@ export class ServiceDetailComponent implements OnInit, OnDestroy {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
formatPercent(value: number): string {
|
||||
return value.toFixed(1) + '%';
|
||||
}
|
||||
|
||||
getEnvKeys(envVars: Record<string, string>): string[] {
|
||||
return Object.keys(envVars);
|
||||
}
|
||||
|
||||
parseImageInfo(image: string, registry?: string): { registry: string; repository: string; tag: string } {
|
||||
// Handle digest format: image@sha256:...
|
||||
let imageWithoutDigest = image;
|
||||
if (image.includes('@')) {
|
||||
imageWithoutDigest = image.split('@')[0];
|
||||
}
|
||||
|
||||
// Split tag: image:tag
|
||||
const tagIndex = imageWithoutDigest.lastIndexOf(':');
|
||||
let repository = imageWithoutDigest;
|
||||
let tag = 'latest';
|
||||
|
||||
if (tagIndex > 0 && !imageWithoutDigest.substring(tagIndex).includes('/')) {
|
||||
repository = imageWithoutDigest.substring(0, tagIndex);
|
||||
tag = imageWithoutDigest.substring(tagIndex + 1);
|
||||
}
|
||||
|
||||
// Parse registry from repository
|
||||
let parsedRegistry = registry || 'Docker Hub';
|
||||
if (!registry && repository.includes('/')) {
|
||||
const firstPart = repository.split('/')[0];
|
||||
// If first part looks like a registry (contains . or :)
|
||||
if (firstPart.includes('.') || firstPart.includes(':')) {
|
||||
parsedRegistry = firstPart;
|
||||
repository = repository.substring(firstPart.length + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return { registry: parsedRegistry, repository, tag };
|
||||
}
|
||||
|
||||
cancelEdit(): void {
|
||||
this.editMode.set(false);
|
||||
if (this.service()) {
|
||||
|
||||
Reference in New Issue
Block a user