#103 fixed log retrieval through websocket

This commit is contained in:
Fabio Formosa
2026-05-08 21:41:04 +02:00
parent f6d6cd16e7
commit f6e02ae181
9 changed files with 100 additions and 57 deletions

View File

@@ -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<any>();
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();
});

View File

@@ -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';
@@ -25,7 +25,8 @@ export class LogsPanelComponent implements OnInit, OnDestroy {
constructor(
private logsRxWebsocketService: LogsRxWebsocketService,
private apiService: ApiService
private apiService: ApiService,
private ngZone: NgZone
) {
}
@@ -58,7 +59,7 @@ 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) => {
.subscribe(logRecord => this.ngZone.run(() => this._showNewLog(logRecord)), (err) => {
console.log(err);
// TODO in case of 401
// this.apiService.get('/quartz-manager/session/refresh');

View File

@@ -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<any>();
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();
});

View File

@@ -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';
@@ -19,7 +19,8 @@ export class ProgressPanelComponent implements OnInit, OnDestroy {
private selectedTriggerKey: TriggerKey;
constructor(
private progressRxWebsocketService: ProgressRxWebsocketService
private progressRxWebsocketService: ProgressRxWebsocketService,
private ngZone: NgZone
) { }
@Input()
@@ -44,7 +45,7 @@ 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) => {
.subscribe(progress => this.ngZone.run(() => this.onNewProgressMsg(progress)), (err) => {
console.log(err);
// TODO in case of 401
// this.apiService.get('/quartz-manager/session/refresh');

View File

@@ -161,7 +161,7 @@
</div>
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="!simpleTriggerReactiveForm.enabled">
<button mat-raised-button type="button"
(click)="simpleTriggerReactiveForm.enable();simpleTriggerReactiveForm.controls['triggerName'].disable();">
(click)="openTriggerForm();simpleTriggerReactiveForm.controls['triggerName'].disable();">
Reschedule
</button>
</div>

View File

@@ -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`);

View File

@@ -42,14 +42,15 @@ export class SimpleTriggerConfigComponent implements OnInit {
private jobs: Array<String>;
enabledTriggerForm = false;
@Output()
onNewTrigger = new EventEmitter<SimpleTrigger>();
@Output()
triggerFormOpenChange = new EventEmitter<boolean>();
@Output()
onTriggerSubmitting = new EventEmitter<TriggerKey>();
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 {

View File

@@ -21,6 +21,7 @@
fxFill
[triggerKey]="selectedTriggerKey"
(triggerFormOpenChange)="setNewTriggerFormOpened($event)"
(onTriggerSubmitting)="monitorTrigger($event)"
(onNewTrigger)="onNewTriggerCreated($event)">
</qrzmng-simple-trigger-config>
</div>
@@ -31,13 +32,13 @@
<div class="h-100 min-h-100 flex flex-column gap-6">
<div class="flex flex-column" >
<progress-panel class="flex-1"
[triggerKey]=selectedTriggerKey
[triggerKey]=monitoredTriggerKey
>
</progress-panel>
</div>
<div class="flex flex-column flex-1" style="max-height: calc(100% - 136px); min-height: calc(100% - 210px);">
<logs-panel class="flex flex-1 h-100 max-h-100"
[triggerKey]=selectedTriggerKey
[triggerKey]=monitoredTriggerKey
>
</logs-panel>
</div>

View File

@@ -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;
}