Merge branch 'feature/#101_angular15_update' into develop

This commit is contained in:
Fabio Formosa
2026-05-09 00:11:12 +02:00
28 changed files with 4969 additions and 17172 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/.project
.idea
*.iml
.DS_Store

View File

@@ -26,7 +26,8 @@
"src/favicon.ico"
],
"styles": [
"src/styles.css"
"src/styles.css",
"node_modules/roboto-fontface/css/roboto/roboto-fontface.css"
],
"scripts": []
},

File diff suppressed because it is too large Load Diff

View File

@@ -14,20 +14,20 @@
},
"private": true,
"dependencies": {
"@angular-material-components/datetime-picker": "8.0.0",
"@angular-material-components/moment-adapter": "8.0.0",
"@angular/animations": "14.2.12",
"@angular/cdk": "^14.0.1",
"@angular/common": "14.2.12",
"@angular/compiler": "14.2.12",
"@angular/core": "14.2.12",
"@angular/flex-layout": "14.0.0-beta.41",
"@angular/forms": "14.2.12",
"@angular/material": "^14.0.1",
"@angular/platform-browser": "14.2.12",
"@angular/platform-browser-dynamic": "14.2.12",
"@angular/platform-server": "14.2.12",
"@angular/router": "14.2.12",
"@angular-material-components/datetime-picker": "15.0.0",
"@angular-material-components/moment-adapter": "15.0.0",
"@angular/animations": "15.2.10",
"@angular/cdk": "15.0.1",
"@angular/common": "15.2.10",
"@angular/compiler": "15.2.10",
"@angular/core": "15.2.10",
"@angular/flex-layout": "15.0.0-beta.42",
"@angular/forms": "15.2.10",
"@angular/material": "15.0.1",
"@angular/platform-browser": "15.2.10",
"@angular/platform-browser-dynamic": "15.2.10",
"@angular/platform-server": "15.2.10",
"@angular/router": "15.2.10",
"@auth0/angular-jwt": "5.1.0",
"@fortawesome/fontawesome": "^1.1.4",
"@fortawesome/fontawesome-free-regular": "^5.0.8",
@@ -37,6 +37,7 @@
"hammerjs": "2.0.8",
"moment": "^2.29.1",
"net": "^1.0.2",
"roboto-fontface": "^0.10.0",
"rxjs": "6.5.5",
"sockjs-client": "^1.1.1",
"stompjs": "^2.3.3",
@@ -44,16 +45,16 @@
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "14.2.10",
"@angular-devkit/core": "^14.2.10",
"@angular-eslint/builder": "14.4.0",
"@angular-eslint/eslint-plugin": "14.4.0",
"@angular-eslint/eslint-plugin-template": "14.4.0",
"@angular-eslint/schematics": "14.4.0",
"@angular-eslint/template-parser": "14.4.0",
"@angular/cli": "14.2.10",
"@angular/compiler-cli": "14.2.12",
"@angular/language-service": "14.2.12",
"@angular-devkit/build-angular": "^15.2.10",
"@angular-devkit/core": "^15.2.10",
"@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "^15.2.10",
"@angular/compiler-cli": "15.2.10",
"@angular/language-service": "15.2.10",
"@types/hammerjs": "2.0.34",
"@types/jasmine": "2.5.54",
"@types/jasminewd2": "2.0.3",
@@ -62,7 +63,7 @@
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/eslint-plugin-tslint": "^5.46.0",
"@typescript-eslint/parser": "5.43.0",
"codelyzer": "~6.0.2",
"codelyzer": "6.0.2",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
@@ -80,9 +81,9 @@
"karma-jasmine-html-reporter": "~2.0.0",
"prettier": "^2.8.1",
"prettier-eslint": "^15.0.1",
"protractor": "~7.0.0",
"protractor": "^7.0.0",
"ts-node": "10.9.1",
"typescript": "4.6.4"
"typescript": "4.9.5"
},
"jest": {
"preset": "jest-preset-angular",

View File

@@ -1,6 +1,6 @@
<div fxLayout="column" fxLayoutAlign="space-between stretch" fxFill>
<app-header fxFlex="0 0 auto"></app-header>
<div class="content" fxFlex="100" fxFill>
<div class="content flex h-100">
<router-outlet></router-outlet>
</div>
<app-footer fxFlex="0 0 auto"></app-footer>

View File

@@ -7,4 +7,5 @@
.content {
padding: 20px;
max-height: calc(100vh - 169px);
}

View File

@@ -21,6 +21,7 @@ import {MatDatepickerModule} from '@angular/material/datepicker';
import {MatSelectModule} from '@angular/material/select';
import {MatListModule} from '@angular/material/list';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatDialogModule} from '@angular/material/dialog';
import {MatNativeDateModule} from '@angular/material/core';
import { NgxMatTimepickerModule, NgxMatDatetimePickerModule} from '@angular-material-components/datetime-picker';
@@ -108,6 +109,7 @@ export function jwtOptionsFactory(apiService: ApiService) {
deps: [ApiService]
}
}),
MatDialogModule,
MatMenuModule,
MatTooltipModule,
MatButtonModule,

