#103 minor enhancements

This commit is contained in:
Fabio Formosa
2026-05-06 23:13:44 +02:00
parent 31658416f5
commit d7a78c57ae
8 changed files with 189 additions and 22 deletions

View File

@@ -3,12 +3,16 @@
<mat-card-subtitle><b>JOB LOGS</b></mat-card-subtitle>
</mat-card-header>
<mat-card-content style="position: relative; height: calc(100% - 3em);">
<div *ngIf="!logs || logs.length == 0" fxLayout="row" fxFlexAlign="center stretch" style="text-align: center">
<div fxFill style="height: 100%;">
<img src="assets/image/logs.svg" alt="no logs" width="320" style="margin-top: 6em;" />
</div>
</div>
<div id="logs" style="overflow-y: auto; position: absolute; left: 0; right: 0; top: 0; bottom: 0; overflow: auto;">
<div *ngIf="!selectedTriggerName && (!logs || logs.length == 0)" fxLayout="row" fxFlexAlign="center stretch" style="text-align: center">
<div fxFill style="height: 100%;">
<img src="assets/image/logs.svg" alt="no logs" width="320" style="margin-top: 6em;" />
</div>
</div>
<div *ngIf="isWaitingForLogs()" class="waitingLogs" fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="12px">
<mat-spinner diameter="36"></mat-spinner>
<div>Waiting for logs from {{selectedTriggerName}}...</div>
</div>
<div id="logs" style="overflow-y: auto; position: absolute; left: 0; right: 0; top: 0; bottom: 0; overflow: auto;">
<div
*ngFor = "let log of logs; let first = first" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start" fxLayoutGap="10px">
<div fxFlex="0 1 300px">

View File

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

View File

@@ -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<any>();
const secondMessages = new Subject<any>();
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<any>();
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()

View File

@@ -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) {

View File

@@ -6,12 +6,12 @@
</div>
</div> -->
<mat-card style="padding-bottom: 0">
<mat-card style="padding-bottom: 0" [ngClass]="{'progress-updated': progressUpdated}">
<mat-card-header>
<mat-card-subtitle><b>JOB PROGRESS</b></mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div id="progressBarBox" *ngIf="progress.percentage !== -1">
<div id="progressBarBox" *ngIf="progress.percentage !== -1">
<mat-progress-bar mode="determinate" value="{{progress.percentage}}"></mat-progress-bar>
{{percentageStr}}
</div>

View File

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

View File

@@ -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<any>();
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<any>();
const secondMessages = new Subject<any>();
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<any>();
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()

View File

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