From f6e02ae181731f84bf05123bca8d3dd2dbe00d73 Mon Sep 17 00:00:00 2001 From: Fabio Formosa Date: Fri, 8 May 2026 21:41:04 +0200 Subject: [PATCH] #103 fixed log retrieval through websocket --- .../logs-panel/logs-panel.component.spec.ts | 15 +++++--- .../logs-panel/logs-panel.component.ts | 17 +++++---- .../progress-panel.component.spec.ts | 15 +++++--- .../progress-panel.component.ts | 13 ++++--- .../simple-trigger-config.component.html | 2 +- .../simple-trigger-config.component.spec.ts | 22 +++++++++-- .../simple-trigger-config.component.ts | 38 +++++++++++-------- .../app/views/manager/manager.component.html | 27 ++++++------- .../app/views/manager/manager.component.ts | 8 ++++ 9 files changed, 100 insertions(+), 57 deletions(-) diff --git a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.spec.ts b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.spec.ts index 414e6c3..5b9d497 100644 --- a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.spec.ts +++ b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.spec.ts @@ -5,12 +5,16 @@ import {jest} from '@jest/globals'; describe('LogsPanelComponent', () => { + const ngZone = {run: jest.fn((fn: () => void) => fn())}; + + beforeEach(() => ngZone.run.mockClear()); + it('should subscribe to the selected trigger logs topic', () => { const messages = new Subject(); const logsRxWebsocketService = { watch: jest.fn(() => messages.asObservable()) }; - const component = new LogsPanelComponent(logsRxWebsocketService as any, null); + const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any); component.triggerKey = new TriggerKey('trigger-1', null); @@ -26,6 +30,7 @@ describe('LogsPanelComponent', () => { }; messages.next({body: JSON.stringify(logRecord)}); + expect(ngZone.run).toHaveBeenCalled(); expect(component.logs[0]).toEqual({ time: logRecord.date.toISOString(), type: 'INFO', @@ -43,7 +48,7 @@ describe('LogsPanelComponent', () => { .mockReturnValueOnce(firstMessages.asObservable()) .mockReturnValueOnce(secondMessages.asObservable()) }; - const component = new LogsPanelComponent(logsRxWebsocketService as any, null); + const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any); component.triggerKey = new TriggerKey('trigger-1', null); const firstSubscription = component.topicSubscription; @@ -64,7 +69,7 @@ describe('LogsPanelComponent', () => { .mockReturnValueOnce(secondMessages.asObservable()) .mockReturnValueOnce(firstMessages.asObservable()) }; - const component = new LogsPanelComponent(logsRxWebsocketService as any, null); + const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any); component.triggerKey = new TriggerKey('trigger-1', null); firstMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})}); @@ -89,7 +94,7 @@ describe('LogsPanelComponent', () => { const logsRxWebsocketService = { watch: jest.fn(() => messages.asObservable()) }; - const component = new LogsPanelComponent(logsRxWebsocketService as any, null); + const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any); component.triggerKey = new TriggerKey('trigger-1', null); messages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})}); @@ -105,7 +110,7 @@ describe('LogsPanelComponent', () => { const logsRxWebsocketService = { watch: jest.fn() }; - const component = new LogsPanelComponent(logsRxWebsocketService as any, null); + const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any); expect(() => component.ngOnDestroy()).not.toThrow(); }); diff --git a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.ts b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.ts index 6b4403d..4e46365 100644 --- a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.ts +++ b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, Input, NgZone, OnDestroy, OnInit} from '@angular/core'; import {ApiService} from '../../services'; import {LogsRxWebsocketService} from '../../services/logs.rx-websocket.service'; @@ -23,11 +23,12 @@ export class LogsPanelComponent implements OnInit, OnDestroy { private selectedTriggerKey: TriggerKey; - constructor( - private logsRxWebsocketService: LogsRxWebsocketService, - private apiService: ApiService - ) { - } + constructor( + private logsRxWebsocketService: LogsRxWebsocketService, + private apiService: ApiService, + private ngZone: NgZone + ) { + } @Input() set triggerKey(triggerKey: TriggerKey) { @@ -58,8 +59,8 @@ export class LogsPanelComponent implements OnInit, OnDestroy { this._unsubscribeFromTopic(); this.topicSubscription = this.logsRxWebsocketService.watch(`/topic/logs/${triggerKey.name}`) .pipe(map((msg: any) => JSON.parse(msg.body))) - .subscribe(this._showNewLog, (err) => { - console.log(err); + .subscribe(logRecord => this.ngZone.run(() => this._showNewLog(logRecord)), (err) => { + console.log(err); // TODO in case of 401 // this.apiService.get('/quartz-manager/session/refresh'); }); diff --git a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.spec.ts b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.spec.ts index 05b3f86..88f1788 100644 --- a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.spec.ts +++ b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.spec.ts @@ -5,13 +5,17 @@ import {jest} from '@jest/globals'; describe('ProgressPanelComponent', () => { + const ngZone = {run: jest.fn((fn: () => void) => fn())}; + + beforeEach(() => ngZone.run.mockClear()); + it('should subscribe to the selected trigger progress topic', () => { jest.useFakeTimers(); const messages = new Subject(); const progressRxWebsocketService = { watch: jest.fn(() => messages.asObservable()) }; - const component = new ProgressPanelComponent(progressRxWebsocketService as any); + const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any); component.triggerKey = new TriggerKey('trigger-1', null); @@ -20,6 +24,7 @@ describe('ProgressPanelComponent', () => { messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})}); jest.runOnlyPendingTimers(); + expect(ngZone.run).toHaveBeenCalled(); expect(component.progress.percentage).toEqual(75); expect(component.percentageStr).toEqual('75%'); expect(component.progressUpdated).toBeTruthy(); @@ -34,7 +39,7 @@ describe('ProgressPanelComponent', () => { .mockReturnValueOnce(firstMessages.asObservable()) .mockReturnValueOnce(secondMessages.asObservable()) }; - const component = new ProgressPanelComponent(progressRxWebsocketService as any); + const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any); component.triggerKey = new TriggerKey('trigger-1', null); const firstSubscription = component.topicSubscription; @@ -55,7 +60,7 @@ describe('ProgressPanelComponent', () => { .mockReturnValueOnce(secondMessages.asObservable()) .mockReturnValueOnce(firstMessages.asObservable()) }; - const component = new ProgressPanelComponent(progressRxWebsocketService as any); + const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any); component.triggerKey = new TriggerKey('trigger-1', null); firstMessages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})}); @@ -78,7 +83,7 @@ describe('ProgressPanelComponent', () => { const progressRxWebsocketService = { watch: jest.fn(() => messages.asObservable()) }; - const component = new ProgressPanelComponent(progressRxWebsocketService as any); + const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any); component.triggerKey = new TriggerKey('trigger-1', null); messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})}); @@ -94,7 +99,7 @@ describe('ProgressPanelComponent', () => { const progressRxWebsocketService = { watch: jest.fn() }; - const component = new ProgressPanelComponent(progressRxWebsocketService as any); + const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any); expect(() => component.ngOnDestroy()).not.toThrow(); }); diff --git a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.ts b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.ts index 550305a..6508f44 100644 --- a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.ts +++ b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, OnDestroy, OnInit} from '@angular/core' +import {Component, Input, NgZone, OnDestroy, OnInit} from '@angular/core' import TriggerFiredBundle from '../../model/trigger-fired-bundle.model'; import {TriggerKey} from '../../model/triggerKey.model'; import {ProgressRxWebsocketService} from '../../services/progress.rx-websocket.service'; @@ -18,9 +18,10 @@ export class ProgressPanelComponent implements OnInit, OnDestroy { topicSubscription; private selectedTriggerKey: TriggerKey; - constructor( - private progressRxWebsocketService: ProgressRxWebsocketService - ) { } + constructor( + private progressRxWebsocketService: ProgressRxWebsocketService, + private ngZone: NgZone + ) { } @Input() set triggerKey(triggerKey: TriggerKey) { @@ -44,8 +45,8 @@ export class ProgressPanelComponent implements OnInit, OnDestroy { this._unsubscribeFromTopic(); this.topicSubscription = this.progressRxWebsocketService.watch(`/topic/progress/${triggerKey.name}`) .pipe(map((msg: any) => JSON.parse(msg.body))) - .subscribe(this.onNewProgressMsg, (err) => { - console.log(err); + .subscribe(progress => this.ngZone.run(() => this.onNewProgressMsg(progress)), (err) => { + console.log(err); // TODO in case of 401 // this.apiService.get('/quartz-manager/session/refresh'); }); diff --git a/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.html b/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.html index 47c2480..5427467 100644 --- a/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.html +++ b/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.html @@ -161,7 +161,7 @@
diff --git a/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.spec.ts b/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.spec.ts index 62eeb9e..7ef463c 100644 --- a/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.spec.ts +++ b/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.spec.ts @@ -1,4 +1,4 @@ -import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {ComponentFixture, fakeAsync, flush, TestBed, waitForAsync} from '@angular/core/testing'; import {MatCardModule} from '@angular/material/card'; import {SimpleTriggerConfigComponent} from './simple-trigger-config.component'; import {ApiService, ConfigService, CONTEXT_PATH, SchedulerService} from '../../services'; @@ -124,7 +124,7 @@ describe('SimpleTriggerConfig', () => { openFormAndFillAllMandatoryFields(); }); - it('should emit an event when a new trigger is submitted', () => { + it('should emit an event when a new trigger is submitted', fakeAsync(() => { const componentDe: DebugElement = fixture.debugElement; const mockTrigger = new Trigger(); mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null); @@ -143,14 +143,18 @@ describe('SimpleTriggerConfig', () => { let actualNewTrigger; component.onNewTrigger.subscribe(simpleTrigger => actualNewTrigger = simpleTrigger); + let submittedTriggerKey: TriggerKey; + component.onTriggerSubmitting.subscribe(triggerKey => submittedTriggerKey = triggerKey); submitButton.nativeElement.click(); + expect(submittedTriggerKey).toEqual(new TriggerKey(testTriggerName, null)); + flush(); const postSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`); postSimpleTriggerReq.flush(mockTrigger); expect(actualNewTrigger).toEqual(mockTrigger); - }); + })); it('should not emit an event when an existing trigger is edited', () => { const mockTriggerKey = new TriggerKey(testTriggerName, null); @@ -247,7 +251,7 @@ describe('SimpleTriggerConfig', () => { expect(component.simpleTriggerReactiveForm.value.triggerName).toEqual(testTriggerName); - component.triggerKey = null; + component.openNewTriggerForm(); expect(component.simpleTriggerReactiveForm.value.triggerName).toBeNull(); expect(component.simpleTriggerReactiveForm.value.jobClass).toBeNull(); @@ -255,6 +259,16 @@ describe('SimpleTriggerConfig', () => { }); + it('should not emit form open changes while applying a null trigger input', () => { + let formOpenChangeEmitted = false; + component.triggerFormOpenChange.subscribe(() => formOpenChangeEmitted = true); + + component.triggerKey = null; + + expect(formOpenChangeEmitted).toBeFalsy(); + expect(component.shouldShowTheTriggerCardContent()).toBeFalsy(); + }); + it('should display the warning if there are no eligible jobs', () => { fixture.detectChanges(); const getJobsReq = httpTestingController.expectOne(`${CONTEXT_PATH}/jobs`); diff --git a/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.ts b/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.ts index f40f2bf..2c72926 100644 --- a/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.ts +++ b/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.ts @@ -42,14 +42,15 @@ export class SimpleTriggerConfigComponent implements OnInit { private jobs: Array; - enabledTriggerForm = false; - @Output() onNewTrigger = new EventEmitter(); @Output() triggerFormOpenChange = new EventEmitter(); + @Output() + onTriggerSubmitting = new EventEmitter(); + constructor( private formBuilder: UntypedFormBuilder, private schedulerService: SchedulerService, @@ -58,36 +59,33 @@ export class SimpleTriggerConfigComponent implements OnInit { } ngOnInit() { + this.simpleTriggerReactiveForm.disable(); this.fetchJobs(); } - openTriggerForm() { - this.simpleTriggerReactiveForm.enable(); - } - private fetchJobs() { this.jobService.fetchJobs().subscribe(jobs => this.jobs = jobs); } openTriggerForm() { - this.enabledTriggerForm = true; - this.triggerFormOpenChange.emit(this.enabledTriggerForm); + this.simpleTriggerReactiveForm.enable(); + this.triggerFormOpenChange.emit(true); } private closeTriggerForm() { - this.enabledTriggerForm = false; - this.triggerFormOpenChange.emit(this.enabledTriggerForm); + this.simpleTriggerReactiveForm.disable(); + this.triggerFormOpenChange.emit(false); } @Input() set triggerKey(triggerKey: TriggerKey) { if (!triggerKey) { - this.openNewTriggerForm(); + return; } else if (!this.selectedTriggerKey || this.selectedTriggerKey.name !== triggerKey.name) { this._resetTheTrigger(); this.selectedTriggerKey = {...triggerKey} as TriggerKey; this.fetchSelectedTrigger(); - this.closeTriggerForm(); + this.simpleTriggerReactiveForm.disable(); } } @@ -133,6 +131,17 @@ export class SimpleTriggerConfigComponent implements OnInit { this.schedulerService.updateSimpleTriggerConfig : this.schedulerService.saveSimpleTriggerConfig; const simpleTriggerCommand = this._fromReactiveFormToCommand(); + if (!this.trigger) { + this.onTriggerSubmitting.emit(new TriggerKey(simpleTriggerCommand.triggerName, null)); + setTimeout(() => this.submitTriggerConfig(schedulerServiceCall, simpleTriggerCommand)); + return; + } + + this.submitTriggerConfig(schedulerServiceCall, simpleTriggerCommand); + + } + + private submitTriggerConfig(schedulerServiceCall, simpleTriggerCommand: SimpleTriggerCommand) { this.triggerLoading = true; schedulerServiceCall(simpleTriggerCommand) .subscribe((retTrigger: SimpleTrigger) => { @@ -153,9 +162,8 @@ export class SimpleTriggerConfigComponent implements OnInit { } this.triggerLoading = false; }, () => { -this.triggerLoading = false -}); - + this.triggerLoading = false; + }); } private _triggerPeriodValidator(control: AbstractControl): ValidationErrors | null { diff --git a/quartz-manager-frontend/src/app/views/manager/manager.component.html b/quartz-manager-frontend/src/app/views/manager/manager.component.html index 3c75cac..3668422 100644 --- a/quartz-manager-frontend/src/app/views/manager/manager.component.html +++ b/quartz-manager-frontend/src/app/views/manager/manager.component.html @@ -18,11 +18,12 @@
- + fxFill + [triggerKey]="selectedTriggerKey" + (triggerFormOpenChange)="setNewTriggerFormOpened($event)" + (onTriggerSubmitting)="monitorTrigger($event)" + (onNewTrigger)="onNewTriggerCreated($event)"> +
@@ -30,16 +31,16 @@
- - + +
- - + +
diff --git a/quartz-manager-frontend/src/app/views/manager/manager.component.ts b/quartz-manager-frontend/src/app/views/manager/manager.component.ts index c7eda6f..5d765d4 100644 --- a/quartz-manager-frontend/src/app/views/manager/manager.component.ts +++ b/quartz-manager-frontend/src/app/views/manager/manager.component.ts @@ -20,12 +20,15 @@ export class ManagerComponent implements OnInit { selectedTriggerKey: TriggerKey; + monitoredTriggerKey: TriggerKey; + constructor() {} ngOnInit() {} onNewTriggerRequested() { this.selectedTriggerKey = null; + this.monitoredTriggerKey = null; this.newTriggerFormOpened = true; if (this.triggerConfigComponent) { this.triggerConfigComponent.openNewTriggerForm(); @@ -39,9 +42,14 @@ export class ManagerComponent implements OnInit { setSelectedTrigger(triggerKey: TriggerKey) { this.selectedTriggerKey = triggerKey; + this.monitoredTriggerKey = triggerKey; this.newTriggerFormOpened = false; } + monitorTrigger(triggerKey: TriggerKey) { + this.monitoredTriggerKey = triggerKey; + } + setNewTriggerFormOpened(opened: boolean) { this.newTriggerFormOpened = opened; }