View File

@@ -1,8 +1,8 @@
<mat-toolbar id="footer" style="color: rgba(255, 255, 255, 0.541176);" fxLayout="row" fxLayoutAlign="center center">
<a mat-icon-button href="https://github.com/fabioformosa/quartz-manager">
<img src="assets/image/github.png"/>
&nbsp; Quartz Manager
</a>
<!-- Hand crafted with love by &nbsp;-->
<!-- <a href="https://github.com/fabioformosa" style="color: rgba(255, 255, 255, 0.870588);">Fabio Formosa</a>-->
<a href="https://github.com/fabioformosa/quartz-manager" class="flex flex-row align-items-center" style="gap: 6px">
<div class="flex"><img src="assets/image/github.png"/></div>
<div class="font-size-14 font-weight-500 display-block line-height-100">Quartz Manager</div>
</a>
<!-- Hand crafted with love by &nbsp;-->
<!-- <a href="https://github.com/fabioformosa" style="color: rgba(255, 255, 255, 0.870588);">Fabio Formosa</a>-->
</mat-toolbar>

View File

@@ -1,39 +1,65 @@
<mat-card fxFlex="1 1 auto">
<mat-card-header fxLayout="row" fxLayoutAlign="space-between none" style="padding-right: 1em;">
<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="!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;">
<mat-card class="flex flex-1 max-h-100">
<mat-card-header class="pb-16">
<mat-card-subtitle ><b>JOB LOGS</b></mat-card-subtitle>
</mat-card-header>
<mat-card-content class="flex flex-1 overflow-y-auto">
<div class="flex-1">
<div *ngIf="!selectedTriggerName && (!logs || logs.length == 0)" fxFill class="h-100" style="text-align: center;">
<img
src="assets/image/logs.svg"
alt="no logs"
width="320"
style="margin-top: 6em" />
</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" fxFill style="height: 100%">
<div
*ngFor = "let log of logs; let first = first" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start" fxLayoutGap="10px">
<div fxFlex="0 1 300px">
<span [ngClass]="{'animate__animated animate__zoomIn zoomIn firstLog': first}"> [{{log.time|date:'medium'}}]</span>
</div>
<div fxFlex="1 1 16px">
<span [ngClass]="{'animated zoomIn firstLog': first}">
<i class = "fas" [ngClass]="{'fa-check-circle green': log.type == 'INFO',
'fa-exclamation-triangle yellow': log.type == 'WARN',
'fa-times-circle red': log.type == 'ERROR'}"></i>
</span>
</div>
<div fxFlex="0 1 250px">
<span [ngClass]="{'animate__animated animate__zoomIn zoomIn firstLog': first}">
{{log.threadName}}
</span>
</div>
<div fxFlex="1 1">
<span [ngClass]="{'animate__animated animate__zoomIn zoomIn firstLog': first}"> {{log.msg}}</span>
</div>
*ngFor="let log of logs; let first = first"
fxLayout="row"
fxLayout.xs="column"
fxLayoutAlign="start"
fxLayoutGap="10px">
<div style="flex: 1; max-width: 300px">
<span
[ngClass]="{
'animate__animated animate__zoomIn zoomIn firstLog': first
}">
[{{ log.time | date : 'medium' }}]</span
>
</div>
<div style="flex: 1; max-width: 16px">
<span [ngClass]="{ 'animated zoomIn firstLog': first }">
<i
class="fas"
[ngClass]="{
'fa-check-circle green': log.type == 'INFO',
'fa-exclamation-triangle yellow': log.type == 'WARN',
'fa-times-circle red': log.type == 'ERROR'
}"></i>
</span>
</div>
<div style="flex: 1; max-width: 250px">
<span
[ngClass]="{
'animate__animated animate__zoomIn zoomIn firstLog': first
}">
{{ log.threadName }}
</span>
</div>
<div style="flex: 1">
<span
[ngClass]="{
'animate__animated animate__zoomIn zoomIn firstLog': first
}">
{{ log.msg }}</span
>
</div>
</div>
</div>
</mat-card-content>
</div>
</mat-card-content>
</mat-card>

View File

@@ -1,3 +1,9 @@
:host {
flex: 1;
display: flex;
flex-direction: column;
}
.red{
color: red;
}

View File

