+
{{percentageStr}}
diff --git a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.scss b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.scss
index 7aa05ec..51c57ca 100644
--- a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.scss
+++ b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.scss
@@ -31,3 +31,21 @@
.fireBoxContent{
text-align: center;
}
+
+.progress-updated {
+ animation: progressUpdatePulse 700ms ease-out;
+}
+
+@keyframes progressUpdatePulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(63, 81, 181, 0.35);
+ }
+
+ 45% {
+ box-shadow: 0 0 0 6px rgba(63, 81, 181, 0.16);
+ }
+
+ 100% {
+ box-shadow: 0 0 0 0 rgba(63, 81, 181, 0);
+ }
+}
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
new file mode 100644
index 0000000..05b3f86
--- /dev/null
+++ b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.spec.ts
@@ -0,0 +1,102 @@
+import {Subject} from 'rxjs';
+import {ProgressPanelComponent} from './progress-panel.component';
+import {TriggerKey} from '../../model/triggerKey.model';
+import {jest} from '@jest/globals';
+
+describe('ProgressPanelComponent', () => {
+
+ 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);
+
+ component.triggerKey = new TriggerKey('trigger-1', null);
+
+ expect(progressRxWebsocketService.watch).toHaveBeenCalledWith('/topic/progress/trigger-1');
+
+ messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
+ jest.runOnlyPendingTimers();
+
+ expect(component.progress.percentage).toEqual(75);
+ expect(component.percentageStr).toEqual('75%');
+ expect(component.progressUpdated).toBeTruthy();
+ jest.useRealTimers();
+ });
+
+ it('should unsubscribe from the previous topic when the trigger changes', () => {
+ const firstMessages = new Subject();
+ const secondMessages = new Subject();
+ const progressRxWebsocketService = {
+ watch: jest.fn()
+ .mockReturnValueOnce(firstMessages.asObservable())
+ .mockReturnValueOnce(secondMessages.asObservable())
+ };
+ const component = new ProgressPanelComponent(progressRxWebsocketService as any);
+
+ component.triggerKey = new TriggerKey('trigger-1', null);
+ const firstSubscription = component.topicSubscription;
+ jest.spyOn(firstSubscription, 'unsubscribe');
+
+ component.triggerKey = new TriggerKey('trigger-2', null);
+
+ expect(firstSubscription.unsubscribe).toHaveBeenCalled();
+ expect(progressRxWebsocketService.watch).toHaveBeenCalledWith('/topic/progress/trigger-2');
+ });
+
+ it('should reset progress when the trigger changes', () => {
+ const firstMessages = new Subject();
+ const secondMessages = new Subject();
+ const progressRxWebsocketService = {
+ watch: jest.fn()
+ .mockReturnValueOnce(firstMessages.asObservable())
+ .mockReturnValueOnce(secondMessages.asObservable())
+ .mockReturnValueOnce(firstMessages.asObservable())
+ };
+ const component = new ProgressPanelComponent(progressRxWebsocketService as any);
+
+ component.triggerKey = new TriggerKey('trigger-1', null);
+ firstMessages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
+ expect(component.progress.percentage).toEqual(75);
+
+ component.triggerKey = new TriggerKey('trigger-2', null);
+ expect(component.progress.percentage).toEqual(-1);
+ expect(component.percentageStr).toBeNull();
+ expect(component.progressUpdated).toBeFalsy();
+
+ secondMessages.next({body: JSON.stringify({percentage: 20, timesTriggered: 1})});
+ expect(component.progress.percentage).toEqual(20);
+
+ component.triggerKey = new TriggerKey('trigger-1', null);
+ expect(component.progress.percentage).toEqual(-1);
+ });
+
+ it('should reset progress when no trigger is selected', () => {
+ const messages = new Subject();
+ const progressRxWebsocketService = {
+ watch: jest.fn(() => messages.asObservable())
+ };
+ const component = new ProgressPanelComponent(progressRxWebsocketService as any);
+
+ component.triggerKey = new TriggerKey('trigger-1', null);
+ messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
+
+ component.triggerKey = null;
+
+ expect(component.progress.percentage).toEqual(-1);
+ expect(component.percentageStr).toBeNull();
+ expect(component.progressUpdated).toBeFalsy();
+ });
+
+ it('should ignore destroy when no topic was selected', () => {
+ const progressRxWebsocketService = {
+ watch: jest.fn()
+ };
+ const component = new ProgressPanelComponent(progressRxWebsocketService 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 0d50777..550305a 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,84 +1,91 @@
-import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'
-import {ProgressWebsocketService, QuartzManagerWebsocketMessage} from '../../services';
-
-import { Observable } from 'rxjs';
+import {Component, Input, OnDestroy, OnInit} from '@angular/core'
import TriggerFiredBundle from '../../model/trigger-fired-bundle.model';
-// import {Message} from '@stomp/stompjs';
-
-// import { Subscription } from 'rxjs/Subscription';
-// import {StompService} from '@stomp/ng2-stompjs';
-
-// import { QueueingSubject } from 'queueing-subject'
-// import websocketConnect from 'rxjs-websockets'
-// import 'rxjs/add/operator/share'
-// import {ServerSocket} from '../../services/qz.socket.service'
+import {TriggerKey} from '../../model/triggerKey.model';
+import {ProgressRxWebsocketService} from '../../services/progress.rx-websocket.service';
+import {map} from 'rxjs/operators';
@Component({
selector: 'progress-panel',
templateUrl: './progress-panel.component.html',
styleUrls: ['./progress-panel.component.scss']
})
-export class ProgressPanelComponent implements OnInit {
-
- progress: TriggerFiredBundle = new TriggerFiredBundle();
- percentageStr: string;
-
- // // Stream of messages
- // private subscription: Subscription;
- // public messages: Observable;
- // // Subscription status
- // public subscribed: boolean;
- // // Array of historic message (bodies)
- // public mq: Array = [];
+export class ProgressPanelComponent implements OnInit, OnDestroy {
+
+ progress: TriggerFiredBundle = ProgressPanelComponent._buildEmptyProgress();
+ percentageStr: string;
+ progressUpdated = false;
+ topicSubscription;
+ private selectedTriggerKey: TriggerKey;
constructor(
- private progressWebsocketService: ProgressWebsocketService,
- // private _stompService: StompService,
- // private serverSocket : ServerSocket
+ private progressRxWebsocketService: ProgressRxWebsocketService
) { }
- onNewProgressMsg = (receivedMsg: QuartzManagerWebsocketMessage) => {
- if (receivedMsg.type === 'SUCCESS') {
- const newStatus = receivedMsg.message;
- this.progress = newStatus;
- this.percentageStr = this.progress.percentage + '%';
- }
- }
+ @Input()
+ set triggerKey(triggerKey: TriggerKey) {
+ if (!triggerKey || !triggerKey.name) {
+ this._unsubscribeFromTopic();
+ this.selectedTriggerKey = null;
+ this._resetProgress();
+ return;
+ }
+
+ if (this.selectedTriggerKey?.name === triggerKey.name) {
+ return;
+ }
+
+ this._resetProgress();
+ this.selectedTriggerKey = {...triggerKey} as TriggerKey;
+ this._subscribeToTheTopic(this.selectedTriggerKey);
+ }
+
+ private _subscribeToTheTopic = (triggerKey: TriggerKey) => {
+ 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);
+ // TODO in case of 401
+ // this.apiService.get('/quartz-manager/session/refresh');
+ });
+ };
+
+ onNewProgressMsg = (receivedMsg) => {
+ this.progress = receivedMsg;
+ this.percentageStr = this.progress.percentage + '%';
+ this._markProgressUpdated();
+ }
ngOnInit() {
- const obs = this.progressWebsocketService.getObservable()
- obs.subscribe({
- 'next' : this.onNewProgressMsg,
- 'error' : (err) => {console.log(err)}
- });
-
- // this.subscribed = false;
- // this.subscribe();
-
- // this.serverSocket.connect()
- // this.socketSubscription = this.serverSocket.messages.subscribe((message: string) => {
- // console.log('received message from server: ', message)
- // })
- }
-
- // public subscribe() {
- // if (this.subscribed) {
- // return;
- // }
-
- // // Stream of messages
- // this.messages = this._stompService.subscribe('/topic/progress');
-
- // // Subscribe a function to be run on_next message
- // this.subscription = this.messages.subscribe(this.on_next);
-
- // this.subscribed = true;
- // }
-
- // public on_next = (message: Message) => {
- // this.mq.push(message.body + '\n');
- // console.log(message);
- // }
-
-}
+ }
+
+ ngOnDestroy() {
+ this._unsubscribeFromTopic();
+ }
+
+ private _unsubscribeFromTopic() {
+ if (this.topicSubscription) {
+ this.topicSubscription.unsubscribe();
+ this.topicSubscription = null;
+ }
+ }
+
+ private _resetProgress() {
+ this.progress = ProgressPanelComponent._buildEmptyProgress();
+ this.percentageStr = null;
+ this.progressUpdated = false;
+ }
+
+ private _markProgressUpdated() {
+ this.progressUpdated = false;
+ setTimeout(() => this.progressUpdated = true);
+ }
+
+ private static _buildEmptyProgress() {
+ const progress = new TriggerFiredBundle();
+ progress.percentage = -1;
+ return progress;
+ }
+
+}
diff --git a/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.spec.ts b/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.spec.ts
index 266fe11..2245bb0 100644
--- a/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.spec.ts
+++ b/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.spec.ts
@@ -13,6 +13,12 @@ import {MatDividerModule} from '@angular/material/divider';
describe('SchedulerControlComponent', () => {
+ const schedulerUrl = '/quartz-manager/scheduler';
+ const schedulerButtonSelector = '#schedulerControllerBtn';
+ const schedulerName = 'test-scheduler';
+ const schedulerId = 'test-id';
+ const stoppedStatus = 'STOPPED';
+
let component: SchedulerControlComponent;
let fixture: ComponentFixture;
@@ -38,16 +44,16 @@ describe('SchedulerControlComponent', () => {
it('should display the play button at the beginning since the scheduler is stopped', () => {
expect(component).toBeDefined();
- const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
- const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'STOPPED', []);
+ const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
+ const mockScheduler = new Scheduler(schedulerName, schedulerId, stoppedStatus, []);
getSchedulerReq.flush(mockScheduler);
expect(component.scheduler).toEqual(mockScheduler);
- expect(component.scheduler.status).toEqual('STOPPED');
+ expect(component.scheduler.status).toEqual(stoppedStatus);
fixture.detectChanges();
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
- const schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
+ const schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
expect(schedulerBtnDe).toBeTruthy();
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
@@ -56,13 +62,13 @@ describe('SchedulerControlComponent', () => {
it('should switch the button to pause when the scheduler is started', () => {
expect(component).toBeDefined();
- const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
- const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'STOPPED', []);
+ const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
+ const mockScheduler = new Scheduler(schedulerName, schedulerId, stoppedStatus, []);
getSchedulerReq.flush(mockScheduler);
fixture.detectChanges();
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
- let schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
+ let schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
expect(schedulerBtnDe).toBeTruthy();
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
expect(playIconDe).toBeTruthy();
@@ -72,7 +78,7 @@ describe('SchedulerControlComponent', () => {
startSchedulerReq.flush(null);
fixture.detectChanges();
- schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
+ schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
const pauseIconDe = schedulerBtnDe.query(By.css('.fa-pause'));
expect(pauseIconDe).toBeTruthy();
@@ -80,13 +86,13 @@ describe('SchedulerControlComponent', () => {
it('should switch the button to play when the scheduler is stopped', () => {
expect(component).toBeDefined();
- const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
- const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'RUNNING', []);
+ const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
+ const mockScheduler = new Scheduler(schedulerName, schedulerId, 'RUNNING', []);
getSchedulerReq.flush(mockScheduler);
fixture.detectChanges();
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
- let schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
+ let schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
expect(schedulerBtnDe).toBeTruthy();
const pauseIconDe = schedulerBtnDe.query(By.css('.fa-pause'));
expect(pauseIconDe).toBeTruthy();
@@ -96,7 +102,7 @@ describe('SchedulerControlComponent', () => {
startSchedulerReq.flush(null);
fixture.detectChanges();
- schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
+ schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
expect(playIconDe).toBeTruthy();
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 1f66f48..422752f 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
@@ -23,6 +23,11 @@ import {MisfireInstruction} from '../../model/misfire-instruction.model';
describe('SimpleTriggerConfig', () => {
+ const submitButtonSelector = 'form button[color="primary"]';
+ const repeatIntervalSelector = '#repeatInterval';
+ const testTriggerName = 'test-trigger';
+ const testJobName = 'TestJob';
+
let component: SimpleTriggerConfigComponent;
let fixture: ComponentFixture;
@@ -91,16 +96,16 @@ describe('SimpleTriggerConfig', () => {
fixture.detectChanges();
const getJobsReq = httpTestingController.expectOne(`${CONTEXT_PATH}/jobs`);
- getJobsReq.flush(['TestJob']);
+ getJobsReq.flush([testJobName]);
const componentDe: DebugElement = fixture.debugElement;
- const submitButton = componentDe.query(By.css('form button[color="primary"]'));
+ const submitButton = componentDe.query(By.css(submitButtonSelector));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
- setInputValue(componentDe, '#triggerName', 'test-trigger');
- expect(component.simpleTriggerReactiveForm.controls.triggerName.value).toEqual('test-trigger');
+ setInputValue(componentDe, '#triggerName', testTriggerName);
+ expect(component.simpleTriggerReactiveForm.controls.triggerName.value).toEqual(testTriggerName);
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
setMatSelectValueByIndex(componentDe, '#misfireInstruction', 0);
expect(component.simpleTriggerReactiveForm.controls.misfireInstruction.value).toEqual('MISFIRE_INSTRUCTION_FIRE_NOW');
@@ -111,7 +116,7 @@ describe('SimpleTriggerConfig', () => {
setInputValue(componentDe, '#repeatCount', '1000');
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
- setInputValue(componentDe, '#repeatInterval', '2000');
+ setInputValue(componentDe, repeatIntervalSelector, '2000');
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual(null);
}
@@ -122,18 +127,18 @@ describe('SimpleTriggerConfig', () => {
it('should emit an event when a new trigger is submitted', () => {
const componentDe: DebugElement = fixture.debugElement;
const mockTrigger = new Trigger();
- mockTrigger.triggerKeyDTO = new TriggerKey('test-trigger', null);
- mockTrigger.jobDetailDTO = {jobClassName: 'TestJob', description: null};
+ mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null);
+ mockTrigger.jobDetailDTO = {jobClassName: testJobName, description: null};
mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
openFormAndFillAllMandatoryFields();
- setInputValue(componentDe, '#repeatInterval', '2000');
+ setInputValue(componentDe, repeatIntervalSelector, '2000');
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatInterval).toEqual(2000);
setInputValue(componentDe, '#repeatCount', '100');
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatCount).toEqual(100);
- const submitButton = componentDe.query(By.css('form button[color="primary"]'));
+ const submitButton = componentDe.query(By.css(submitButtonSelector));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
let actualNewTrigger;
@@ -141,28 +146,28 @@ describe('SimpleTriggerConfig', () => {
submitButton.nativeElement.click();
- const postSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
+ 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('test-trigger', null);
+ const mockTriggerKey = new TriggerKey(testTriggerName, null);
component.triggerKey = mockTriggerKey;
fixture.detectChanges();
const mockTrigger = new SimpleTrigger();
- mockTrigger.triggerKeyDTO = new TriggerKey('test-trigger', null);
- mockTrigger.jobDetailDTO = {jobClassName: 'TestJob', description: null};
+ mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null);
+ mockTrigger.jobDetailDTO = {jobClassName: testJobName, description: null};
mockTrigger.mayFireAgain = true;
mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
- const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
+ const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
getSimpleTriggerReq.flush(mockTrigger);
component.simpleTriggerReactiveForm.setValue({
- triggerName: 'test-trigger',
- jobClass: 'TestJob',
+ triggerName: testTriggerName,
+ jobClass: testJobName,
triggerRecurrence: {
repeatInterval: 2000,
repeatCount: 100,
@@ -178,10 +183,10 @@ describe('SimpleTriggerConfig', () => {
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
- setInputValue(componentDe, '#repeatInterval', '4000');
+ setInputValue(componentDe, repeatIntervalSelector, '4000');
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatInterval).toEqual(4000);
- const submitButton = componentDe.query(By.css('form button[color="primary"]'));
+ const submitButton = componentDe.query(By.css(submitButtonSelector));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
let actualNewTrigger;
@@ -189,7 +194,7 @@ describe('SimpleTriggerConfig', () => {
submitButton.nativeElement.click();
- const putSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
+ const putSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
putSimpleTriggerReq.flush(mockTrigger);
expect(actualNewTrigger).toBeUndefined();
@@ -220,8 +225,34 @@ describe('SimpleTriggerConfig', () => {
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
- const submitButton = componentDe.query(By.css('form button[color="primary"]'));
+ const submitButton = componentDe.query(By.css(submitButtonSelector));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
+
+ expect(component.simpleTriggerReactiveForm.value.triggerName).toBeNull();
+
+ });
+
+ it('should reset the form when a new trigger is selected', () => {
+ const mockTriggerKey = new TriggerKey(testTriggerName, null);
+ component.triggerKey = mockTriggerKey;
+
+ const mockTrigger = new SimpleTrigger();
+ mockTrigger.triggerKeyDTO = mockTriggerKey;
+ mockTrigger.jobDetailDTO = {jobClassName: testJobName, description: null};
+ mockTrigger.mayFireAgain = true;
+ mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
+
+ const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
+ getSimpleTriggerReq.flush(mockTrigger);
+
+ expect(component.simpleTriggerReactiveForm.value.triggerName).toEqual(testTriggerName);
+
+ component.triggerKey = null;
+
+ expect(component.simpleTriggerReactiveForm.value.triggerName).toBeNull();
+ expect(component.simpleTriggerReactiveForm.value.jobClass).toBeNull();
+ expect(component.shouldShowTheTriggerCardContent()).toBeTruthy();
+
});
it('should display the warning if there are no eligible 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 1b643ae..99ae80f 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
@@ -34,9 +34,8 @@ export class SimpleTriggerConfigComponent implements OnInit {
scheduler: Scheduler;
- triggerLoading = true;
+ triggerLoading = false;
- private fetchedTriggers = false;
private triggerInProgress = false;
private selectedTriggerKey: TriggerKey;
@@ -48,6 +47,9 @@ export class SimpleTriggerConfigComponent implements OnInit {
@Output()
onNewTrigger = new EventEmitter();
+ @Output()
+ triggerFormOpenChange = new EventEmitter();
+
constructor(
private formBuilder: UntypedFormBuilder,
private schedulerService: SchedulerService,
@@ -65,18 +67,37 @@ export class SimpleTriggerConfigComponent implements OnInit {
openTriggerForm() {
this.enabledTriggerForm = true;
+ this.triggerFormOpenChange.emit(this.enabledTriggerForm);
}
private closeTriggerForm() {
this.enabledTriggerForm = false;
+ this.triggerFormOpenChange.emit(this.enabledTriggerForm);
}
@Input()
set triggerKey(triggerKey: TriggerKey) {
- this.selectedTriggerKey = {...triggerKey} as TriggerKey;
- this.fetchSelectedTrigger();
+ if (!triggerKey) {
+ this.openNewTriggerForm();
+ } else if (!this.selectedTriggerKey || this.selectedTriggerKey.name !== triggerKey.name) {
+ this._resetTheTrigger();
+ this.selectedTriggerKey = {...triggerKey} as TriggerKey;
+ this.fetchSelectedTrigger();
+ this.closeTriggerForm();
+ }
}
+ openNewTriggerForm() {
+ this._resetTheTrigger();
+ this.openTriggerForm();
+ }
+
+ private _resetTheTrigger() {
+ this.trigger = null;
+ this.triggerInProgress = false;
+ this.selectedTriggerKey = null;
+ this.simpleTriggerReactiveForm.reset(new SimpleTriggerReactiveForm());
+ }
fetchSelectedTrigger = () => {
this.triggerLoading = true;
@@ -94,7 +115,11 @@ export class SimpleTriggerConfigComponent implements OnInit {
existsATriggerInProgress = (): boolean => this.trigger && this.triggerInProgress;
onResetReactiveForm = () => {
- this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
+ if (this.trigger) {
+ this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
+ } else {
+ this.simpleTriggerReactiveForm.reset(new SimpleTriggerReactiveForm());
+ }
this.closeTriggerForm();
};
@@ -103,13 +128,13 @@ export class SimpleTriggerConfigComponent implements OnInit {
this.schedulerService.updateSimpleTriggerConfig : this.schedulerService.saveSimpleTriggerConfig;
const simpleTriggerCommand = this._fromReactiveFormToCommand();
+ this.triggerLoading = true;
schedulerServiceCall(simpleTriggerCommand)
.subscribe((retTrigger: SimpleTrigger) => {
this.trigger = retTrigger;
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(retTrigger));
- this.fetchedTriggers = true;
this.triggerInProgress = this.trigger.mayFireAgain;
if (schedulerServiceCall === this.schedulerService.saveSimpleTriggerConfig) {
@@ -118,8 +143,13 @@ export class SimpleTriggerConfigComponent implements OnInit {
this.closeTriggerForm();
}, error => {
- this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
- });
+ if (this.trigger) {
+ this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
+ }
+ this.triggerLoading = false;
+ }, () => {
+this.triggerLoading = false
+});
}
diff --git a/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.html b/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.html
index 88677dd..7718dfb 100644
--- a/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.html
+++ b/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.html
@@ -9,7 +9,9 @@
+ [ngClass]="{'selectedTrigger': selectedTrigger && selectedTrigger.name==triggerKey.name}"
+ (click)="selectTrigger(triggerKey)"
+ >
{{ triggerKey.name }}
diff --git a/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.ts b/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.ts
index 9e98f91..3a41163 100644
--- a/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.ts
+++ b/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.ts
@@ -81,16 +81,17 @@ export class TriggerListComponent implements OnInit {
}
onNewTriggerBtnClicked() {
- if (this.getTriggerKeyList() && this.getTriggerKeyList().length > 0) {
- this.dialog.open(UnsupportedMultipleJobsDialog)
- } else {
- this.onNewTriggerClicked.emit();
- }
+ this.onNewTriggerClicked.emit();
+ // if (this.getTriggerKeyList() && this.getTriggerKeyList().length > 0) {
+ // this.dialog.open(UnsupportedMultipleJobsDialog)
+ // } else {
+ // this.onNewTriggerClicked.emit();
+ // }
}
onNewTrigger(newTrigger: SimpleTrigger) {
this.newTriggers = [newTrigger, ...this.newTriggers];
- this.selectedTrigger = newTrigger.triggerKeyDTO;
+ this.selectTrigger(newTrigger.triggerKeyDTO);
}
}
diff --git a/quartz-manager-frontend/src/app/polyfills.ts b/quartz-manager-frontend/src/app/polyfills.ts
index 622efa0..766305d 100644
--- a/quartz-manager-frontend/src/app/polyfills.ts
+++ b/quartz-manager-frontend/src/app/polyfills.ts
@@ -14,7 +14,7 @@
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
-/***************************************************************************************************
+/** *************************************************************************************************
* BROWSER POLYFILLS
*/
@@ -51,14 +51,14 @@ import 'core-js/es7/reflect';
-/***************************************************************************************************
+/** *************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
-/***************************************************************************************************
+/** *************************************************************************************************
* APPLICATION IMPORTS
*/
@@ -68,7 +68,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
*/
// import 'intl'; // Run `npm install --save intl`.
-/***************************************************************************************************
+/** *************************************************************************************************
* MATERIAL 2
*/
import 'hammerjs/hammer';
diff --git a/quartz-manager-frontend/src/app/services/index.ts b/quartz-manager-frontend/src/app/services/index.ts
index a3e5802..81435d0 100644
--- a/quartz-manager-frontend/src/app/services/index.ts
+++ b/quartz-manager-frontend/src/app/services/index.ts
@@ -3,9 +3,8 @@ export * from './user.service';
export * from './config.service';
export * from './auth.service';
export * from './scheduler.service';
-export * from './websocket.service';
-export * from './progress.websocket.service';
-export * from './logs.websocket.service';
+export * from './progress.rx-websocket.service';
+export * from './logs.rx-websocket.service';
export * from './trigger.service'
export * from './job.service'
diff --git a/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.spec.ts b/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.spec.ts
new file mode 100644
index 0000000..64fa05e
--- /dev/null
+++ b/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.spec.ts
@@ -0,0 +1,39 @@
+import { TestBed } from '@angular/core/testing';
+
+import { LogsRxWebsocketService } from './logs.rx-websocket.service';
+import {ApiService} from './api.service';
+import {RxStomp} from '@stomp/rx-stomp';
+import {jest} from '@jest/globals';
+
+describe('LogsRxWebsocketService', () => {
+ let service: LogsRxWebsocketService;
+ let configureSpy;
+ let activateSpy;
+
+ beforeEach(() => {
+ configureSpy = jest.spyOn(RxStomp.prototype, 'configure');
+ activateSpy = jest.spyOn(RxStomp.prototype, 'activate').mockImplementation(() => undefined);
+
+ TestBed.configureTestingModule({
+ providers: [
+ {provide: ApiService, useValue: {getToken: () => 'test-token'}}
+ ]
+ });
+ service = TestBed.inject(LogsRxWebsocketService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should configure rx-stomp with the logs websocket endpoint', () => {
+ expect(configureSpy).toHaveBeenCalled();
+ expect(activateSpy).toHaveBeenCalled();
+
+ const config = configureSpy.mock.calls[configureSpy.mock.calls.length - 1][0];
+ expect(config.heartbeatIncoming).toEqual(0);
+ expect(config.heartbeatOutgoing).toEqual(20000);
+ expect(config.reconnectDelay).toEqual(200);
+ expect(config.webSocketFactory.toString()).toContain('/logs?access_token=');
+ });
+});
diff --git a/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.ts b/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.ts
new file mode 100644
index 0000000..76a07b4
--- /dev/null
+++ b/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@angular/core';
+import {RxStompService} from './rx-stomp.service';
+import {ApiService} from './api.service';
+import SockJS from 'sockjs-client';
+import {CONTEXT_PATH, getBaseUrl} from './config.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class LogsRxWebsocketService extends RxStompService {
+
+ constructor(private apiService: ApiService) {
+ super({
+ webSocketFactory: () => new SockJS(`${getBaseUrl()}${CONTEXT_PATH}/logs?access_token=${this.apiService.getToken()}`),
+ heartbeatIncoming: 0,
+ heartbeatOutgoing: 20000,
+ reconnectDelay: 200,
+ debug: (msg: string): void => {
+ console.log(new Date(), msg);
+ }
+ });
+ }
+}
diff --git a/quartz-manager-frontend/src/app/services/logs.websocket.service.ts b/quartz-manager-frontend/src/app/services/logs.websocket.service.ts
deleted file mode 100644
index 97e77dd..0000000
--- a/quartz-manager-frontend/src/app/services/logs.websocket.service.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import {Injectable} from '@angular/core';
-import {WebsocketService, ApiService, getBaseUrl, CONTEXT_PATH} from '.';
-import {SocketOption} from '../model/SocketOption.model';
-
-@Injectable()
-export class LogsWebsocketService extends WebsocketService {
-
- constructor(private apiService: ApiService) {
- super(new SocketOption(getBaseUrl() + `${CONTEXT_PATH}/logs`, '/topic/logs', apiService.getToken))
- }
-
-}
diff --git a/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.spec.ts b/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.spec.ts
new file mode 100644
index 0000000..cf32c29
--- /dev/null
+++ b/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.spec.ts
@@ -0,0 +1,39 @@
+import { TestBed } from '@angular/core/testing';
+import {RxStomp} from '@stomp/rx-stomp';
+import {jest} from '@jest/globals';
+
+import { ProgressRxWebsocketService } from './progress.rx-websocket.service';
+import {ApiService} from './api.service';
+
+describe('ProgressRxWebsocketService', () => {
+ let service: ProgressRxWebsocketService;
+ let configureSpy;
+ let activateSpy;
+
+ beforeEach(() => {
+ configureSpy = jest.spyOn(RxStomp.prototype, 'configure');
+ activateSpy = jest.spyOn(RxStomp.prototype, 'activate').mockImplementation(() => undefined);
+
+ TestBed.configureTestingModule({
+ providers: [
+ {provide: ApiService, useValue: {getToken: () => 'test-token'}}
+ ]
+ });
+ service = TestBed.inject(ProgressRxWebsocketService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should configure rx-stomp with the progress websocket endpoint', () => {
+ expect(configureSpy).toHaveBeenCalled();
+ expect(activateSpy).toHaveBeenCalled();
+
+ const config = configureSpy.mock.calls[configureSpy.mock.calls.length - 1][0];
+ expect(config.heartbeatIncoming).toEqual(0);
+ expect(config.heartbeatOutgoing).toEqual(20000);
+ expect(config.reconnectDelay).toEqual(200);
+ expect(config.webSocketFactory.toString()).toContain('/progress?access_token=');
+ });
+});
diff --git a/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.ts b/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.ts
new file mode 100644
index 0000000..647f668
--- /dev/null
+++ b/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@angular/core';
+import {RxStompService} from './rx-stomp.service';
+import {ApiService} from './api.service';
+import SockJS from 'sockjs-client';
+import {CONTEXT_PATH, getBaseUrl} from './config.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ProgressRxWebsocketService extends RxStompService {
+
+ constructor(private apiService: ApiService) {
+ super({
+ webSocketFactory: () => new SockJS(`${getBaseUrl()}${CONTEXT_PATH}/progress?access_token=${this.apiService.getToken()}`),
+ heartbeatIncoming: 0,
+ heartbeatOutgoing: 20000,
+ reconnectDelay: 200,
+ debug: (msg: string): void => {
+ console.log(new Date(), msg);
+ }
+ });
+ }
+}
diff --git a/quartz-manager-frontend/src/app/services/progress.websocket.service.ts b/quartz-manager-frontend/src/app/services/progress.websocket.service.ts
deleted file mode 100644
index 7322e6f..0000000
--- a/quartz-manager-frontend/src/app/services/progress.websocket.service.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import {Injectable} from '@angular/core';
-import {WebsocketService, ApiService, getBaseUrl, CONTEXT_PATH} from '.';
-import {SocketOption} from '../model/SocketOption.model';
-
-@Injectable()
-export class ProgressWebsocketService extends WebsocketService {
-
- constructor(private apiService: ApiService) {
- super(new SocketOption(getBaseUrl() + `${CONTEXT_PATH}/progress`, '/topic/progress', apiService.getToken))
- }
-
-}
diff --git a/quartz-manager-frontend/src/app/services/rx-stomp.service.ts b/quartz-manager-frontend/src/app/services/rx-stomp.service.ts
new file mode 100644
index 0000000..dfdd16a
--- /dev/null
+++ b/quartz-manager-frontend/src/app/services/rx-stomp.service.ts
@@ -0,0 +1,12 @@
+import {RxStomp} from '@stomp/rx-stomp';
+import {RxStompConfig} from '@stomp/rx-stomp/esm6/rx-stomp-config';
+
+export class RxStompService extends RxStomp {
+
+ constructor(rxStompConfig: RxStompConfig) {
+ super();
+ super.configure(rxStompConfig);
+ super.activate();
+ }
+
+}
diff --git a/quartz-manager-frontend/src/app/services/user.service.ts b/quartz-manager-frontend/src/app/services/user.service.ts
index ae0bf61..224953f 100644
--- a/quartz-manager-frontend/src/app/services/user.service.ts
+++ b/quartz-manager-frontend/src/app/services/user.service.ts
@@ -35,7 +35,7 @@ export class UserService {
this.currentUser = user;
this.router.initialNavigation();
}, err => {
- console.log(`error retrieving current user due to ` + JSON.stringify(err));
+ console.log('error retrieving current user due to ' + JSON.stringify(err));
const httpErrorResponse = err as HttpErrorResponse;
if (httpErrorResponse.status === 404) {
this.isAnAnonymousUser = true;
diff --git a/quartz-manager-frontend/src/app/services/websocket.service.ts b/quartz-manager-frontend/src/app/services/websocket.service.ts
deleted file mode 100644
index 599f936..0000000
--- a/quartz-manager-frontend/src/app/services/websocket.service.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import {Observable, Subscriber} from 'rxjs';
-import {SocketEndpoint} from '../model/SocketEndpoint.model'
-
-
-import Stomp from 'stompjs';
-import SockJS from 'sockjs-client';
-import {SocketOption} from '../model/SocketOption.model';
-
-interface WebsocketSubscriber {
- index: number,
- observer: Subscriber
-}
-
-export interface QuartzManagerWebsocketMessage {
- type: string;
- message: any;
- headers: any;
- self: boolean;
-}
-
-export class WebsocketService {
-
- _options: SocketOption;
-
- _socket: SocketEndpoint = new SocketEndpoint();
-
- observableStompConnection: Observable;
- subscribers: Array = [];
- subscriberIndex = 0;
-
- _messageIds: Array = [];
-
- reconnectionPromise: any;
-
- constructor(options: SocketOption) {
- this._options = options
- this.createObservableSocket();
- this.connect();
- }
-
- getOptions = () => {
- }
-
- private createObservableSocket = () => {
- this.observableStompConnection = new Observable((observer) => {
- const subscriberIndex = this.subscriberIndex++;
- this.addToSubscribers({index: subscriberIndex, observer});
- return () => this.removeFromSubscribers(subscriberIndex);
- });
- }
-
- private addToSubscribers = (subscriber) => {
- this.subscribers.push(subscriber);
- }
-
- private removeFromSubscribers = (index) => {
- this.subscribers = this.subscribers.filter(subscriber => subscriber.index !== index);
- }
-
- getObservable = () => {
- return this.observableStompConnection;
- };
-
- getMessage = function (data): QuartzManagerWebsocketMessage {
- const out: QuartzManagerWebsocketMessage = {};
- out.type = 'SUCCESS';
- out.message = JSON.parse(data.body);
- out.headers = {};
- out.headers.messageId = data.headers['message-id'];
-
- const messageIdIndex = this._messageIds.indexOf(out.headers.messageId);
- if (messageIdIndex > -1) {
- out.self = true;
- this._messageIds = this._messageIds.splice(messageIdIndex, 1);
- }
- return out;
- };
-
- _socketListener = (frame) => {
- console.log('Connected: ' + frame);
- this._socket.stomp.subscribe(
- this._options.topicName,
- data => this.subscribers.forEach(subscriber => subscriber.observer.next(this.getMessage(data)))
- );
- }
-
- _onSocketError = (errorMsg) => {
- const out: any = {};
- out.type = 'ERROR';
- out.message = errorMsg;
- this.subscribers.forEach(subscriber => subscriber.observer.error(out));
- this.scheduleReconnection();
- }
-
- scheduleReconnection = () => {
- this.reconnectionPromise = setTimeout(() => {
- console.log('Socket reconnecting... (if it fails, next attempt in ' + this._options.reconnectionTimeout + ' msec)');
- this.connect();
- }, this._options.reconnectionTimeout);
- }
-
- reconnectNow = function () {
- this._socket.stomp.disconnect();
- if (this.reconnectionPromise && this.reconnectionPromise.cancel) {
- this.reconnectionPromise.cancel();
- }
- this.connect();
- };
-
- send = (message) => {
- const id = Math.floor(Math.random() * 1000000);
- this._socket.stomp.send(this._options.brokerName, {
- priority: 9
- }, JSON.stringify({
- message: message,
- id: id
- }));
- this._messageIds.push(id);
- };
-
- connect = () => {
- const headers = {};
-
- let socketUrl = this._options.socketUrl;
- if (this._options.getAccessToken()) {
- socketUrl += `?access_token=${this._options.getAccessToken()}`;
- }
-
- this._socket.client = new SockJS(socketUrl);
- this._socket.stomp = Stomp.over(this._socket.client);
- this._socket.stomp.connect(headers, this._socketListener, this._onSocketError);
- this._socket.stomp.onclose = this.scheduleReconnection;
- }
-
-
-}
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 05b11c9..1428332 100644
--- a/quartz-manager-frontend/src/app/views/manager/manager.component.html
+++ b/quartz-manager-frontend/src/app/views/manager/manager.component.html
@@ -19,18 +19,25 @@
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 01ce9f1..20a9a7d 100644
--- a/quartz-manager-frontend/src/app/views/manager/manager.component.ts
+++ b/quartz-manager-frontend/src/app/views/manager/manager.component.ts
@@ -1,8 +1,4 @@
import {Component, OnInit, ViewChild} from '@angular/core';
-import {
- ConfigService,
- UserService
-} from '../../services';
import {SimpleTrigger} from '../../model/simple-trigger.model';
import {TriggerKey} from '../../model/triggerKey.model';
import {SimpleTriggerConfigComponent} from '../../components/simple-trigger-config';
@@ -32,15 +28,25 @@ export class ManagerComponent implements OnInit {
}
onNewTriggerRequested() {
- this.triggerConfigComponent.openTriggerForm();
+ this.selectedTriggerKey = null;
+ this.newTriggerFormOpened = true;
+ if (this.triggerConfigComponent) {
+ this.triggerConfigComponent.openNewTriggerForm();
+ }
}
onNewTriggerCreated(newTrigger: SimpleTrigger) {
this.triggerListComponent.onNewTrigger(newTrigger);
+ this.newTriggerFormOpened = false;
}
setSelectedTrigger(triggerKey: TriggerKey) {
this.selectedTriggerKey = triggerKey;
+ this.newTriggerFormOpened = false;
+ }
+
+ setNewTriggerFormOpened(opened: boolean) {
+ this.newTriggerFormOpened = opened;
}
}
diff --git a/quartz-manager-frontend/src/polyfills.ts b/quartz-manager-frontend/src/polyfills.ts
index ecc7cec..95ab387 100644
--- a/quartz-manager-frontend/src/polyfills.ts
+++ b/quartz-manager-frontend/src/polyfills.ts
@@ -14,7 +14,7 @@
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
-/***************************************************************************************************
+/** *************************************************************************************************
* BROWSER POLYFILLS
*/
@@ -50,7 +50,7 @@ import 'core-js/es6/reflect';
-/***************************************************************************************************
+/** *************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
@@ -59,7 +59,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
-/***************************************************************************************************
+/** *************************************************************************************************
* APPLICATION IMPORTS
*/
@@ -69,7 +69,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
*/
// import 'intl'; // Run `npm install --save intl`.
-/***************************************************************************************************
+/** *************************************************************************************************
* MATERIAL 2
*/
import 'hammerjs/hammer';
diff --git a/quartz-manager-frontend/src/typings.d.ts b/quartz-manager-frontend/src/typings.d.ts
index ef5c7bd..388fb92 100644
--- a/quartz-manager-frontend/src/typings.d.ts
+++ b/quartz-manager-frontend/src/typings.d.ts
@@ -1,5 +1,5 @@
/* SystemJS module definition */
-declare var module: NodeModule;
+declare let module: NodeModule;
interface NodeModule {
id: string;
}
diff --git a/quartz-manager-parent/.gitignore b/quartz-manager-parent/.gitignore
index 29e1e7f..a3232ff 100644
--- a/quartz-manager-parent/.gitignore
+++ b/quartz-manager-parent/.gitignore
@@ -4,4 +4,4 @@
.classpath
.project
.idea
-*.iml
+/**/*.iml
diff --git a/quartz-manager-parent/pom.xml b/quartz-manager-parent/pom.xml
index 491f57c..15f0ba1 100644
--- a/quartz-manager-parent/pom.xml
+++ b/quartz-manager-parent/pom.xml
@@ -51,6 +51,7 @@
1.6.7
2.5.3
3.0.1
+ 3.11.0.3922
fabioformosa
https://sonarcloud.io
@@ -139,6 +140,11 @@
maven-failsafe-plugin
${maven-failsafe-plugin.version}
+
+ org.sonarsource.scanner.maven
+ sonar-maven-plugin
+ ${sonar-maven-plugin.version}
+
org.jacoco
jacoco-maven-plugin
diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/jobs/AbstractQuartzManagerJob.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/jobs/AbstractQuartzManagerJob.java
index 609ee8d..20d6c90 100644
--- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/jobs/AbstractQuartzManagerJob.java
+++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/jobs/AbstractQuartzManagerJob.java
@@ -38,11 +38,13 @@ public abstract class AbstractQuartzManagerJob implements Job {
LogRecord logMsg = doIt(jobExecutionContext);
log.info(logMsg.getMessage());
+ String triggerName = jobExecutionContext.getTrigger().getKey().getName();
+
logMsg.setThreadName(Thread.currentThread().getName());
- webSocketLogsNotifier.send(logMsg);
+ webSocketLogsNotifier.send(triggerName, logMsg);
TriggerFiredBundleDTO triggerFiredBundleDTO = WebSocketProgressNotifier.buildTriggerFiredBundle(jobExecutionContext);
- webSocketProgressNotifier.send(triggerFiredBundleDTO);
+ webSocketProgressNotifier.send(triggerName, triggerFiredBundleDTO);
}
}
diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketLogsNotifier.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketLogsNotifier.java
index 9ced563..9cdbfb7 100644
--- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketLogsNotifier.java
+++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketLogsNotifier.java
@@ -14,7 +14,7 @@ public class WebSocketLogsNotifier implements WebhookSender {
private SimpMessageSendingOperations messagingTemplate;
@Override
- public void send(LogRecord logRecord) {
- messagingTemplate.convertAndSend(TOPIC_LOGS, logRecord);
+ public void send(String triggerName, LogRecord logRecord) {
+ messagingTemplate.convertAndSend(TOPIC_LOGS + "/" + triggerName, logRecord);
}
}
diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketProgressNotifier.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketProgressNotifier.java
index 7eeba6d..d616bf9 100644
--- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketProgressNotifier.java
+++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketProgressNotifier.java
@@ -20,8 +20,8 @@ public class WebSocketProgressNotifier implements WebhookSender {
- void send(T message);
+ void send(String triggerName, T message);
}
diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/jobs/SampleJobTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/jobs/SampleJobTest.java
index f56bd0d..2ffde76 100644
--- a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/jobs/SampleJobTest.java
+++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/jobs/SampleJobTest.java
@@ -4,16 +4,26 @@ import it.fabioformosa.quartzmanager.api.dto.TriggerFiredBundleDTO;
import it.fabioformosa.quartzmanager.api.jobs.entities.LogRecord;
import it.fabioformosa.quartzmanager.api.websockets.WebhookSender;
import org.assertj.core.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-import org.quartz.*;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.quartz.JobBuilder;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobKey;
+import org.quartz.ScheduleBuilder;
+import org.quartz.SimpleScheduleBuilder;
+import org.quartz.Trigger;
+import org.quartz.TriggerBuilder;
-import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+@ExtendWith(MockitoExtension.class)
class SampleJobTest {
@InjectMocks
@@ -24,14 +34,16 @@ class SampleJobTest {
@Mock
private WebhookSender webSocketLogsNotifier;
- @BeforeEach
- void setUp() {
- MockitoAnnotations.openMocks(this);
- }
+ @Captor
+ private ArgumentCaptor logRecordCaptor;
+
+ @Captor
+ private ArgumentCaptor triggerFiredBundleDTOCaptor;
@Test
void givenASampleJob_whenTheJobIsExecuted_thenTheWebhookSendersAreCalled() {
JobExecutionContext jobExecutionContext = Mockito.mock(JobExecutionContext.class);
+ String triggerName = "test-trigger";
ScheduleBuilder schedulerBuilder = SimpleScheduleBuilder.simpleSchedule()
.withRepeatCount(5)
@@ -40,6 +52,7 @@ class SampleJobTest {
.newJob(SampleJob.class).withIdentity(JobKey.jobKey("test-job"))
.build();
Trigger trigger = TriggerBuilder.newTrigger()
+ .withIdentity(triggerName)
.forJob(jobDetail)
.withSchedule(schedulerBuilder)
.build();
@@ -47,24 +60,23 @@ class SampleJobTest {
Mockito.when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail);
sampleJob.execute(jobExecutionContext);
- Mockito.verify(webSocketLogsNotifier).send(argThat(actualLogRecord -> {
- Assertions.assertThat(actualLogRecord.getMessage()).isEqualTo("Hello!");
- Assertions.assertThat(actualLogRecord.getType()).isEqualTo(LogRecord.LogType.INFO);
- Assertions.assertThat(actualLogRecord.getDate()).isNotNull();
- Assertions.assertThat(actualLogRecord.getThreadName()).isNotNull();
- return true;
- }));
- Mockito.verify(webSocketProgressNotifier).send(argThat(triggerFiredBundleDTO -> {
- Assertions.assertThat(triggerFiredBundleDTO.getJobKey()).isEqualTo("test-job");
- Assertions.assertThat(triggerFiredBundleDTO.getRepeatCount()).isEqualTo(6);
- Assertions.assertThat(triggerFiredBundleDTO.getJobClass()).isEqualTo(SampleJob.class.getName());
- Assertions.assertThat(triggerFiredBundleDTO.getTimesTriggered()).isZero();
- Assertions.assertThat(triggerFiredBundleDTO.getNextFireTime()).isNull();
- Assertions.assertThat(triggerFiredBundleDTO.getPercentage()).isZero();
- Assertions.assertThat(triggerFiredBundleDTO.getFinalFireTime()).isNotNull();
- Assertions.assertThat(triggerFiredBundleDTO.getPreviousFireTime()).isNull();
- return true;
- }));
+ Mockito.verify(webSocketLogsNotifier).send(eq(triggerName), logRecordCaptor.capture());
+ LogRecord actualLogRecord = logRecordCaptor.getValue();
+ Assertions.assertThat(actualLogRecord.getMessage()).isEqualTo("Hello!");
+ Assertions.assertThat(actualLogRecord.getType()).isEqualTo(LogRecord.LogType.INFO);
+ Assertions.assertThat(actualLogRecord.getDate()).isNotNull();
+ Assertions.assertThat(actualLogRecord.getThreadName()).isNotNull();
+
+ Mockito.verify(webSocketProgressNotifier).send(eq(triggerName), triggerFiredBundleDTOCaptor.capture());
+ TriggerFiredBundleDTO triggerFiredBundleDTO = triggerFiredBundleDTOCaptor.getValue();
+ Assertions.assertThat(triggerFiredBundleDTO.getJobKey()).isEqualTo("test-job");
+ Assertions.assertThat(triggerFiredBundleDTO.getRepeatCount()).isEqualTo(6);
+ Assertions.assertThat(triggerFiredBundleDTO.getJobClass()).isEqualTo(SampleJob.class.getName());
+ Assertions.assertThat(triggerFiredBundleDTO.getTimesTriggered()).isZero();
+ Assertions.assertThat(triggerFiredBundleDTO.getNextFireTime()).isNull();
+ Assertions.assertThat(triggerFiredBundleDTO.getPercentage()).isZero();
+ Assertions.assertThat(triggerFiredBundleDTO.getFinalFireTime()).isNotNull();
+ Assertions.assertThat(triggerFiredBundleDTO.getPreviousFireTime()).isNull();
}
@Test
diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceIntegrationTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceIntegrationTest.java
index f26e829..74c633d 100644
--- a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceIntegrationTest.java
+++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceIntegrationTest.java
@@ -18,13 +18,17 @@ import java.util.Date;
@SpringBootTest
class SimpleTriggerServiceIntegrationTest {
+ private static final String SAMPLE_JOB_CLASS = "it.fabioformosa.quartzmanager.api.jobs.SampleJob";
+ private static final String SAMPLE_EXTRA_JOB_CLASS = "it.fabioformosa.samplepackage.SampleExtraJob";
+ private static final String FIRST_TRIGGER_SUFFIX = "A";
+ private static final String SECOND_TRIGGER_SUFFIX = "B";
+
@Autowired
private SimpleTriggerService simpleTriggerService;
@Test
void givenASimpleTriggerCommandDTOWithAllData_whenANewSimpleTriggerIsScheduled_thenShouldGetATriggertDTO() throws SchedulerException, ClassNotFoundException {
String simpleTriggerTestName = "simpleTriggerWithAllData";
- String jobClass = "it.fabioformosa.quartzmanager.api.jobs.SampleJob";
Date startDate = new Date();
Date endDate = DateUtils.addHoursToNow(5);
int repeatCount = 3;
@@ -41,7 +45,7 @@ class SimpleTriggerServiceIntegrationTest {
.repeatCount(repeatCount)
.repeatInterval(repeatInterval)
.misfireInstruction(misfireInstructionFireNow)
- .jobClass(jobClass)
+ .jobClass(SAMPLE_JOB_CLASS)
.build())
.build();
SimpleTriggerDTO simpleTriggerDTO = simpleTriggerService.scheduleSimpleTrigger(simpleTriggerCommand);
@@ -61,12 +65,11 @@ class SimpleTriggerServiceIntegrationTest {
@Test
void givenASimpleTriggerCommandDTOWithMissingOptionalField_whenANewSimpleTriggerIsScheduled_thenShouldGetATriggertDTO() throws SchedulerException, ClassNotFoundException {
String simpleTriggerTestName = "simpleTriggerWithoutOptionalData";
- String jobClass = "it.fabioformosa.quartzmanager.api.jobs.SampleJob";
SimpleTriggerCommandDTO simpleTriggerCommand = SimpleTriggerCommandDTO.builder()
.triggerName(simpleTriggerTestName)
.simpleTriggerInputDTO(SimpleTriggerInputDTO.builder()
- .jobClass(jobClass)
+ .jobClass(SAMPLE_JOB_CLASS)
.build())
.build();
SimpleTriggerDTO simpleTriggerDTO = simpleTriggerService.scheduleSimpleTrigger(simpleTriggerCommand);
@@ -81,4 +84,49 @@ class SimpleTriggerServiceIntegrationTest {
Assertions.assertThat(simpleTriggerDTO.getRepeatInterval()).isZero();
}
+ @Test
+ void givenTwoSimpleTriggerCommandDTOsForTheSameJob_whenScheduled_thenShouldCreateTwoTriggers() throws SchedulerException, ClassNotFoundException {
+ String triggerNamePrefix = "sameJobTrigger" + System.nanoTime();
+
+ SimpleTriggerDTO firstTrigger = simpleTriggerService.scheduleSimpleTrigger(
+ buildSimpleTriggerCommand(triggerNamePrefix + FIRST_TRIGGER_SUFFIX, SAMPLE_JOB_CLASS)
+ );
+ SimpleTriggerDTO secondTrigger = simpleTriggerService.scheduleSimpleTrigger(
+ buildSimpleTriggerCommand(triggerNamePrefix + SECOND_TRIGGER_SUFFIX, SAMPLE_JOB_CLASS)
+ );
+
+ Assertions.assertThat(firstTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + FIRST_TRIGGER_SUFFIX);
+ Assertions.assertThat(secondTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + SECOND_TRIGGER_SUFFIX);
+ Assertions.assertThat(firstTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_JOB_CLASS);
+ Assertions.assertThat(secondTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_JOB_CLASS);
+ Assertions.assertThat(firstTrigger.getJobKeyDTO().getName()).isNotEqualTo(secondTrigger.getJobKeyDTO().getName());
+ }
+
+ @Test
+ void givenTwoSimpleTriggerCommandDTOsForDifferentJobs_whenScheduled_thenShouldCreateTwoTriggers() throws SchedulerException, ClassNotFoundException {
+ String triggerNamePrefix = "differentJobTrigger" + System.nanoTime();
+
+ SimpleTriggerDTO firstTrigger = simpleTriggerService.scheduleSimpleTrigger(
+ buildSimpleTriggerCommand(triggerNamePrefix + FIRST_TRIGGER_SUFFIX, SAMPLE_JOB_CLASS)
+ );
+ SimpleTriggerDTO secondTrigger = simpleTriggerService.scheduleSimpleTrigger(
+ buildSimpleTriggerCommand(triggerNamePrefix + SECOND_TRIGGER_SUFFIX, SAMPLE_EXTRA_JOB_CLASS)
+ );
+
+ Assertions.assertThat(firstTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + FIRST_TRIGGER_SUFFIX);
+ Assertions.assertThat(secondTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + SECOND_TRIGGER_SUFFIX);
+ Assertions.assertThat(firstTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_JOB_CLASS);
+ Assertions.assertThat(secondTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_EXTRA_JOB_CLASS);
+ }
+
+ private static SimpleTriggerCommandDTO buildSimpleTriggerCommand(String triggerName, String jobClass) {
+ return SimpleTriggerCommandDTO.builder()
+ .triggerName(triggerName)
+ .simpleTriggerInputDTO(SimpleTriggerInputDTO.builder()
+ .jobClass(jobClass)
+ .startDate(DateUtils.addHoursToNow(1))
+ .build())
+ .build();
+ }
+
}
diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketNotifierTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketNotifierTest.java
new file mode 100644
index 0000000..94b38ad
--- /dev/null
+++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketNotifierTest.java
@@ -0,0 +1,48 @@
+package it.fabioformosa.quartzmanager.api.websockets;
+
+import it.fabioformosa.quartzmanager.api.dto.TriggerFiredBundleDTO;
+import it.fabioformosa.quartzmanager.api.jobs.entities.LogRecord;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.springframework.messaging.simp.SimpMessageSendingOperations;
+
+import static org.mockito.MockitoAnnotations.openMocks;
+
+class WebSocketNotifierTest {
+
+ @InjectMocks
+ private WebSocketLogsNotifier webSocketLogsNotifier;
+
+ @InjectMocks
+ private WebSocketProgressNotifier webSocketProgressNotifier;
+
+ @Mock
+ private SimpMessageSendingOperations messagingTemplate;
+
+ @BeforeEach
+ void setUp() {
+ openMocks(this);
+ }
+
+ @Test
+ void givenATriggerName_whenALogIsSent_thenShouldSendItToTheTriggerLogsTopic() {
+ LogRecord logRecord = new LogRecord(LogRecord.LogType.INFO, "Hello!");
+
+ webSocketLogsNotifier.send("trigger-1", logRecord);
+
+ Mockito.verify(messagingTemplate).convertAndSend("/topic/logs/trigger-1", logRecord);
+ }
+
+ @Test
+ void givenATriggerName_whenProgressIsSent_thenShouldSendItToTheTriggerProgressTopic() {
+ TriggerFiredBundleDTO triggerFiredBundleDTO = new TriggerFiredBundleDTO();
+
+ webSocketProgressNotifier.send("trigger-1", triggerFiredBundleDTO);
+
+ Mockito.verify(messagingTemplate).convertAndSend("/topic/progress/trigger-1", triggerFiredBundleDTO);
+ }
+
+}