adjast wss + docker logs

This commit is contained in:
2026-05-11 13:10:00 +03:00
parent 724b74b3c8
commit 4d574d5fdf
9 changed files with 327 additions and 67 deletions
@@ -1,13 +1,14 @@
// project-details.component.ts
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { CommonModule } from '@angular/common';
import { Subscription, timer, switchMap } from 'rxjs';
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';
import { MatCard, MatCardContent } from '@angular/material/card';
@Component({
selector: 'app-project-details',
@@ -25,16 +26,16 @@ import {MatCard, MatCardContent} from '@angular/material/card';
styleUrls: ['./project-details.component.scss']
})
export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChecked {
@ViewChild('consoleScroll') private consoleContainer!: ElementRef; // Привязка к #consoleScroll
@ViewChild('consoleScroll') private consoleContainer!: ElementRef; // Прив'язка до вікна терміналу
project?: Project;
config?: any;
containers: DockerContainer[] = [];
logs: string[] = [];
logs: string[] = []; // Масив для зберігання рядків логів
loading = true;
private pollingSub?: Subscription;
private logSub?: Subscription;
private pollingSub?: Subscription; // Підписка на моніторинг контейнерів
private logSub?: Subscription; // Підписка на потік логів через STOMP
constructor(
private route: ActivatedRoute,
@@ -50,46 +51,90 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
}
}
// Завантаження основних даних проекту
private loadData(id: string): void {
this.projectService.getProjectDetails(id).subscribe({
next: (data: ProjectDetailResponse) => {
this.project = data.project;
this.config = data.config;
this.loading = false;
this.connectLogs(id);
},
error: (err) => {
console.error('Ошибка загрузки данных проекта:', err);
console.error('Помилка завантаження проекту:', err);
this.loading = false;
}
});
}
// Опитування стану контейнерів кожні 5 секунд
private startMonitoring(id: string): void {
// Опрос состояния контейнеров каждые 5 секунд[cite: 5, 6]
this.pollingSub = timer(0, 5000).pipe(
switchMap(() => this.projectService.getProjectContainers(id))
switchMap(() => this.projectService.getProjectContainers(id).pipe(
catchError(() => of([])) // У разі помилки повертаємо порожній список
))
).subscribe({
next: (data) => this.containers = data,
error: (err) => console.error('Ошибка мониторинга контейнеров:', err)
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)
});
}
private connectLogs(id: string): void {
// Підключення до трансляції логів
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),
error: (err) => console.error('WS Error:', err)
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.projectService.deployProject(this.project.id).subscribe();
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();
}
@@ -103,44 +148,23 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
ngOnDestroy(): void {
this.pollingSub?.unsubscribe();
this.logSub?.unsubscribe();
this.projectService.disconnectLogs(); // Закриваємо з'єднання з брокером
}
confirmDelete() {
if (confirm("Уничтожить проект навсегда?")) {
// 1. Сначала «ослепляем» компонент, чтобы он перестал слать запросы
this.pollingSub?.unsubscribe(); // Твой метод, который делает subscription.unsubscribe()
// 2. Только теперь отправляем команду на ликвидацию
if (this.project) this.projectService.deleteProject(this.project.id).subscribe({
next: () => {
console.log("Проект стерт, уходим на дашборд");
this.router.navigate(['/main/user/projects']);
},
error: (err) => {
console.error("Ошибка при удалении, но проект мог уже исчезнуть", err);
// Всё равно уходим, так как оставаться на странице 404 нет смысла
this.router.navigate(['/main/user/projects']);
}
});
}
}
// Метод для окрашивания логов в терминале
// Форматування для відображення кольорів у терміналі
formatLogLine(line: string): string {
if (!line) return '';
// Окрашиваем ключевые слова для лучшей читаемости
let formatted = line;
if (line.includes('ERROR') || line.includes('Error') || line.includes('failed')) {
formatted = `<span style="color: #ff5f56; font-weight: bold;">${line}</span>`;
} else if (line.includes('WARN') || line.includes('Warning') || line.includes('Conflict')) {
} else if (line.includes('WARN') || line.includes('Warning')) {
formatted = `<span style="color: #ffbd2e;">${line}</span>`;
} else if (line.includes('INFO') || line.includes('success') || line.includes('Started')) {
} else if (line.includes('INFO') || line.includes('success')) {
formatted = `<span style="color: #27c93f;">${line}</span>`;
} else if (line.includes('DEBUG')) {
formatted = `<span style="color: #ae81ff;">${line}</span>`;
}
return formatted;
}
}
@@ -1,8 +1,10 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import {Observable, Subject} from 'rxjs';
import { GlobalConstants } from '../global-constants';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import SockJS from 'sockjs-client';
import { Client, IMessage } from '@stomp/stompjs';
// Унифицируем название интерфейса для списка контейнеров
export interface DockerContainer {
@@ -44,6 +46,7 @@ export interface ProjectDetailResponse {
export class ProjectService {
// Используем baseUrl, как определено в начале класса
private baseUrl = GlobalConstants.API_URL;
private stompClient: Client | null = null;
constructor(private http: HttpClient) {}
@@ -80,12 +83,40 @@ export class ProjectService {
}
// WebSocket для трансляции логов в реальном времени[cite: 3, 5]
public connectToDockerLogs(projectId: string): WebSocketSubject<any> {
const socketUrl = this.baseUrl.replace('http', 'ws') + '/ws-jambotron/' + projectId;
return webSocket({
url: socketUrl,
deserializer: (msg) => msg.data
public connectToDockerLogs(containerId: string): Observable<string> {
const logSubject = new Subject<string>();
// Твой эндпоинт из WebSocketConfig.java
const socket = new SockJS(`${this.baseUrl}/ws-jambotron`);
this.stompClient = new Client({
webSocketFactory: () => socket,
debug: (str) => console.log(str),
onConnect: () => {
// 1. Подписываемся на логи конкретного контейнера
this.stompClient?.subscribe(`/topic/logs/${containerId}`, (message: IMessage) => {
logSubject.next(message.body);
});
// 2. Отправляем сигнал бэкенду начать стриминг (согласно LogController)
this.stompClient?.publish({
destination: '/app/start-logs',
body: containerId
});
},
onStompError: (frame) => {
logSubject.error('Broker error: ' + frame.headers['message']);
}
});
this.stompClient.activate();
return logSubject.asObservable();
}
public disconnectLogs(): void {
if (this.stompClient) {
this.stompClient.deactivate();
}
}
// ИСПРАВЛЕНО: возвращаем DockerContainer и используем правильный baseUrl[cite: 5]