import { Component, inject, signal, OnInit, OnDestroy, effect, ViewChild, ElementRef } from '@angular/core';
import { Location } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { ApiService } from '../../core/services/api.service';
import { ToastService } from '../../core/services/toast.service';
import { WebSocketService } from '../../core/services/websocket.service';
import { LogStreamService } from '../../core/services/log-stream.service';
import { IPlatformService, IContainerStats, TPlatformServiceType } from '../../core/types/api.types';
import { ContainerStatsComponent } from '../../shared/components/container-stats/container-stats.component';
import {
CardComponent,
CardHeaderComponent,
CardTitleComponent,
CardDescriptionComponent,
CardContentComponent,
} from '../../ui/card/card.component';
import { ButtonComponent } from '../../ui/button/button.component';
import { BadgeComponent } from '../../ui/badge/badge.component';
import { SkeletonComponent } from '../../ui/skeleton/skeleton.component';
@Component({
selector: 'app-platform-service-detail',
standalone: true,
imports: [
FormsModule,
CardComponent,
CardHeaderComponent,
CardTitleComponent,
CardDescriptionComponent,
CardContentComponent,
ButtonComponent,
BadgeComponent,
SkeletonComponent,
ContainerStatsComponent,
],
template: `
Back to Services
@if (loading() && !service()) {
} @else if (service()) {
{{ service()!.displayName }}
{{ service()!.status }}
@if (service()!.isCore) {
Core Service
}
}
@if (loading() && !service()) {
@for (_ of [1,2,3]; track $index) {
}
} @else if (service()) {
Service Details
Platform service information
- Type
- {{ service()!.type }}
- Resource Types
-
@for (type of service()!.resourceTypes; track type) {
{{ type }}
}
@if (service()!.containerId) {
- Container ID
- {{ service()!.containerId?.slice(0, 12) }}
}
@if (service()!.createdAt) {
- Created
- {{ formatDate(service()!.createdAt!) }}
}
Actions
Manage platform service state
@if (service()!.isCore) {
This is a core service managed by Onebox. It cannot be stopped manually.
} @else {
@if (service()!.status === 'stopped' || service()!.status === 'not-deployed' || service()!.status === 'failed') {
}
@if (service()!.status === 'running') {
}
}
@if (service()!.status === 'running') {
}
About {{ service()!.displayName }}
{{ getServiceDescription(service()!.type) }}
@if (service()!.status === 'running') {
Logs
@if (logStream.isStreaming()) {
Live streaming
} @else {
Container logs
}
@if (logStream.isStreaming()) {
} @else {
}
@if (logStream.state().error) {
Error: {{ logStream.state().error }}
} @else if (logStream.logs().length > 0) {
@for (line of logStream.logs(); track $index) {
{{ line }}
}
} @else if (logStream.isStreaming()) {
Waiting for logs...
} @else {
Click "Stream" to start live log streaming
}
}
}
`,
})
export class PlatformServiceDetailComponent implements OnInit, OnDestroy {
private location = inject(Location);
private route = inject(ActivatedRoute);
private router = inject(Router);
private api = inject(ApiService);
private toast = inject(ToastService);
private ws = inject(WebSocketService);
logStream = inject(LogStreamService);
@ViewChild('logContainer') logContainer!: ElementRef;
service = signal(null);
stats = signal(null);
loading = signal(false);
actionLoading = signal(false);
autoScroll = true;
private statsInterval: any;
constructor() {
// Listen for WebSocket stats updates for platform services
effect(() => {
const update = this.ws.statsUpdate();
const currentService = this.service();
// Platform services use "onebox-{type}" as service name in WebSocket
if (update && currentService && update.serviceName === `onebox-${currentService.type}`) {
this.stats.set(update.stats);
}
});
// Auto-scroll when new logs arrive
effect(() => {
const logs = this.logStream.logs();
if (logs.length > 0 && this.autoScroll && this.logContainer?.nativeElement) {
setTimeout(() => {
this.logContainer.nativeElement.scrollTop = this.logContainer.nativeElement.scrollHeight;
});
}
});
}
ngOnInit(): void {
const type = this.route.snapshot.paramMap.get('type') as TPlatformServiceType;
if (type) {
this.loadService(type);
}
}
goBack(): void {
this.location.back();
}
async loadService(type: TPlatformServiceType): Promise {
this.loading.set(true);
try {
const response = await this.api.getPlatformService(type);
if (response.success && response.data) {
this.service.set(response.data);
// Load stats if service is running
if (response.data.status === 'running') {
this.loadStats(type);
// Start polling stats every 5 seconds
this.startStatsPolling(type);
}
} else {
this.toast.error(response.error || 'Platform service not found');
this.router.navigate(['/services']);
}
} catch {
this.toast.error('Failed to load platform service');
} finally {
this.loading.set(false);
}
}
async loadStats(type: TPlatformServiceType): Promise {
try {
const response = await this.api.getPlatformServiceStats(type);
if (response.success && response.data) {
this.stats.set(response.data);
}
} catch {
// Silent fail - stats are optional
}
}
startStatsPolling(type: TPlatformServiceType): void {
// Clear existing interval if any
if (this.statsInterval) {
clearInterval(this.statsInterval);
}
// Poll every 5 seconds
this.statsInterval = setInterval(() => {
if (this.service()?.status === 'running') {
this.loadStats(type);
}
}, 5000);
}
getStatusVariant(status: string): 'success' | 'destructive' | 'warning' | 'secondary' {
switch (status) {
case 'running': return 'success';
case 'stopped':
case 'not-deployed': return 'secondary';
case 'failed': return 'destructive';
case 'starting':
case 'stopping': return 'warning';
default: return 'secondary';
}
}
formatDate(timestamp: number): string {
return new Date(timestamp).toLocaleString();
}
getServiceDescription(type: TPlatformServiceType): string {
const descriptions: Record = {
mongodb: 'MongoDB is a document-oriented NoSQL database used for high volume data storage. It stores data in flexible, JSON-like documents.',
minio: 'MinIO is a high-performance, S3-compatible object storage service. Use it to store unstructured data like photos, videos, log files, and backups.',
redis: 'Redis is an in-memory data structure store, used as a distributed cache, message broker, and key-value database.',
postgresql: 'PostgreSQL is a powerful, open-source object-relational database system with over 35 years of active development.',
rabbitmq: 'RabbitMQ is a message broker that enables applications to communicate with each other using messages through queues.',
caddy: 'Caddy is a powerful, enterprise-ready, open-source web server with automatic HTTPS. It serves as the reverse proxy for Onebox.',
};
return descriptions[type] || 'A platform service managed by Onebox.';
}
async startService(): Promise {
const type = this.service()?.type;
if (!type) return;
this.actionLoading.set(true);
try {
const response = await this.api.startPlatformService(type);
if (response.success) {
this.toast.success('Platform service started');
this.loadService(type);
} else {
this.toast.error(response.error || 'Failed to start platform service');
}
} catch {
this.toast.error('Failed to start platform service');
} finally {
this.actionLoading.set(false);
}
}
async stopService(): Promise {
const type = this.service()?.type;
if (!type) return;
this.actionLoading.set(true);
try {
const response = await this.api.stopPlatformService(type);
if (response.success) {
this.toast.success('Platform service stopped');
// Clear stats and stop polling
this.stats.set(null);
if (this.statsInterval) {
clearInterval(this.statsInterval);
this.statsInterval = null;
}
this.loadService(type);
} else {
this.toast.error(response.error || 'Failed to stop platform service');
}
} catch {
this.toast.error('Failed to stop platform service');
} finally {
this.actionLoading.set(false);
}
}
ngOnDestroy(): void {
this.logStream.disconnect();
if (this.statsInterval) {
clearInterval(this.statsInterval);
}
}
startLogStream(): void {
const type = this.service()?.type;
if (type) {
this.logStream.connectPlatform(type);
}
}
stopLogStream(): void {
this.logStream.disconnect();
}
clearLogs(): void {
this.logStream.clearLogs();
}
}