Files
onebox/ui/src/app/features/dashboard/traffic-card.component.ts

163 lines
5.5 KiB
TypeScript

import { Component, inject, signal, OnInit, OnDestroy } from '@angular/core';
import { ApiService } from '../../core/services/api.service';
import { ITrafficStats } from '../../core/types/api.types';
import {
CardComponent,
CardHeaderComponent,
CardTitleComponent,
CardDescriptionComponent,
CardContentComponent,
} from '../../ui/card/card.component';
import { SkeletonComponent } from '../../ui/skeleton/skeleton.component';
@Component({
selector: 'app-traffic-card',
standalone: true,
imports: [
CardComponent,
CardHeaderComponent,
CardTitleComponent,
CardDescriptionComponent,
CardContentComponent,
SkeletonComponent,
],
template: `
<ui-card>
<ui-card-header class="flex flex-col space-y-1.5">
<ui-card-title>Traffic (Last Hour)</ui-card-title>
<ui-card-description>Request metrics from access logs</ui-card-description>
</ui-card-header>
<ui-card-content class="space-y-3">
@if (loading() && !stats()) {
<ui-skeleton class="h-4 w-32" />
<ui-skeleton class="h-4 w-24" />
<ui-skeleton class="h-4 w-28" />
} @else if (stats()) {
<div class="space-y-2">
<!-- Request count -->
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Requests</span>
<span class="text-sm font-medium">{{ formatNumber(stats()!.requestCount) }}</span>
</div>
<!-- Error rate -->
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Errors</span>
<span class="text-sm font-medium" [class.text-destructive]="stats()!.errorRate > 5">
{{ stats()!.errorCount }} ({{ stats()!.errorRate }}%)
</span>
</div>
<!-- Avg response time -->
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Avg Response</span>
<span class="text-sm font-medium" [class.text-warning]="stats()!.avgResponseTime > 500">
{{ stats()!.avgResponseTime }}ms
</span>
</div>
<!-- Requests per minute -->
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Req/min</span>
<span class="text-sm font-medium">{{ stats()!.requestsPerMinute }}</span>
</div>
<!-- Status code distribution -->
<div class="pt-2 border-t">
<div class="flex gap-1 h-2 rounded overflow-hidden bg-muted">
@if (getStatusPercent('2xx') > 0) {
<div
class="bg-success transition-all"
[style.width.%]="getStatusPercent('2xx')"
[title]="'2xx: ' + stats()!.statusCounts['2xx']">
</div>
}
@if (getStatusPercent('3xx') > 0) {
<div
class="bg-blue-500 transition-all"
[style.width.%]="getStatusPercent('3xx')"
[title]="'3xx: ' + stats()!.statusCounts['3xx']">
</div>
}
@if (getStatusPercent('4xx') > 0) {
<div
class="bg-warning transition-all"
[style.width.%]="getStatusPercent('4xx')"
[title]="'4xx: ' + stats()!.statusCounts['4xx']">
</div>
}
@if (getStatusPercent('5xx') > 0) {
<div
class="bg-destructive transition-all"
[style.width.%]="getStatusPercent('5xx')"
[title]="'5xx: ' + stats()!.statusCounts['5xx']">
</div>
}
</div>
<div class="flex justify-between mt-1 text-xs text-muted-foreground">
<span>2xx</span>
<span>3xx</span>
<span>4xx</span>
<span>5xx</span>
</div>
</div>
</div>
} @else {
<div class="text-sm text-muted-foreground">No traffic data available</div>
}
</ui-card-content>
</ui-card>
`,
})
export class TrafficCardComponent implements OnInit, OnDestroy {
private api = inject(ApiService);
stats = signal<ITrafficStats | null>(null);
loading = signal(false);
private refreshInterval: any;
ngOnInit(): void {
this.loadStats();
// Refresh every 30 seconds
this.refreshInterval = setInterval(() => this.loadStats(), 30000);
}
ngOnDestroy(): void {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
}
async loadStats(): Promise<void> {
this.loading.set(true);
try {
const response = await this.api.getTrafficStats(60);
if (response.success && response.data) {
this.stats.set(response.data);
}
} catch (err) {
console.error('Failed to load traffic stats:', err);
} finally {
this.loading.set(false);
}
}
formatNumber(num: number): string {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
getStatusPercent(status: string): number {
const s = this.stats();
if (!s || s.requestCount === 0) return 0;
const count = s.statusCounts[status] || 0;
return (count / s.requestCount) * 100;
}
}