// project-details.component.ts import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { CommonModule } from '@angular/common'; import { Subscription, timer, switchMap, catchError, of } from 'rxjs'; import { ProjectService, Project, DockerContainer, ProjectDetailResponse } from '../../../services/project.service'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatDividerModule } from '@angular/material/divider'; import { MatCard, MatCardContent } from '@angular/material/card'; @Component({ selector: 'app-project-details', standalone: true, imports: [ CommonModule, MatIconModule, MatButtonModule, MatProgressSpinnerModule, MatDividerModule, MatCard, MatCardContent ], templateUrl: './project-details.component.html', styleUrls: ['./project-details.component.scss'] }) export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChecked { @ViewChild('consoleScroll') private consoleContainer!: ElementRef; // Прив'язка до вікна терміналу project?: Project; config?: any; containers: DockerContainer[] = []; logs: string[] = []; // Масив для зберігання рядків логів loading = true; private pollingSub?: Subscription; // Підписка на моніторинг контейнерів private logSub?: Subscription; // Підписка на потік логів через STOMP constructor( private route: ActivatedRoute, private router: Router, private projectService: ProjectService ) {} ngOnInit(): void { const id = this.route.snapshot.paramMap.get('id'); if (id) { this.loadData(id); this.startMonitoring(id); } } // Завантаження основних даних проекту private loadData(id: string): void { this.projectService.getProjectDetails(id).subscribe({ next: (data: ProjectDetailResponse) => { this.project = data.project; this.config = data.config; this.loading = false; }, error: (err) => { console.error('Помилка завантаження проекту:', err); this.loading = false; } }); } // Опитування стану контейнерів кожні 5 секунд private startMonitoring(id: string): void { this.pollingSub = timer(0, 5000).pipe( switchMap(() => this.projectService.getProjectContainers(id).pipe( catchError(() => of([])) // У разі помилки повертаємо порожній список )) ).subscribe({ next: (data: DockerContainer[]) => { this.containers = data; // Якщо з'явилися контейнери і ми ще не підключені до логів — підключаємося if (this.containers.length > 0 && !this.logSub) { const mainContainer = this.containers.find(c => c.state === 'running') || this.containers[0]; this.connectLogs(mainContainer.id); } }, error: (err) => console.error('Помилка моніторингу:', err) }); } // Підключення до трансляції логів connectLogs(id: string): void { console.log(`Підключення до логів контейнера: ${id}`); // Очищаємо попередню підписку перед створенням нової if (this.logSub) { this.logSub.unsubscribe(); this.projectService.disconnectLogs(); } this.logs = []; // Очищаємо консоль при зміні контейнера this.logSub = this.projectService.connectToDockerLogs(id).subscribe({ next: (msg) => { this.logs.push(msg); // Обмежуємо кількість рядків у пам'яті (останні 200) if (this.logs.length > 200) this.logs.shift(); }, error: (err) => console.error('STOMP Error:', err) }); } deploy(): void { if (this.project) { this.loading = true; this.projectService.deployProject(this.project.id).subscribe({ next: () => this.loading = false, error: () => this.loading = false }); } } stop(): void { if (this.project) this.projectService.stopProject(this.project.id).subscribe(); } confirmDelete() { if (confirm("Знищити проект назавжди?")) { this.pollingSub?.unsubscribe(); // Зупиняємо оновлення інтерфейсу if (this.project) { this.projectService.deleteProject(this.project.id).subscribe({ next: () => this.router.navigate(['/main/user/projects']), error: () => this.router.navigate(['/main/user/projects']) }); } } } ngAfterViewChecked(): void { this.scrollToBottom(); } private scrollToBottom(): void { try { this.consoleContainer.nativeElement.scrollTop = this.consoleContainer.nativeElement.scrollHeight; } catch (err) {} } ngOnDestroy(): void { this.pollingSub?.unsubscribe(); this.logSub?.unsubscribe(); this.projectService.disconnectLogs(); // Закриваємо з'єднання з брокером } // Форматування для відображення кольорів у терміналі formatLogLine(line: string): string { if (!line) return ''; let formatted = line; if (line.includes('ERROR') || line.includes('Error') || line.includes('failed')) { formatted = `${line}`; } else if (line.includes('WARN') || line.includes('Warning')) { formatted = `${line}`; } else if (line.includes('INFO') || line.includes('success')) { formatted = `${line}`; } else if (line.includes('DEBUG')) { formatted = `${line}`; } return formatted; } }