mirror of
https://github.com/fabioformosa/quartz-manager.git
synced 2026-05-14 22:00:30 +09:00
#103 minor enhancements
This commit is contained in:
@@ -3,11 +3,15 @@
|
|||||||
<mat-card-subtitle><b>JOB LOGS</b></mat-card-subtitle>
|
<mat-card-subtitle><b>JOB LOGS</b></mat-card-subtitle>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content style="position: relative; height: calc(100% - 3em);">
|
<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 *ngIf="!selectedTriggerName && (!logs || logs.length == 0)" fxLayout="row" fxFlexAlign="center stretch" style="text-align: center">
|
||||||
<div fxFill style="height: 100%;">
|
<div fxFill style="height: 100%;">
|
||||||
<img src="assets/image/logs.svg" alt="no logs" width="320" style="margin-top: 6em;" />
|
<img src="assets/image/logs.svg" alt="no logs" width="320" style="margin-top: 6em;" />
|
||||||
</div>
|
</div>
|
||||||
</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 id="logs" style="overflow-y: auto; position: absolute; left: 0; right: 0; top: 0; bottom: 0; overflow: auto;">
|
||||||
<div
|
<div
|
||||||
*ngFor = "let log of logs; let first = first" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start" fxLayoutGap="10px">
|
*ngFor = "let log of logs; let first = first" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start" fxLayoutGap="10px">
|
||||||
|
|||||||
@@ -13,6 +13,13 @@
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.waitingLogs {
|
||||||
|
color: #6b7280;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 180px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Scrollbar CSS ===== */
|
/* ===== Scrollbar CSS ===== */
|
||||||
/* Firefox */
|
/* Firefox */
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ describe('LogsPanelComponent', () => {
|
|||||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||||
|
|
||||||
expect(logsRxWebsocketService.watch).toHaveBeenCalledWith('/topic/logs/trigger-1');
|
expect(logsRxWebsocketService.watch).toHaveBeenCalledWith('/topic/logs/trigger-1');
|
||||||
|
expect(component.selectedTriggerName).toEqual('trigger-1');
|
||||||
|
expect(component.isWaitingForLogs()).toBeTruthy();
|
||||||
|
|
||||||
const logRecord = {
|
const logRecord = {
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
@@ -30,6 +32,7 @@ describe('LogsPanelComponent', () => {
|
|||||||
msg: 'job completed',
|
msg: 'job completed',
|
||||||
threadName: 'worker-1'
|
threadName: 'worker-1'
|
||||||
});
|
});
|
||||||
|
expect(component.isWaitingForLogs()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should unsubscribe from the previous topic when the trigger changes', () => {
|
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');
|
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', () => {
|
it('should ignore destroy when no topic was selected', () => {
|
||||||
const logsRxWebsocketService = {
|
const logsRxWebsocketService = {
|
||||||
watch: jest.fn()
|
watch: jest.fn()
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export class LogsPanelComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
logs = new Array();
|
logs = new Array();
|
||||||
|
|
||||||
|
selectedTriggerName: string;
|
||||||
|
|
||||||
topicSubscription;
|
topicSubscription;
|
||||||
|
|
||||||
private selectedTriggerKey: TriggerKey;
|
private selectedTriggerKey: TriggerKey;
|
||||||
@@ -32,13 +34,23 @@ export class LogsPanelComponent implements OnInit, OnDestroy {
|
|||||||
if (!triggerKey || !triggerKey.name) {
|
if (!triggerKey || !triggerKey.name) {
|
||||||
this._unsubscribeFromTopic();
|
this._unsubscribeFromTopic();
|
||||||
this.selectedTriggerKey = null;
|
this.selectedTriggerKey = null;
|
||||||
|
this.selectedTriggerName = null;
|
||||||
|
this._resetLogs();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.selectedTriggerKey?.name === triggerKey.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._resetLogs();
|
||||||
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
||||||
|
this.selectedTriggerName = triggerKey.name;
|
||||||
this._subscribeToTheTopic(this.selectedTriggerKey);
|
this._subscribeToTheTopic(this.selectedTriggerKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isWaitingForLogs = (): boolean => !!this.selectedTriggerName && (!this.logs || this.logs.length === 0);
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +76,10 @@ export class LogsPanelComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _resetLogs() {
|
||||||
|
this.logs = [];
|
||||||
|
}
|
||||||
|
|
||||||
_showNewLog = (logRecord) => {
|
_showNewLog = (logRecord) => {
|
||||||
if (this.logs.length > this.MAX_LOGS) {
|
if (this.logs.length > this.MAX_LOGS) {
|
||||||
this.logs.pop();
|
this.logs.pop();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
<mat-card style="padding-bottom: 0">
|
<mat-card style="padding-bottom: 0" [ngClass]="{'progress-updated': progressUpdated}">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-subtitle><b>JOB PROGRESS</b></mat-card-subtitle>
|
<mat-card-subtitle><b>JOB PROGRESS</b></mat-card-subtitle>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
|
|||||||
@@ -31,3 +31,21 @@
|
|||||||
.fireBoxContent{
|
.fireBoxContent{
|
||||||
text-align: center;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {jest} from '@jest/globals';
|
|||||||
describe('ProgressPanelComponent', () => {
|
describe('ProgressPanelComponent', () => {
|
||||||
|
|
||||||
it('should subscribe to the selected trigger progress topic', () => {
|
it('should subscribe to the selected trigger progress topic', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
const messages = new Subject<any>();
|
const messages = new Subject<any>();
|
||||||
const progressRxWebsocketService = {
|
const progressRxWebsocketService = {
|
||||||
watch: jest.fn(() => messages.asObservable())
|
watch: jest.fn(() => messages.asObservable())
|
||||||
@@ -17,9 +18,12 @@ describe('ProgressPanelComponent', () => {
|
|||||||
expect(progressRxWebsocketService.watch).toHaveBeenCalledWith('/topic/progress/trigger-1');
|
expect(progressRxWebsocketService.watch).toHaveBeenCalledWith('/topic/progress/trigger-1');
|
||||||
|
|
||||||
messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
|
messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
|
||||||
expect(component.progress.percentage).toEqual(75);
|
expect(component.progress.percentage).toEqual(75);
|
||||||
expect(component.percentageStr).toEqual('75%');
|
expect(component.percentageStr).toEqual('75%');
|
||||||
|
expect(component.progressUpdated).toBeTruthy();
|
||||||
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should unsubscribe from the previous topic when the trigger changes', () => {
|
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');
|
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', () => {
|
it('should ignore destroy when no topic was selected', () => {
|
||||||
const progressRxWebsocketService = {
|
const progressRxWebsocketService = {
|
||||||
watch: jest.fn()
|
watch: jest.fn()
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import {map} from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class ProgressPanelComponent implements OnInit, OnDestroy {
|
export class ProgressPanelComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
progress: TriggerFiredBundle = new TriggerFiredBundle();
|
progress: TriggerFiredBundle = ProgressPanelComponent._buildEmptyProgress();
|
||||||
percentageStr: string;
|
percentageStr: string;
|
||||||
|
progressUpdated = false;
|
||||||
|
|
||||||
topicSubscription;
|
topicSubscription;
|
||||||
private selectedTriggerKey: TriggerKey;
|
private selectedTriggerKey: TriggerKey;
|
||||||
@@ -26,9 +27,15 @@ export class ProgressPanelComponent implements OnInit, OnDestroy {
|
|||||||
if (!triggerKey || !triggerKey.name) {
|
if (!triggerKey || !triggerKey.name) {
|
||||||
this._unsubscribeFromTopic();
|
this._unsubscribeFromTopic();
|
||||||
this.selectedTriggerKey = null;
|
this.selectedTriggerKey = null;
|
||||||
|
this._resetProgress();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.selectedTriggerKey?.name === triggerKey.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._resetProgress();
|
||||||
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
||||||
this._subscribeToTheTopic(this.selectedTriggerKey);
|
this._subscribeToTheTopic(this.selectedTriggerKey);
|
||||||
}
|
}
|
||||||
@@ -47,6 +54,7 @@ export class ProgressPanelComponent implements OnInit, OnDestroy {
|
|||||||
onNewProgressMsg = (receivedMsg) => {
|
onNewProgressMsg = (receivedMsg) => {
|
||||||
this.progress = receivedMsg;
|
this.progress = receivedMsg;
|
||||||
this.percentageStr = this.progress.percentage + '%';
|
this.percentageStr = this.progress.percentage + '%';
|
||||||
|
this._markProgressUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user