From d7a78c57aeb2ca6a92f7b7dc7f56a86e0afa6361 Mon Sep 17 00:00:00 2001 From: Fabio Formosa Date: Wed, 6 May 2026 23:13:44 +0200 Subject: [PATCH] #103 minor enhancements --- .../logs-panel/logs-panel.component.html | 16 +++--- .../logs-panel/logs-panel.component.scss | 17 +++++-- .../logs-panel/logs-panel.component.spec.ts | 49 +++++++++++++++++++ .../logs-panel/logs-panel.component.ts | 18 ++++++- .../progress-panel.component.html | 4 +- .../progress-panel.component.scss | 18 +++++++ .../progress-panel.component.spec.ts | 48 ++++++++++++++++++ .../progress-panel.component.ts | 41 +++++++++++++--- 8 files changed, 189 insertions(+), 22 deletions(-) diff --git a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.html b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.html index eccd084..7d8d78c 100644 --- a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.html +++ b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.html @@ -3,12 +3,16 @@ JOB LOGS -
-
- no logs -
-
-
+
+
+ no logs +
+
+
+ +
Waiting for logs from {{selectedTriggerName}}...
+
+
diff --git a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.scss b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.scss index 9529fd0..104f119 100644 --- a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.scss +++ b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.scss @@ -9,11 +9,18 @@ color: gold; } -#logs{ - font-size: 1em; -} - -/* ===== Scrollbar CSS ===== */ +#logs{ + font-size: 1em; +} + +.waitingLogs { + color: #6b7280; + height: 100%; + min-height: 180px; + text-align: center; +} + +/* ===== Scrollbar CSS ===== */ /* Firefox */ * { scrollbar-width: auto; 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 d157604..414e6c3 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 @@ -15,6 +15,8 @@ describe('LogsPanelComponent', () => { component.triggerKey = new TriggerKey('trigger-1', null); expect(logsRxWebsocketService.watch).toHaveBeenCalledWith('/topic/logs/trigger-1'); + expect(component.selectedTriggerName).toEqual('trigger-1'); + expect(component.isWaitingForLogs()).toBeTruthy(); const logRecord = { date: new Date(), @@ -30,6 +32,7 @@ describe('LogsPanelComponent', () => { msg: 'job completed', threadName: 'worker-1' }); + expect(component.isWaitingForLogs()).toBeFalsy(); }); it('should unsubscribe from the previous topic when the trigger changes', () => { @@ -52,6 +55,52 @@ describe('LogsPanelComponent', () => { expect(logsRxWebsocketService.watch).toHaveBeenCalledWith('/topic/logs/trigger-2'); }); + it('should clear logs when the trigger changes', () => { + const firstMessages = new Subject(); + const secondMessages = new Subject(); + const logsRxWebsocketService = { + watch: jest.fn() + .mockReturnValueOnce(firstMessages.asObservable()) + .mockReturnValueOnce(secondMessages.asObservable()) + .mockReturnValueOnce(firstMessages.asObservable()) + }; + const component = new LogsPanelComponent(logsRxWebsocketService as any, null); + + component.triggerKey = new TriggerKey('trigger-1', null); + firstMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})}); + expect(component.logs.length).toEqual(1); + + component.triggerKey = new TriggerKey('trigger-2', null); + expect(component.logs).toEqual([]); + expect(component.selectedTriggerName).toEqual('trigger-2'); + expect(component.isWaitingForLogs()).toBeTruthy(); + + secondMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'second log', threadName: 'worker-2'})}); + expect(component.logs.length).toEqual(1); + + component.triggerKey = new TriggerKey('trigger-1', null); + expect(component.logs).toEqual([]); + expect(component.selectedTriggerName).toEqual('trigger-1'); + expect(component.isWaitingForLogs()).toBeTruthy(); + }); + + it('should clear logs when no trigger is selected', () => { + const messages = new Subject(); + const logsRxWebsocketService = { + watch: jest.fn(() => messages.asObservable()) + }; + const component = new LogsPanelComponent(logsRxWebsocketService as any, null); + + component.triggerKey = new TriggerKey('trigger-1', null); + messages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})}); + + component.triggerKey = null; + + expect(component.logs).toEqual([]); + expect(component.selectedTriggerName).toBeNull(); + expect(component.isWaitingForLogs()).toBeFalsy(); + }); + it('should ignore destroy when no topic was selected', () => { const logsRxWebsocketService = { watch: jest.fn() 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 4e2f292..6b4403d 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 @@ -15,7 +15,9 @@ export class LogsPanelComponent implements OnInit, OnDestroy { MAX_LOGS = 30; - logs = new Array(); + logs = new Array(); + + selectedTriggerName: string; topicSubscription; @@ -32,12 +34,22 @@ export class LogsPanelComponent implements OnInit, OnDestroy { if (!triggerKey || !triggerKey.name) { this._unsubscribeFromTopic(); this.selectedTriggerKey = null; + this.selectedTriggerName = null; + this._resetLogs(); return; } + if (this.selectedTriggerKey?.name === triggerKey.name) { + return; + } + + this._resetLogs(); this.selectedTriggerKey = {...triggerKey} as TriggerKey; + this.selectedTriggerName = triggerKey.name; this._subscribeToTheTopic(this.selectedTriggerKey); } + + isWaitingForLogs = (): boolean => !!this.selectedTriggerName && (!this.logs || this.logs.length === 0); ngOnInit() { } @@ -63,6 +75,10 @@ export class LogsPanelComponent implements OnInit, OnDestroy { this.topicSubscription = null; } } + + private _resetLogs() { + this.logs = []; + } _showNewLog = (logRecord) => { if (this.logs.length > this.MAX_LOGS) { diff --git a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.html b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.html index 6816bcf..9e2c21e 100644 --- a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.html +++ b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.html @@ -6,12 +6,12 @@
--> - + JOB PROGRESS -
+
{{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 index d842cca..05b3f86 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 @@ -6,6 +6,7 @@ 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()) @@ -17,9 +18,12 @@ describe('ProgressPanelComponent', () => { 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', () => { @@ -42,6 +46,50 @@ describe('ProgressPanelComponent', () => { 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() 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 6713884..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 @@ -9,10 +9,11 @@ import {map} from 'rxjs/operators'; templateUrl: './progress-panel.component.html', styleUrls: ['./progress-panel.component.scss'] }) -export class ProgressPanelComponent implements OnInit, OnDestroy { - - progress: TriggerFiredBundle = new TriggerFiredBundle(); - percentageStr: string; +export class ProgressPanelComponent implements OnInit, OnDestroy { + + progress: TriggerFiredBundle = ProgressPanelComponent._buildEmptyProgress(); + percentageStr: string; + progressUpdated = false; topicSubscription; private selectedTriggerKey: TriggerKey; @@ -26,9 +27,15 @@ export class ProgressPanelComponent implements OnInit, OnDestroy { 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); } @@ -44,10 +51,11 @@ export class ProgressPanelComponent implements OnInit, OnDestroy { }); }; - onNewProgressMsg = (receivedMsg) => { - this.progress = receivedMsg; - this.percentageStr = this.progress.percentage + '%'; - } + onNewProgressMsg = (receivedMsg) => { + this.progress = receivedMsg; + this.percentageStr = this.progress.percentage + '%'; + this._markProgressUpdated(); + } ngOnInit() { } @@ -63,4 +71,21 @@ export class ProgressPanelComponent implements OnInit, OnDestroy { } } + 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; + } + }