@@ -5,12 +5,16 @@ import {jest} from '@jest/globals';
describe('LogsPanelComponent', () => {
const ngZone = {run: jest.fn((fn: () => void) => fn())};
beforeEach(() => ngZone.run.mockClear());
it('should subscribe to the selected trigger logs topic', () => {
const messages = new Subject<any>();
const logsRxWebsocketService = {
watch: jest.fn(() => messages.asObservable())
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
@@ -26,6 +30,7 @@ describe('LogsPanelComponent', () => {
};
messages.next({body: JSON.stringify(logRecord)});
expect(ngZone.run).toHaveBeenCalled();
expect(component.logs[0]).toEqual({
time: logRecord.date.toISOString(),
type: 'INFO',
@@ -43,7 +48,7 @@ describe('LogsPanelComponent', () => {
.mockReturnValueOnce(firstMessages.asObservable())
.mockReturnValueOnce(secondMessages.asObservable())
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
const firstSubscription = component.topicSubscription;
@@ -64,7 +69,7 @@ describe('LogsPanelComponent', () => {
.mockReturnValueOnce(secondMessages.asObservable())
.mockReturnValueOnce(firstMessages.asObservable())
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
firstMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})});
@@ -89,7 +94,7 @@ describe('LogsPanelComponent', () => {
const logsRxWebsocketService = {
watch: jest.fn(() => messages.asObservable())
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
messages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})});
@@ -105,7 +110,7 @@ describe('LogsPanelComponent', () => {
const logsRxWebsocketService = {
watch: jest.fn()
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
expect(() => component.ngOnDestroy()).not.toThrow();
});

View File

@@ -1,4 +1,4 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {Component, Input, NgZone, OnDestroy, OnInit} from '@angular/core';
import {ApiService} from '../../services';
import {LogsRxWebsocketService} from '../../services/logs.rx-websocket.service';
@@ -23,11 +23,12 @@ export class LogsPanelComponent implements OnInit, OnDestroy {
private selectedTriggerKey: TriggerKey;
constructor(
private logsRxWebsocketService: LogsRxWebsocketService,
private apiService: ApiService
) {
}
constructor(
private logsRxWebsocketService: LogsRxWebsocketService,
private apiService: ApiService,
private ngZone: NgZone
) {
}
@Input()
set triggerKey(triggerKey: TriggerKey) {
@@ -58,8 +59,8 @@ export class LogsPanelComponent implements OnInit, OnDestroy {
this._unsubscribeFromTopic();
this.topicSubscription = this.logsRxWebsocketService.watch(`/topic/logs/${triggerKey.name}`)
.pipe(map((msg: any) => JSON.parse(msg.body)))
.subscribe(this._showNewLog, (err) => {
console.log(err);
.subscribe(logRecord => this.ngZone.run(() => this._showNewLog(logRecord)), (err) => {
console.log(err);
// TODO in case of 401
// this.apiService.get('/quartz-manager/session/refresh');
});

View File

@@ -6,12 +6,12 @@
</div>
</div> -->
<mat-card style="padding-bottom: 0" [ngClass]="{'progress-updated': progressUpdated}">
<mat-card-header>
<mat-card style="padding-bottom: 0" [ngClass]="{'progress-updated': progressUpdated}">
<mat-card-header style="padding-bottom: 16px;">
<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

@@ -5,13 +5,17 @@ import {jest} from '@jest/globals';
describe('ProgressPanelComponent', () => {
const ngZone = {run: jest.fn((fn: () => void) => fn())};
beforeEach(() => ngZone.run.mockClear());
it('should subscribe to the selected trigger progress topic', () => {
jest.useFakeTimers();
const messages = new Subject<any>();
const progressRxWebsocketService = {
watch: jest.fn(() => messages.asObservable())
};
const component = new ProgressPanelComponent(progressRxWebsocketService as any);
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
@@ -20,6 +24,7 @@ describe('ProgressPanelComponent', () => {
messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
jest.runOnlyPendingTimers();
expect(ngZone.run).toHaveBeenCalled();
expect(component.progress.percentage).toEqual(75);
expect(component.percentageStr).toEqual('75%');
expect(component.progressUpdated).toBeTruthy();
@@ -34,7 +39,7 @@ describe('ProgressPanelComponent', () => {
.mockReturnValueOnce(firstMessages.asObservable())
.mockReturnValueOnce(secondMessages.asObservable())
};
const component = new ProgressPanelComponent(progressRxWebsocketService as any);
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
const firstSubscription = component.topicSubscription;
@@ -55,7 +60,7 @@ describe('ProgressPanelComponent', () => {
.mockReturnValueOnce(secondMessages.asObservable())
.mockReturnValueOnce(firstMessages.asObservable())
};
const component = new ProgressPanelComponent(progressRxWebsocketService as any);
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
firstMessages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
@@ -78,7 +83,7 @@ describe('ProgressPanelComponent', () => {
const progressRxWebsocketService = {
watch: jest.fn(() => messages.asObservable())
};
const component = new ProgressPanelComponent(progressRxWebsocketService as any);
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
@@ -94,7 +99,7 @@ describe('ProgressPanelComponent', () => {
const progressRxWebsocketService = {
watch: jest.fn()
};
const component = new ProgressPanelComponent(progressRxWebsocketService as any);
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
expect(() => component.ngOnDestroy()).not.toThrow();
});

View File

@@ -1,4 +1,4 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core'
import {Component, Input, NgZone, OnDestroy, OnInit} from '@angular/core'
import TriggerFiredBundle from '../../model/trigger-fired-bundle.model';
import {TriggerKey} from '../../model/triggerKey.model';
import {ProgressRxWebsocketService} from '../../services/progress.rx-websocket.service';
@@ -18,9 +18,10 @@ export class ProgressPanelComponent implements OnInit, OnDestroy {
topicSubscription;
private selectedTriggerKey: TriggerKey;
constructor(
private progressRxWebsocketService: ProgressRxWebsocketService
) { }
constructor(
private progressRxWebsocketService: ProgressRxWebsocketService,
private ngZone: NgZone
) { }
@Input()
set triggerKey(triggerKey: TriggerKey) {
@@ -44,8 +45,8 @@ export class ProgressPanelComponent implements OnInit, OnDestroy {
this._unsubscribeFromTopic();
this.topicSubscription = this.progressRxWebsocketService.watch(`/topic/progress/${triggerKey.name}`)
.pipe(map((msg: any) => JSON.parse(msg.body)))
.subscribe(this.onNewProgressMsg, (err) => {
console.log(err);
.subscribe(progress => this.ngZone.run(() => this.onNewProgressMsg(progress)), (err) => {
console.log(err);
// TODO in case of 401
// this.apiService.get('/quartz-manager/session/refresh');
});

View File

@@ -13,13 +13,13 @@
<mat-card-subtitle style="margin: auto;"><b>SCHEDULER</b></mat-card-subtitle>
</div>
<mat-divider [vertical]="true"></mat-divider>
<div fxLayout="column">
<div fxLayout="column" class="justify-space-between">
<div><label>Name</label></div>
<div><span id="scheduler-name">{{scheduler?.name}}</span></div>
<div><span id="scheduler-name">{{scheduler?.name}}</span></div>
</div>
<div fxLayout="column">
<div fxLayout="column" class="justify-space-between">
<div><label>Instance ID</label></div>
<div><span id="scheduler-instance">{{scheduler?.instanceId}}</span></div>
<div><span id="scheduler-instance">{{scheduler?.instanceId}}</span></div>
</div>
</div>
</mat-card-content>

View File

@@ -11,7 +11,12 @@ label{
font-size: smaller;
}
#scheduler-name{
text-transform: capitalize;
font-size: larger;
}
#scheduler-name{
text-transform: capitalize;
font-size: 14px;
}
#scheduler-instance {
text-transform: capitalize;
font-size: 14px;
}

View File

@@ -1,11 +1,11 @@
<mat-card fxFlex="1 1 auto">
<mat-card-header>
<mat-card-header style="padding-bottom: 16px;">
<mat-card-subtitle><b>TRIGGER DETAILS</b></mat-card-subtitle>
</mat-card-header>
<mat-divider></mat-divider>
<mat-card-content *ngIf="shouldShowTheTriggerCardContent()" style="position: relative; height: 100%">
<div fxLayout="column" style="overflow-y: auto; position: absolute; left: 0; right: 0; top: 0; bottom: 0;
overflow: auto;height: calc(100% - 3em); padding-top: 1em;">
overflow: auto;padding: 1em;">
<mat-card id="noEligibleJobsAlert" *ngIf="jobs?.length === 0" style="background-color: #ff6385">
<mat-card-content>
<i class="fas fa-exclamation-circle" style="color: #fff"></i>&nbsp;<strong>WARNING</strong>
@@ -14,15 +14,13 @@
app prop <i>quartz-manager.jobClassPackages</i> with the correct java package </p>
</mat-card-content>
</mat-card>
<form name="triggerConfigForm" fxFlex="1 1 100%"
<form name="triggerConfigForm" class="trigger-config-form" fxFlex="1 1 100%"
[formGroup]="simpleTriggerReactiveForm" (ngSubmit)="onSubmitTriggerConfig()">
<div>
<mat-form-field
[appearance]="enabledTriggerForm && !trigger ? 'standard': 'none'"
class="full-size-input">
<mat-label>Trigger Name</mat-label>
<input id="triggerName"
[readonly]="!enabledTriggerForm || trigger"
matInput placeholder="name of the trigger (unique)"
formControlName="triggerName" name="triggerName">
<mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerName.errors?.required">
@@ -33,12 +31,11 @@
<div>
<mat-form-field
[appearance]="enabledTriggerForm ? 'standard': 'none'"
class="full-size-input"
>
<mat-label>Job Class</mat-label>
<mat-select id="jobClass" name="jobClass" formControlName="jobClass" [disabled]="!enabledTriggerForm">
<mat-option *ngFor="let job of jobs" [value]="job" style="font-size: 0.8em">
<mat-select id="jobClass" name="jobClass" formControlName="jobClass">
<mat-option *ngFor="let job of jobs" [value]="job" class="font-13">
{{job}}
</mat-option>
</mat-select>
@@ -50,23 +47,21 @@
<div>
<mat-form-field
[appearance]="enabledTriggerForm ? 'standard': 'none'"
class="full-size-input"
>
<mat-label>Misfire Instruction</mat-label>
<mat-select id="misfireInstruction" name="misfireInstruction" formControlName="misfireInstruction"
[disabled]="!enabledTriggerForm" style="font-size: 0.8em">
<mat-option value="MISFIRE_INSTRUCTION_FIRE_NOW">FIRE NOW</mat-option>
<mat-option value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT">RESCHEDULE NOW WITH
<mat-select id="misfireInstruction" name="misfireInstruction" formControlName="misfireInstruction">
<mat-option class="font-13" value="MISFIRE_INSTRUCTION_FIRE_NOW">FIRE NOW</mat-option>
<mat-option class="font-13" value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT">RESCHEDULE NOW WITH
EXISTING REPEAT COUNT
</mat-option>
<mat-option value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT">RESCHEDULE NOW WITH
<mat-option class="font-13" value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT">RESCHEDULE NOW WITH
REMAINING REPEAT COUNT
</mat-option>
<mat-option value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT">RESCHEDULE NEXT WITH
<mat-option class="font-13" value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT">RESCHEDULE NEXT WITH
REMAINING COUNT
</mat-option>
<mat-option value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT">RESCHEDULE NEXT WITH EXISTING
<mat-option class="font-13" value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT">RESCHEDULE NEXT WITH EXISTING
COUNT
</mat-option>
</mat-select>
@@ -82,12 +77,10 @@
<div formGroupName="triggerPeriod">
<div>
<mat-form-field
[appearance]="enabledTriggerForm ? 'standard': 'none'"
class="full-size-input"
>
<mat-label>Start Date (optional)</mat-label>
<input id="startDate"
[readonly]="!enabledTriggerForm"
matInput
[ngxMatDatetimePicker]="startDatePicker" placeholder="Choose a start date"
formControlName="startDate" name="startDate">
@@ -99,12 +92,10 @@
<div>
<mat-form-field
[appearance]="enabledTriggerForm ? 'standard': 'none'"
class="full-size-input"
>
<mat-label>End Date (optional)</mat-label>
<input id="endDate"
[readonly]="!enabledTriggerForm"
matInput
[ngxMatDatetimePicker]="endDatePicker" placeholder="Choose a end date"
formControlName="endDate" name="endDate"
@@ -122,12 +113,10 @@
<div formGroupName="triggerRecurrence">
<div>
<mat-form-field
[appearance]="enabledTriggerForm ? 'standard': 'none'"
class="full-size-input"
>
<mat-label>Repeat Interval [in mills]</mat-label>
<input id="repeatInterval"
[readonly]="!enabledTriggerForm"
matInput placeholder="Repeat Interval [in mills]" type="number"
formControlName="repeatInterval" name="repeatInterval"
>
@@ -138,12 +127,10 @@
</div>
<div>
<mat-form-field
[appearance]="enabledTriggerForm ? 'standard': 'none'"
class="full-size-input"
>
<mat-label>Repeat Count</mat-label>
<input id="repeatCount"
[readonly]="!enabledTriggerForm"
matInput placeholder="Repeat Count (-1 REPEAT INDEFINITELY)" type="number"
formControlName="repeatCount" name="repeatCount"
>
@@ -158,26 +145,23 @@
<br/>
<div fxLayout="row" fxFlexAlign="space-evenly center" style="padding-bottom: 1em;">
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="enabledTriggerForm">
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="simpleTriggerReactiveForm.enabled">
<button mat-raised-button
type="button"
*ngIf="enabledTriggerForm"
(click)="onResetReactiveForm()">
Cancel
</button>
</div>
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="enabledTriggerForm">
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="simpleTriggerReactiveForm.enabled">
<button mat-raised-button
type="submit" color="primary"
[disabled]="simpleTriggerReactiveForm.invalid"
*ngIf="enabledTriggerForm">
[disabled]="simpleTriggerReactiveForm.invalid">
Submit
</button>
</div>
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="!enabledTriggerForm">
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="!simpleTriggerReactiveForm.enabled">
<button mat-raised-button type="button"
*ngIf="!enabledTriggerForm"
(click)="enabledTriggerForm = true">
(click)="openTriggerForm();simpleTriggerReactiveForm.controls['triggerName'].disable();">
Reschedule
</button>
</div>

View File

@@ -5,6 +5,22 @@
.full-size-input{
width: 100%;
}
:host ::ng-deep .trigger-config-form .mat-mdc-form-field {
font-size: 13px;
}
:host ::ng-deep .trigger-config-form .mat-mdc-select-value,
:host ::ng-deep .trigger-config-form .mat-mdc-select-value-text,
:host ::ng-deep .trigger-config-form .mat-mdc-input-element,
:host ::ng-deep .trigger-config-form .mdc-floating-label {
font-size: 13px;
}
:host ::ng-deep .trigger-config-form .mat-mdc-select-trigger {
min-height: 20px;
}
/* ===== Scrollbar CSS ===== */
/* Firefox */
* {

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {ComponentFixture, fakeAsync, flush, TestBed, waitForAsync} from '@angular/core/testing';
import {MatCardModule} from '@angular/material/card';
import {SimpleTriggerConfigComponent} from './simple-trigger-config.component';
import {ApiService, ConfigService, CONTEXT_PATH, SchedulerService} from '../../services';
@@ -86,7 +86,7 @@ describe('SimpleTriggerConfig', () => {
const dropdownDe = componentDe.query(By.css(dropdownSelector));
dropdownDe.nativeElement.click();
fixture.detectChanges();
const matOptionDe = componentDe.query(By.css('.mat-select-panel')).queryAll(By.css('.mat-option'));
const matOptionDe = componentDe.query(By.css('.mat-mdc-select-panel')).queryAll(By.css('.mat-mdc-option'));
matOptionDe[index].nativeElement.click();
fixture.detectChanges();
}
@@ -124,7 +124,7 @@ describe('SimpleTriggerConfig', () => {
openFormAndFillAllMandatoryFields();
});
it('should emit an event when a new trigger is submitted', () => {
it('should emit an event when a new trigger is submitted', fakeAsync(() => {
const componentDe: DebugElement = fixture.debugElement;
const mockTrigger = new Trigger();
mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null);
@@ -143,14 +143,18 @@ describe('SimpleTriggerConfig', () => {
let actualNewTrigger;
component.onNewTrigger.subscribe(simpleTrigger => actualNewTrigger = simpleTrigger);
let submittedTriggerKey: TriggerKey;
component.onTriggerSubmitting.subscribe(triggerKey => submittedTriggerKey = triggerKey);
submitButton.nativeElement.click();
expect(submittedTriggerKey).toEqual(new TriggerKey(testTriggerName, null));
flush();
const postSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
postSimpleTriggerReq.flush(mockTrigger);
expect(actualNewTrigger).toEqual(mockTrigger);
});
}));
it('should not emit an event when an existing trigger is edited', () => {
const mockTriggerKey = new TriggerKey(testTriggerName, null);
@@ -207,14 +211,14 @@ describe('SimpleTriggerConfig', () => {
component.trigger = new SimpleTrigger();
component.trigger.triggerKeyDTO = mockTriggerKey;
fixture.detectChanges();
const mockTrigger = new Trigger();
mockTrigger.triggerKeyDTO = mockTriggerKey;
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: 'TestJob', description: null};
const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/my-simple-trigger`);
getSimpleTriggerReq.flush(mockTrigger);
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
const submitButton = componentDe.query(By.css('form button'));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Reschedule');
@@ -247,7 +251,7 @@ describe('SimpleTriggerConfig', () => {
expect(component.simpleTriggerReactiveForm.value.triggerName).toEqual(testTriggerName);
component.triggerKey = null;
component.openNewTriggerForm();
expect(component.simpleTriggerReactiveForm.value.triggerName).toBeNull();
expect(component.simpleTriggerReactiveForm.value.jobClass).toBeNull();
@@ -255,6 +259,16 @@ describe('SimpleTriggerConfig', () => {
});
it('should not emit form open changes while applying a null trigger input', () => {
let formOpenChangeEmitted = false;
component.triggerFormOpenChange.subscribe(() => formOpenChangeEmitted = true);
component.triggerKey = null;
expect(formOpenChangeEmitted).toBeFalsy();
expect(component.shouldShowTheTriggerCardContent()).toBeFalsy();
});
it('should display the warning if there are no eligible jobs', () => {
fixture.detectChanges();
const getJobsReq = httpTestingController.expectOne(`${CONTEXT_PATH}/jobs`);

View File

@@ -42,14 +42,15 @@ export class SimpleTriggerConfigComponent implements OnInit {
private jobs: Array<String>;
enabledTriggerForm = false;
@Output()
onNewTrigger = new EventEmitter<SimpleTrigger>();
@Output()
triggerFormOpenChange = new EventEmitter<boolean>();
@Output()
onTriggerSubmitting = new EventEmitter<TriggerKey>();
constructor(
private formBuilder: UntypedFormBuilder,
private schedulerService: SchedulerService,
@@ -58,6 +59,7 @@ export class SimpleTriggerConfigComponent implements OnInit {
}
ngOnInit() {
this.simpleTriggerReactiveForm.disable();
this.fetchJobs();
}
@@ -66,24 +68,24 @@ export class SimpleTriggerConfigComponent implements OnInit {
}
openTriggerForm() {
this.enabledTriggerForm = true;
this.triggerFormOpenChange.emit(this.enabledTriggerForm);
this.simpleTriggerReactiveForm.enable();
this.triggerFormOpenChange.emit(true);
}
private closeTriggerForm() {
this.enabledTriggerForm = false;
this.triggerFormOpenChange.emit(this.enabledTriggerForm);
this.simpleTriggerReactiveForm.disable();
this.triggerFormOpenChange.emit(false);
}
@Input()
set triggerKey(triggerKey: TriggerKey) {
if (!triggerKey) {
this.openNewTriggerForm();
return;
} else if (!this.selectedTriggerKey || this.selectedTriggerKey.name !== triggerKey.name) {
this._resetTheTrigger();
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
this.fetchSelectedTrigger();
this.closeTriggerForm();
this.simpleTriggerReactiveForm.disable();
}
}
@@ -107,10 +109,11 @@ export class SimpleTriggerConfigComponent implements OnInit {
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(retTrigger))
this.triggerLoading = false;
this.triggerInProgress = this.trigger.mayFireAgain;
this.simpleTriggerReactiveForm.disable();
})
}
shouldShowTheTriggerCardContent = (): boolean => this.trigger !== null || this.enabledTriggerForm;
shouldShowTheTriggerCardContent = (): boolean => this.trigger !== null || this.simpleTriggerReactiveForm.enabled;
existsATriggerInProgress = (): boolean => this.trigger && this.triggerInProgress;
@@ -128,6 +131,17 @@ export class SimpleTriggerConfigComponent implements OnInit {
this.schedulerService.updateSimpleTriggerConfig : this.schedulerService.saveSimpleTriggerConfig;
const simpleTriggerCommand = this._fromReactiveFormToCommand();
if (!this.trigger) {
this.onTriggerSubmitting.emit(new TriggerKey(simpleTriggerCommand.triggerName, null));
setTimeout(() => this.submitTriggerConfig(schedulerServiceCall, simpleTriggerCommand));
return;
}
this.submitTriggerConfig(schedulerServiceCall, simpleTriggerCommand);
}
private submitTriggerConfig(schedulerServiceCall, simpleTriggerCommand: SimpleTriggerCommand) {
this.triggerLoading = true;
schedulerServiceCall(simpleTriggerCommand)
.subscribe((retTrigger: SimpleTrigger) => {
@@ -148,9 +162,8 @@ export class SimpleTriggerConfigComponent implements OnInit {
}
this.triggerLoading = false;
}, () => {
this.triggerLoading = false
});
this.triggerLoading = false;
});
}
private _triggerPeriodValidator(control: AbstractControl): ValidationErrors | null {
@@ -191,7 +204,7 @@ this.triggerLoading = false
};
private _fromReactiveFormToCommand = (): SimpleTriggerCommand => {
const reactiveFormValue = this.simpleTriggerReactiveForm.value;
const reactiveFormValue = this.simpleTriggerReactiveForm.getRawValue();
const simpleTriggerCommand = new SimpleTriggerCommand();
simpleTriggerCommand.triggerName = reactiveFormValue.triggerName;
simpleTriggerCommand.jobClass = reactiveFormValue.jobClass;

View File

@@ -6,12 +6,14 @@ import {MatDialog, MatDialogRef} from '@angular/material/dialog';
@Component({
template: `
<h3 mat-dialog-title>Coming Soon</h3>
<div mat-dialog-content>
<p>This feature is in roadmap and it will come with the next releases</p>
</div>
<div mat-dialog-actions>
<button mat-button (click)="closeDialog()" style="padding: 0.5em;width: 5em;">Ok</button>
<div style="padding:16px">
<h3 mat-dialog-title>Coming Soon</h3>
<div mat-dialog-content>
<p>This feature is in roadmap and it will come with the next releases</p>
</div>
<div mat-dialog-actions>
<button mat-button (click)="closeDialog()" style="padding: 0.5em;width: 5em;">Ok</button>
</div>
</div>`,
})
// tslint:disable-next-line:component-class-suffix

View File

@@ -1,3 +1,7 @@
:host {
flex: 1;
}
.content {
width: 100%;
}

View File

@@ -1,47 +1,48 @@
<div id="managerViewContainer" fxLayout="column" fxLayoutAlign="left stretch" fxLayoutGap="10px" fxFill>
<div id="schedulerBarContainer" fxLayout="column" fxLayoutAlign="left stretch">
<div id="managerViewContainer" class="flex flex-column flex-1 gap-6 h-100">
<div id="schedulerBarContainer">
<qrzmng-scheduler-control></qrzmng-scheduler-control>
</div>
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="center stretch" fxFlex="1 1 auto">
<div fxFlex="0 0 250px">
<div id="manager-content-container" class="flex flex-row flex-1 gap-6">
<div class="flex-1" style="max-width: 250px">
<div fxLayout="row" fxLayoutAlign="stretch" fxFill>
<qrzmng-trigger-list
(onNewTriggerClicked)="onNewTriggerRequested()"
[openedNewTriggerForm]="newTriggerFormOpened"
(onSelectedTrigger)="setSelectedTrigger($event)"
fxFill></qrzmng-trigger-list>
(onNewTriggerClicked)="onNewTriggerRequested()"
[openedNewTriggerForm]="newTriggerFormOpened"
(onSelectedTrigger)="setSelectedTrigger($event)"
fxFill></qrzmng-trigger-list>
</div>
</div>
<div fxFlex="1 1 350px">
<div fxLayout="row" fxFill>
<div class="flex-1" style="max-width: 350px">
<div fxLayout="row" fxFill>
<div fxLayout="column" fxFill>
<qrzmng-simple-trigger-config fxFill
<qrzmng-simple-trigger-config
fxFill
[triggerKey]="selectedTriggerKey"
(triggerFormOpenChange)="setNewTriggerFormOpened($event)"
(onTriggerSubmitting)="monitorTrigger($event)"
(onNewTrigger)="onNewTriggerCreated($event)">
</qrzmng-simple-trigger-config>
</div>
</div>
</div>
</div>
<div fxFlex="1 1 auto" style="margin-left: 20px;">
<div fxFlex="1 1 auto" fxLayout="column" fxLayoutAlign="start stretch" fxLayoutGap="6px">
<progress-panel
[triggerKey]=selectedTriggerKey
>
</progress-panel>
<logs-panel fxFlex="1 1 auto" fxFill
[triggerKey]=selectedTriggerKey
>
</logs-panel>
<div class="flex-1">
<div class="h-100 min-h-100 flex flex-column gap-6">
<div class="flex flex-column" >
<progress-panel class="flex-1"
[triggerKey]=monitoredTriggerKey
>
</progress-panel>
</div>
<div class="flex flex-column flex-1" style="max-height: calc(100% - 136px); min-height: calc(100% - 210px);">
<logs-panel class="flex flex-1 h-100 max-h-100"
[triggerKey]=monitoredTriggerKey
>
</logs-panel>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1 +1,10 @@
:host {
display: flex;
flex-direction: column;
flex: 1;
}
#manager-content-container {
height: calc(100% - 80px);
max-height: calc(100% - 80px);
}

View File

@@ -1,8 +1,8 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {SimpleTrigger} from '../../model/simple-trigger.model';
import {TriggerKey} from '../../model/triggerKey.model';
import {SimpleTriggerConfigComponent} from '../../components/simple-trigger-config';
import {TriggerListComponent} from '../../components';
import { Component, OnInit, ViewChild } from '@angular/core';
import { SimpleTrigger } from '../../model/simple-trigger.model';
import { TriggerKey } from '../../model/triggerKey.model';
import { SimpleTriggerConfigComponent } from '../../components/simple-trigger-config';
import { TriggerListComponent } from '../../components';
@Component({
selector: 'manager',
@@ -10,7 +10,6 @@ import {TriggerListComponent} from '../../components';
styleUrls: ['./manager.component.scss']
})
export class ManagerComponent implements OnInit {
@ViewChild(SimpleTriggerConfigComponent)
private triggerConfigComponent!: SimpleTriggerConfigComponent;
@@ -21,14 +20,15 @@ export class ManagerComponent implements OnInit {
selectedTriggerKey: TriggerKey;
constructor(
) { }
monitoredTriggerKey: TriggerKey;
ngOnInit() {
}
constructor() {}
ngOnInit() {}
onNewTriggerRequested() {
this.selectedTriggerKey = null;
this.monitoredTriggerKey = null;
this.newTriggerFormOpened = true;
if (this.triggerConfigComponent) {
this.triggerConfigComponent.openNewTriggerForm();
@@ -42,11 +42,15 @@ export class ManagerComponent implements OnInit {
setSelectedTrigger(triggerKey: TriggerKey) {
this.selectedTriggerKey = triggerKey;
this.monitoredTriggerKey = triggerKey;
this.newTriggerFormOpened = false;
}
monitorTrigger(triggerKey: TriggerKey) {
this.monitoredTriggerKey = triggerKey;
}
setNewTriggerFormOpened(opened: boolean) {
this.newTriggerFormOpened = opened;
}
}

View File

@@ -12,3 +12,91 @@ body {
flex:1;
background-color: #f1f1f1;
}
/**
TODO: Remove the below utility classes once tailwind is integrated.
*/
.font-13 {
font-size: 13px;
}
.font-large {
font-size: large;
}
.font-larger {
font-size: larger;
}
.justify-space-between {
justify-content: space-between;
}
.flex {
display: flex;
}
.flex-row {
flex-direction: row;
}
.flex-column {
flex-direction: column;
}
.flex-1 {
flex: 1;
}
.h-100 {
height: 100%;
}
.min-h-100 {
min-height: 100%;
}
.max-h-100 {
max-height: 100%;
}
.w-100 {
width: 100%;
}
.gap-6 {
gap: 6px;
}
.overflow-hidden {
overflow: hidden;
}
.overflow-y-auto {
overflow-y: auto;
}
.pb-16 {
padding-bottom: 16px !important;
}
.mdc-list-item__primary-text {
font-size: 0.8em !important;
}
.font-size-14 {
font-size: 14px;
}
.font-weight-500 {
font-weight: 500;
}
.display-block {
display: block;
}
.line-height-100 {
line-height: 100%;
}
.align-items-center {
align-items: center;
}

View File

@@ -10,7 +10,7 @@
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2020",
"target": "ES2022",
"typeRoots": [
"node_modules/@types"
],
@@ -18,6 +18,7 @@
"es2016",
"dom"
],
"module": "es2020"
"module": "es2020",
"useDefineForClassFields": false
}
}