#131 upgraded Angular to v21

This commit is contained in:
Fabio Formosa
2026-05-09 10:51:18 +02:00
parent 23417fa6a2
commit e90648c027
58 changed files with 15753 additions and 14005 deletions

View File

@@ -5,8 +5,8 @@
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.
last 2 Chrome versions
last 2 Firefox versions
last 2 Edge versions
last 2 Safari versions
last 2 iOS versions

View File

@@ -28,20 +28,12 @@ Happy linting! 💖
"plugins": [
"eslint-plugin-import",
"@angular-eslint/eslint-plugin",
"@typescript-eslint",
"@typescript-eslint/tslint"
"@typescript-eslint"
],
"root": true,
"rules": {
"@angular-eslint/component-class-suffix": "error",
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"@angular-eslint/component-class-suffix": "off",
"@angular-eslint/component-selector": "off",
"@angular-eslint/directive-class-suffix": "error",
"@angular-eslint/directive-selector": [
"error",
@@ -51,7 +43,6 @@ Happy linting! 💖
"style": "camelCase"
}
],
"@angular-eslint/no-host-metadata-property": "error",
"@angular-eslint/no-input-rename": "error",
"@angular-eslint/no-inputs-metadata-property": "error",
"@angular-eslint/no-output-rename": "error",
@@ -80,19 +71,8 @@ Happy linting! 💖
}
}
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "variable",
"format": [
"camelCase",
"UPPER_CASE"
],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid"
}
],
"@typescript-eslint/member-ordering": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": [
@@ -109,26 +89,10 @@ Happy linting! 💖
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/quotes": [
"error",
"single"
],
"@typescript-eslint/semi": [
"off",
null
],
"@typescript-eslint/tslint/config": [
"error",
{
"rules": {
"import-spacing": true,
"invoke-injectable": true,
"no-access-missing-member": true,
"templates-use-public": true,
"whitespace": true
}
}
],
"@typescript-eslint/type-annotation-spacing": "off",
"@typescript-eslint/unified-signatures": "error",
"brace-style": [

View File

@@ -19,7 +19,7 @@
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"allowedCommonJsDependencies": [
"stompjs", "sockjs-client", "moment", "angular2-uuid"
"@stomp/stompjs", "stompjs", "sockjs-client", "angular2-uuid"
],
"assets": [
"src/assets",
@@ -67,18 +67,18 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "quartz-manager-ui:build:development"
"buildTarget": "quartz-manager-ui:build:development"
},
"configurations": {
"production": {
"browserTarget": "quartz-manager-ui:build:production"
"buildTarget": "quartz-manager-ui:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "quartz-manager-ui:build"
"buildTarget": "quartz-manager-ui:build"
}
},
"lint": {
@@ -89,38 +89,35 @@
}
}
}
},
"quartz-manager-ui-e2e": {
"root": "e2e",
"sourceRoot": "e2e",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "quartz-manager-ui:serve"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": []
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"prefix": "qrzmng",
"style": "css"
"style": "css",
"type": "component"
},
"@schematics/angular:directive": {
"prefix": "qrzmng"
"prefix": "qrzmng",
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
},
"cli": {

View File

@@ -0,0 +1,12 @@
const {createEsmPreset} = require('jest-preset-angular/presets');
module.exports = {
...createEsmPreset({
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$'
}),
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
transformIgnorePatterns: [
'node_modules/(?!(@angular|@stomp/rx-stomp|@stomp/stompjs|.*\\.mjs$)/)'
]
};

View File

@@ -1 +1,3 @@
import 'jest-preset-angular/setup-jest';
import {setupZoneTestEnv} from 'jest-preset-angular/setup-env/zone/index.mjs';
setupZoneTestEnv();

View File

@@ -1,42 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/0.13/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
files: [
],
preprocessors: {
},
mime: {
'text/x-typescript': ['ts','tsx']
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
reporters: config.angularCli && config.angularCli.codeCoverage
? ['progress', 'coverage-istanbul']
: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -6,89 +6,68 @@
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build --configuration production",
"test": "jest",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"lint": "ng lint",
"lint:sonar": "eslint --no-eslintrc -c .eslintrc.sonar.json \"src/**/*.ts\"",
"lint:sonar:fix": "eslint --no-eslintrc -c .eslintrc.sonar.json \"src/**/*.ts\" --fix",
"e2e": "ng e2e"
"lint:sonar:fix": "eslint --no-eslintrc -c .eslintrc.sonar.json \"src/**/*.ts\" --fix"
},
"private": true,
"dependencies": {
"@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",
"@angular/animations": "21.2.12",
"@angular/cdk": "21.2.10",
"@angular/common": "21.2.12",
"@angular/compiler": "21.2.12",
"@angular/core": "21.2.12",
"@angular/forms": "21.2.12",
"@angular/material": "21.2.10",
"@angular/platform-browser": "21.2.12",
"@angular/platform-browser-dynamic": "21.2.12",
"@angular/platform-server": "21.2.12",
"@angular/router": "21.2.12",
"@auth0/angular-jwt": "5.2.0",
"@danielmoncada/angular-datetime-picker": "21.0.0",
"@fortawesome/fontawesome": "^1.1.4",
"@fortawesome/fontawesome-free-regular": "^5.0.8",
"@fortawesome/fontawesome-free-solid": "^5.0.8",
"@stomp/rx-stomp": "1.2.0",
"core-js": "2.5.1",
"@stomp/rx-stomp": "2.4.0",
"@stomp/stompjs": "^7.2.0",
"hammerjs": "2.0.8",
"moment": "^2.29.1",
"net": "^1.0.2",
"roboto-fontface": "^0.10.0",
"rxjs": "6.5.5",
"rxjs": "^7.8.2",
"sockjs-client": "^1.1.1",
"stompjs": "^2.3.3",
"tslib": "~2.4.1",
"zone.js": "~0.12.0"
"tslib": "^2.8.1",
"uuid": "^13.0.0",
"zone.js": "~0.16.0"
},
"devDependencies": {
"@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",
"@angular-devkit/build-angular": "^21.2.10",
"@angular-devkit/core": "^21.2.10",
"@angular-eslint/builder": "21.3.1",
"@angular-eslint/eslint-plugin": "21.3.1",
"@angular-eslint/eslint-plugin-template": "21.3.1",
"@angular-eslint/schematics": "21.3.1",
"@angular-eslint/template-parser": "21.3.1",
"@angular/cli": "^21.2.10",
"@angular/compiler-cli": "21.2.12",
"@angular/language-service": "21.2.12",
"@types/hammerjs": "2.0.34",
"@types/jasmine": "2.5.54",
"@types/jasminewd2": "2.0.3",
"@types/jest": "28.1.1",
"@types/node": "^12.11.1",
"@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",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"@types/jasmine": "^5.1.13",
"@types/jest": "^30.0.0",
"@types/node": "^22.13.14",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-sonarjs": "^0.16.0",
"jasmine-core": "~4.5.0",
"jasmine-spec-reporter": "~7.0.0",
"jest": "28.1.3",
"jest-preset-angular": "~12.2.3",
"karma": "~6.4.1",
"karma-chrome-launcher": "~3.1.1",
"karma-cli": "2.0.0",
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"eslint-plugin-sonarjs": "^4.0.3",
"jest": "30.4.1",
"jest-environment-jsdom": "^30.2.0",
"jest-preset-angular": "^16.1.5",
"jsdom": "^27.3.0",
"prettier": "^2.8.1",
"prettier-eslint": "^15.0.1",
"protractor": "^7.0.0",
"ts-node": "10.9.1",
"typescript": "4.9.5"
},
"jest": {
"preset": "jest-preset-angular",
"setupFilesAfterEnv": [
"<rootDir>/jest.setup.ts"
]
"typescript": "5.9.3"
}
}

View File

@@ -1,32 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/long-stack-trace-zone';
import 'zone.js/dist/proxy.js';
import 'zone.js/dist/sync-test';
import 'zone.js/dist/jasmine-patch';
import 'zone.js/dist/async-test';
import 'zone.js/dist/fake-async-test';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
declare let __karma__: any;
declare let require: any;
// Prevent Karma from running prematurely.
__karma__.loaded = function () {};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
// Finally, start Karma to run the tests.
__karma__.start();

View File

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

View File

@@ -1,4 +1,4 @@
import {TestBed, async, waitForAsync} from '@angular/core/testing';
import {TestBed, waitForAsync} from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
@@ -42,7 +42,7 @@ describe('AppComponent', () => {
}).compileComponents();
}));
it('should create the app', async(() => {
it('should create the app', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();

View File

@@ -1,14 +1,22 @@
import {Component} from '@angular/core';
// I remove temporary fontawesome5 and downgrade to fontawesome4
import fontawesome from '@fortawesome/fontawesome';
import solid from '@fortawesome/fontawesome-free-solid/';
fontawesome.library.add(solid);
import fontawesome from '@fortawesome/fontawesome';
import {
faCheckCircle,
faExclamationCircle,
faExclamationTriangle,
faPause,
faPlay,
faTimesCircle
} from '@fortawesome/fontawesome-free-solid';
fontawesome.library.add(faCheckCircle, faExclamationCircle, faExclamationTriangle, faPause, faPlay, faTimesCircle);
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: false
})
export class AppComponent {

View File

@@ -1,7 +1,7 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER} from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import {JWT_OPTIONS, JwtModule} from '@auth0/angular-jwt';
@@ -17,18 +17,14 @@ import {MatToolbarModule} from '@angular/material/toolbar';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
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';
import { NgxMatMomentModule } from '@angular-material-components/moment-adapter';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FlexLayoutModule } from '@angular/flex-layout';
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 {OwlDateTimeModule, OwlNativeDateTimeModule} from '@danielmoncada/angular-datetime-picker';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { ManagerComponent } from './views/manager';
@@ -73,86 +69,79 @@ export function jwtOptionsFactory(apiService: ApiService) {
tokenGetter: () => {
return apiService.getToken();
},
whitelistedDomains: ['localhost:8080', 'localhost:4200']
allowedDomains: ['localhost:8080', 'localhost:4200']
}
}
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
FooterComponent,
ManagerComponent,
GithubComponent,
LoginComponent,
NotFoundComponent,
AccountMenuComponent,
SimpleTriggerConfigComponent,
SchedulerControlComponent,
LogsPanelComponent,
ProgressPanelComponent,
ForbiddenComponent,
GenericErrorComponent,
TriggerListComponent
],
imports: [
BrowserAnimationsModule,
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule,
AppRoutingModule,
JwtModule.forRoot({
jwtOptionsProvider: {
provide: JWT_OPTIONS,
useFactory: jwtOptionsFactory,
deps: [ApiService]
}
}),
MatDialogModule,
MatMenuModule,
MatTooltipModule,
MatButtonModule,
MatChipsModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatToolbarModule,
MatCardModule,
MatListModule,
MatProgressSpinnerModule,
MatProgressBarModule,
MatDatepickerModule, MatNativeDateModule,
NgxMatMomentModule,
NgxMatDatetimePickerModule,
MatSidenavModule,
FlexLayoutModule
],
providers: [
{
provide: APP_BASE_HREF,
useValue: getHtmlBaseUrl()
},
{
'provide': APP_INITIALIZER,
'useFactory': initUserFactory,
'deps': [UserService],
'multi': true
},
LoginGuard,
GuestGuard,
AdminGuard,
SchedulerService,
JobService,
TriggerService,
ProgressRxWebsocketService,
LogsRxWebsocketService,
AuthService,
ApiService,
UserService,
ConfigService,
MatIconRegistry
],
bootstrap: [AppComponent]
})
@NgModule({ declarations: [
AppComponent,
HeaderComponent,
FooterComponent,
ManagerComponent,
GithubComponent,
LoginComponent,
NotFoundComponent,
AccountMenuComponent,
SimpleTriggerConfigComponent,
SchedulerControlComponent,
LogsPanelComponent,
ProgressPanelComponent,
ForbiddenComponent,
GenericErrorComponent,
TriggerListComponent
],
bootstrap: [AppComponent], imports: [BrowserAnimationsModule,
BrowserModule,
FormsModule,
ReactiveFormsModule,
AppRoutingModule,
JwtModule.forRoot({
jwtOptionsProvider: {
provide: JWT_OPTIONS,
useFactory: jwtOptionsFactory,
deps: [ApiService]
}
}),
MatDialogModule,
MatMenuModule,
MatTooltipModule,
MatButtonModule,
MatChipsModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatToolbarModule,
MatCardModule,
MatListModule,
MatProgressSpinnerModule,
MatProgressBarModule,
OwlDateTimeModule,
OwlNativeDateTimeModule,
MatSidenavModule,
], providers: [
{
provide: APP_BASE_HREF,
useValue: getHtmlBaseUrl()
},
{
'provide': APP_INITIALIZER,
'useFactory': initUserFactory,
'deps': [UserService],
'multi': true
},
LoginGuard,
GuestGuard,
AdminGuard,
SchedulerService,
JobService,
TriggerService,
ProgressRxWebsocketService,
LogsRxWebsocketService,
AuthService,
ApiService,
UserService,
ConfigService,
MatIconRegistry,
provideHttpClient(withInterceptorsFromDi())
] })
export class AppModule { }

View File

@@ -1,4 +1,4 @@
<mat-toolbar id="footer" style="color: rgba(255, 255, 255, 0.541176);" fxLayout="row" fxLayoutAlign="center center">
<mat-toolbar id="footer" class="flex flex-row justify-center align-items-center" style="color: rgba(255, 255, 255, 0.541176);">
<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>

View File

@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss'],
standalone: false
})
export class FooterComponent implements OnInit {

View File

@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-github',
templateUrl: './github.component.html',
styleUrls: ['./github.component.scss']
selector: 'app-github',
templateUrl: './github.component.html',
styleUrls: ['./github.component.scss'],
standalone: false
})
export class GithubComponent implements OnInit {

View File

@@ -7,9 +7,10 @@ import {
import { Router } from '@angular/router';
@Component({
selector: 'app-account-menu',
templateUrl: './account-menu.component.html',
styleUrls: ['./account-menu.component.scss']
selector: 'app-account-menu',
templateUrl: './account-menu.component.html',
styleUrls: ['./account-menu.component.scss'],
standalone: false
})
export class AccountMenuComponent implements OnInit {

View File

@@ -1,35 +1,39 @@
<mat-toolbar color="primary" class="app-navbar">
<button mat-button mat-ripple routerLink="/">
<!-- <img alt="Quartz Manager" class="app-angular-logo" src="assets/image/angular-white-transparent.svg">-->
<span>Quartz Manager</span>
</button>
<div class="right">
<div fxFlex="1 1 auto" fxLayout="row" fxLayoutAlign="flex-end center">
<button *ngIf="!hasSignedIn() && !noAuthenticationRequired()" routerLink="/login" mat-button mat-ripple>
<span>Login</span>
</button>
<button
class="greeting-button"
*ngIf="hasSignedIn() && !noAuthenticationRequired()"
mat-button mat-ripple
[matMenuTriggerFor]="accountMenu">
<span>Hi, {{userName()}}</span>
</button>
<button
class="greeting-hamburger"
*ngIf="hasSignedIn()"
mat-icon-button mat-ripple
[matMenuTriggerFor]="accountMenu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #accountMenu
class="app-header-accountMenu"
yposition="below"
[overlapTrigger]="false">
<app-account-menu ></app-account-menu>
</mat-menu>
</div>
</div>
</mat-toolbar>
<mat-toolbar color="primary" class="app-navbar">
<button mat-button mat-ripple routerLink="/">
<!-- <img alt="Quartz Manager" class="app-angular-logo" src="assets/image/angular-white-transparent.svg">-->
<span>Quartz Manager</span>
</button>
<div class="right">
<div class="flex flex-row flex-1 justify-flex-end align-items-center">
@if (!hasSignedIn() && !noAuthenticationRequired()) {
<button routerLink="/login" mat-button mat-ripple>
<span>Login</span>
</button>
} @if (hasSignedIn() && !noAuthenticationRequired()) {
<button
class="greeting-button"
mat-button
mat-ripple
[matMenuTriggerFor]="accountMenu">
<span>Hi, {{ userName() }}</span>
</button>
} @if (hasSignedIn()) {
<button
class="greeting-hamburger"
mat-icon-button
mat-ripple
[matMenuTriggerFor]="accountMenu">
<mat-icon>menu</mat-icon>
</button>
}
<mat-menu
#accountMenu
class="app-header-accountMenu"
yposition="below"
[overlapTrigger]="false">
<app-account-menu></app-account-menu>
</mat-menu>
</div>
</div>
</mat-toolbar>

View File

@@ -6,10 +6,11 @@ import {
} from '../../services';
import { Router } from '@angular/router';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
standalone: false
})
export class HeaderComponent implements OnInit {

View File

@@ -1,65 +1,67 @@
<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 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>
</div>
</mat-card-content>
</mat-card>
<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">
@if (!selectedTriggerName && (!logs || logs.length == 0)) {
<div class="h-100 w-100" style="text-align: center">
<img
src="assets/image/logs.svg"
alt="no logs"
width="320"
style="margin-top: 6em" />
</div>
} @if (isWaitingForLogs()) {
<div
class="waitingLogs flex flex-column align-items-center justify-center gap-12">
<mat-spinner diameter="36"></mat-spinner>
<div>Waiting for logs from {{ selectedTriggerName }}...</div>
</div>
}
<div id="logs" class="w-100" style="height: 100%">
@for (log of logs; track log; let first = $first) {
<div
class="log-row flex flex-row gap-10">
<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>
</div>
</mat-card-content>
</mat-card>

View File

@@ -18,7 +18,7 @@ describe('LogsPanelComponent', () => {
component.triggerKey = new TriggerKey('trigger-1', null);
expect(logsRxWebsocketService.watch).toHaveBeenCalledWith('/topic/logs/trigger-1');
expect(logsRxWebsocketService.watch.mock.calls[0]).toEqual(['/topic/logs/trigger-1']);
expect(component.selectedTriggerName).toEqual('trigger-1');
expect(component.isWaitingForLogs()).toBeTruthy();
@@ -57,7 +57,7 @@ describe('LogsPanelComponent', () => {
component.triggerKey = new TriggerKey('trigger-2', null);
expect(firstSubscription.unsubscribe).toHaveBeenCalled();
expect(logsRxWebsocketService.watch).toHaveBeenCalledWith('/topic/logs/trigger-2');
expect(logsRxWebsocketService.watch.mock.calls[1]).toEqual(['/topic/logs/trigger-2']);
});
it('should clear logs when the trigger changes', () => {

View File

@@ -6,10 +6,11 @@ import {map} from 'rxjs/operators';
import {TriggerKey} from '../../model/triggerKey.model';
@Component({
selector: 'logs-panel',
templateUrl: './logs-panel.component.html',
styleUrls: ['./logs-panel.component.scss']
@Component({
selector: 'logs-panel',
templateUrl: './logs-panel.component.html',
styleUrls: ['./logs-panel.component.scss'],
standalone: false
})
export class LogsPanelComponent implements OnInit, OnDestroy {

View File

@@ -1,43 +1,72 @@
<!-- <div class="progress" [hidden]="progress.percentage < 0">
<div class="progress-bar"
role="progressbar"
[ngStyle]="{width: percentageStr}">
{{percentageStr}}
</div>
</div> -->
<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">
<mat-progress-bar mode="determinate" value="{{progress.percentage}}"></mat-progress-bar>
{{percentageStr}}
</div>
<div id="counterBox" fxLayout="row" fxLayoutAlign="center" *ngIf="progress.timesTriggered">
<span id="timesTriggeredCounter" class="animated pulse">{{progress.timesTriggered}}</span>
<span id="totCounter" *ngIf="progress.repeatCount > 0">&nbsp;/&nbsp;{{progress.repeatCount}} </span>
</div>
<mat-divider *ngIf="progress.timesTriggered"></mat-divider>
<div fxLayout="row" fxLayoutAlign="space-around center">
<div class="fireBox">
<div class="fireBoxHeader">prev fire time</div>
<div class="fireBoxContent"><span class="animated pulse">{{progress.previousFireTime|date:'dd-MM-yyyy HH:mm:ss'}}</span></div>
<div class="fireBoxContent" [hidden]="progress.previousFireTime"><span>-</span></div>
</div>
<div class="fireBox">
<div class="fireBoxHeader">next fire time</div>
<div class="fireBoxContent"><span class="animated pulse">{{progress.nextFireTime|date:'dd-MM-yyyy HH:mm:ss'}}</span></div>
<div class="fireBoxContent" [hidden]="progress.nextFireTime"><span>-</span></div>
</div>
<div class="fireBox">
<div class="fireBoxHeader">final fire time</div>
<div class="fireBoxContent"><span class="animated pulse">{{progress.finalFireTime|date:'dd-MM-yyyy HH:mm:ss'}}</span></div>
<div class="fireBoxContent" [hidden]="progress.finalFireTime"><span>-</span></div>
</div>
</div>
</mat-card-content>
</mat-card>
<!-- <div class="progress" [hidden]="progress.percentage < 0">
<div class="progress-bar"
role="progressbar"
[ngStyle]="{width: percentageStr}">
{{percentageStr}}
</div>
</div> -->
<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>
@if (progress.percentage !== -1) {
<div id="progressBarBox">
<mat-progress-bar
mode="determinate"
value="{{ progress.percentage }}"></mat-progress-bar>
{{ percentageStr }}
</div>
} @if (progress.timesTriggered) {
<div id="counterBox" class="flex flex-row justify-center">
<span id="timesTriggeredCounter" class="animated pulse">{{
progress.timesTriggered
}}</span>
@if (progress.repeatCount > 0) {
<span id="totCounter">&nbsp;/&nbsp;{{ progress.repeatCount }} </span>
}
</div>
} @if (progress.timesTriggered) {
<mat-divider></mat-divider>
}
<div class="flex flex-row align-items-center justify-space-around">
<div class="fireBox">
<div class="fireBoxHeader">prev fire time</div>
<div class="fireBoxContent">
<span class="animated pulse">{{
progress.previousFireTime | date : 'dd-MM-yyyy HH:mm:ss'
}}</span>
</div>
<div class="fireBoxContent" [hidden]="progress.previousFireTime">
<span>-</span>
</div>
</div>
<div class="fireBox">
<div class="fireBoxHeader">next fire time</div>
<div class="fireBoxContent">
<span class="animated pulse">{{
progress.nextFireTime | date : 'dd-MM-yyyy HH:mm:ss'
}}</span>
</div>
<div class="fireBoxContent" [hidden]="progress.nextFireTime">
<span>-</span>
</div>
</div>
<div class="fireBox">
<div class="fireBoxHeader">final fire time</div>
<div class="fireBoxContent">
<span class="animated pulse">{{
progress.finalFireTime | date : 'dd-MM-yyyy HH:mm:ss'
}}</span>
</div>
<div class="fireBoxContent" [hidden]="progress.finalFireTime">
<span>-</span>
</div>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@@ -19,7 +19,7 @@ describe('ProgressPanelComponent', () => {
component.triggerKey = new TriggerKey('trigger-1', null);
expect(progressRxWebsocketService.watch).toHaveBeenCalledWith('/topic/progress/trigger-1');
expect(progressRxWebsocketService.watch.mock.calls[0]).toEqual(['/topic/progress/trigger-1']);
messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
jest.runOnlyPendingTimers();
@@ -48,7 +48,7 @@ describe('ProgressPanelComponent', () => {
component.triggerKey = new TriggerKey('trigger-2', null);
expect(firstSubscription.unsubscribe).toHaveBeenCalled();
expect(progressRxWebsocketService.watch).toHaveBeenCalledWith('/topic/progress/trigger-2');
expect(progressRxWebsocketService.watch.mock.calls[1]).toEqual(['/topic/progress/trigger-2']);
});
it('should reset progress when the trigger changes', () => {

View File

@@ -4,10 +4,11 @@ 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']
@Component({
selector: 'progress-panel',
templateUrl: './progress-panel.component.html',
styleUrls: ['./progress-panel.component.scss'],
standalone: false
})
export class ProgressPanelComponent implements OnInit, OnDestroy {

View File

@@ -1,26 +1,40 @@
<mat-card>
<mat-card-content>
<div fxLayout="row" fxLayoutAlign="left stretch" fxLayoutGap="30px">
<button id="schedulerControllerBtn" mat-raised-button class="btn btn-default large-btn" (click)="startOrPause()">
<span *ngIf = "scheduler?.status === 'RUNNING'">
<i class="fas fa-pause red"></i>
</span>
<span *ngIf = "scheduler?.status === 'STOPPED' || scheduler?.status === 'PAUSED'">
<i class="fas fa-play green"></i>
</span>
</button>
<div fxLayout="column center">
<mat-card-subtitle style="margin: auto;"><b>SCHEDULER</b></mat-card-subtitle>
</div>
<mat-divider [vertical]="true"></mat-divider>
<div fxLayout="column" class="justify-space-between">
<div><label>Name</label></div>
<div><span id="scheduler-name">{{scheduler?.name}}</span></div>
</div>
<div fxLayout="column" class="justify-space-between">
<div><label>Instance ID</label></div>
<div><span id="scheduler-instance">{{scheduler?.instanceId}}</span></div>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card>
<mat-card-content>
<div class="flex flex-row align-items-stretch gap-30">
<button
id="schedulerControllerBtn"
mat-raised-button
class="btn btn-default large-btn"
(click)="startOrPause()">
@if (scheduler?.status === 'RUNNING') {
<span>
<i class="fas fa-pause red"></i>
</span>
} @if (scheduler?.status === 'STOPPED' || scheduler?.status ===
'PAUSED') {
<span>
<i class="fas fa-play green"></i>
</span>
}
</button>
<div class="flex flex-column align-items-center">
<mat-card-subtitle style="margin: auto"
><b>SCHEDULER</b></mat-card-subtitle
>
</div>
<mat-divider [vertical]="true"></mat-divider>
<div class="flex flex-column justify-space-between">
<div><label>Name</label></div>
<div>
<span id="scheduler-name">{{ scheduler?.name }}</span>
</div>
</div>
<div class="flex flex-column justify-space-between">
<div><label>Instance ID</label></div>
<div>
<span id="scheduler-instance">{{ scheduler?.instanceId }}</span>
</div>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@@ -2,10 +2,11 @@ import {Component, OnInit} from '@angular/core';
import {SchedulerService, UserService} from '../../services';
import {Scheduler} from '../../model/scheduler.model';
@Component({
selector: 'qrzmng-scheduler-control',
templateUrl: './scheduler-control.component.html',
styleUrls: ['./scheduler-control.component.scss']
@Component({
selector: 'qrzmng-scheduler-control',
templateUrl: './scheduler-control.component.html',
styleUrls: ['./scheduler-control.component.scss'],
standalone: false
})
export class SchedulerControlComponent implements OnInit {

View File

@@ -1,173 +1,257 @@
<mat-card fxFlex="1 1 auto">
<mat-card-header style="padding-bottom: 16px;">
<mat-card class="trigger-config-card">
<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;padding: 1em;">
<mat-card id="noEligibleJobsAlert" *ngIf="jobs?.length === 0" style="background-color: #ff6385">
@if (shouldShowTheTriggerCardContent()) {
<mat-card-content class="trigger-config-content">
<div
class="flex flex-column"
style="
overflow-y: auto;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow: auto;
padding: 1em;
">
@if (jobs?.length === 0) {
<mat-card id="noEligibleJobsAlert" style="background-color: #ff6385">
<mat-card-content>
<i class="fas fa-exclamation-circle" style="color: #fff"></i>&nbsp;<strong>WARNING</strong>
Not found any eligible job classes for quartz-manager! <br/>
<p style="font-size: 0.8em">Please, make sure you have extended <i>AbstractQuartzManagerJob</i> and set the
app prop <i>quartz-manager.jobClassPackages</i> with the correct java package </p>
<i class="fas fa-exclamation-circle" style="color: #fff"></i
>&nbsp;<strong>WARNING</strong> Not found any eligible job classes for
quartz-manager! <br />
<p style="font-size: 0.8em">
Please, make sure you have extended
<i>AbstractQuartzManagerJob</i> and set the app prop
<i>quartz-manager.jobClassPackages</i> with the correct java package
</p>
</mat-card-content>
</mat-card>
<form name="triggerConfigForm" class="trigger-config-form" fxFlex="1 1 100%"
[formGroup]="simpleTriggerReactiveForm" (ngSubmit)="onSubmitTriggerConfig()">
}
<form
name="triggerConfigForm"
class="trigger-config-form"
class="flex-1"
[formGroup]="simpleTriggerReactiveForm"
(ngSubmit)="onSubmitTriggerConfig()">
<div>
<mat-form-field
class="full-size-input">
<mat-form-field class="full-size-input">
<mat-label>Trigger Name</mat-label>
<input id="triggerName"
matInput placeholder="name of the trigger (unique)"
formControlName="triggerName" name="triggerName">
<mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerName.errors?.required">
Name is <strong>required</strong>
</mat-error>
<input
id="triggerName"
matInput
placeholder="name of the trigger (unique)"
formControlName="triggerName"
name="triggerName" />
@if
(simpleTriggerReactiveForm.controls.triggerName.errors?.required) {
<mat-error> Name is <strong>required</strong> </mat-error>
}
</mat-form-field>
</div>
<div>
<mat-form-field
class="full-size-input"
>
<mat-form-field class="full-size-input">
<mat-label>Job Class</mat-label>
<mat-select id="jobClass" name="jobClass" formControlName="jobClass">
<mat-option *ngFor="let job of jobs" [value]="job" class="font-13">
{{job}}
<mat-select
id="jobClass"
name="jobClass"
formControlName="jobClass">
@for (job of jobs; track job) {
<mat-option [value]="job" class="font-13">
{{ job }}
</mat-option>
}
</mat-select>
<mat-error *ngIf="simpleTriggerReactiveForm.controls.jobClass.errors?.required">
Job is <strong>required</strong>
</mat-error>
@if (simpleTriggerReactiveForm.controls.jobClass.errors?.required) {
<mat-error> Job is <strong>required</strong> </mat-error>
}
</mat-form-field>
</div>
<div>
<mat-form-field
class="full-size-input"
>
<mat-form-field class="full-size-input">
<mat-label>Misfire Instruction</mat-label>
<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-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 class="font-13" value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT">RESCHEDULE NOW WITH
REMAINING REPEAT COUNT
<mat-option
class="font-13"
value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT"
>RESCHEDULE NOW WITH REMAINING REPEAT COUNT
</mat-option>
<mat-option class="font-13" value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT">RESCHEDULE NEXT WITH
REMAINING COUNT
<mat-option
class="font-13"
value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT"
>RESCHEDULE NEXT WITH REMAINING COUNT
</mat-option>
<mat-option class="font-13" value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT">RESCHEDULE NEXT WITH EXISTING
COUNT
<mat-option
class="font-13"
value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT"
>RESCHEDULE NEXT WITH EXISTING COUNT
</mat-option>
</mat-select>
<mat-error *ngIf="simpleTriggerReactiveForm.controls.misfireInstruction.errors?.required">
@if
(simpleTriggerReactiveForm.controls.misfireInstruction.errors?.required)
{
<mat-error>
The misfire instruction is <strong>required</strong>
</mat-error>
}
</mat-form-field>
<div class="small" [innerHTML]="getMisfireInstructionCaption()"></div>
</div>
<br/>
<br />
<div formGroupName="triggerPeriod">
<div>
<mat-form-field
class="full-size-input"
>
<mat-form-field class="full-size-input">
<mat-label>Start Date (optional)</mat-label>
<input id="startDate"
matInput
[ngxMatDatetimePicker]="startDatePicker" placeholder="Choose a start date"
formControlName="startDate" name="startDate">
<mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #startDatePicker showSpinners="true" showSeconds="true">
</ngx-mat-datetime-picker>
<input
id="startDate"
matInput
[owlDateTime]="startDatePicker"
[owlDateTimeTrigger]="startDatePicker"
placeholder="Choose a start date"
formControlName="startDate"
name="startDate" />
<button
type="button"
class="datetime-picker-trigger"
mat-icon-button
matSuffix
[owlDateTimeTrigger]="startDatePicker">
<mat-icon>event</mat-icon>
</button>
<owl-date-time
#startDatePicker
[showSecondsTimer]="true">
</owl-date-time>
</mat-form-field>
</div>
<div>
<mat-form-field
class="full-size-input"
>
<mat-form-field class="full-size-input">
<mat-label>End Date (optional)</mat-label>
<input id="endDate"
matInput
[ngxMatDatetimePicker]="endDatePicker" placeholder="Choose a end date"
formControlName="endDate" name="endDate"
>
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #endDatePicker showSpinners="true" showSeconds="true">
</ngx-mat-datetime-picker>
<input
id="endDate"
matInput
[owlDateTime]="endDatePicker"
[owlDateTimeTrigger]="endDatePicker"
placeholder="Choose a end date"
formControlName="endDate"
name="endDate" />
<button
type="button"
class="datetime-picker-trigger"
mat-icon-button
matSuffix
[owlDateTimeTrigger]="endDatePicker">
<mat-icon>event</mat-icon>
</button>
<owl-date-time
#endDatePicker
[showSecondsTimer]="true">
</owl-date-time>
</mat-form-field>
<mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerPeriod.errors?.invalidTriggerPeriod" style="font-size: small">
the end date cannot be <strong>before</strong> the start date
@if
(simpleTriggerReactiveForm.controls.triggerPeriod.errors?.invalidTriggerPeriod)
{
<mat-error style="font-size: small">
the end date cannot be <strong>before</strong> the start date
</mat-error>
}
</div>
</div>
<div formGroupName="triggerRecurrence">
<div>
<mat-form-field
class="full-size-input"
>
<mat-form-field class="full-size-input">
<mat-label>Repeat Interval [in mills]</mat-label>
<input id="repeatInterval"
matInput placeholder="Repeat Interval [in mills]" type="number"
formControlName="repeatInterval" name="repeatInterval"
>
<mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerRecurrence.errors?.invalidTriggerRecurrence">
repeatCount and repeatInterval must be <strong>both</strong> set or unset
<input
id="repeatInterval"
matInput
placeholder="Repeat Interval [in mills]"
type="number"
formControlName="repeatInterval"
name="repeatInterval" />
@if
(simpleTriggerReactiveForm.controls.triggerRecurrence.errors?.invalidTriggerRecurrence)
{
<mat-error>
repeatCount and repeatInterval must be <strong>both</strong> set
or unset
</mat-error>
}
</mat-form-field>
</div>
<div>
<mat-form-field
class="full-size-input"
>
<mat-form-field class="full-size-input">
<mat-label>Repeat Count</mat-label>
<input id="repeatCount"
matInput placeholder="Repeat Count (-1 REPEAT INDEFINITELY)" type="number"
formControlName="repeatCount" name="repeatCount"
>
<mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerRecurrence.errors?.invalidTriggerRecurrence">
repeatCount and repeatInterval must be <strong>both</strong> set or unset
<input
id="repeatCount"
matInput
placeholder="Repeat Count (-1 REPEAT INDEFINITELY)"
type="number"
formControlName="repeatCount"
name="repeatCount" />
@if
(simpleTriggerReactiveForm.controls.triggerRecurrence.errors?.invalidTriggerRecurrence)
{
<mat-error>
repeatCount and repeatInterval must be <strong>both</strong> set
or unset
</mat-error>
}
</mat-form-field>
</div>
</div>
<br/>
<div fxLayout="row" fxFlexAlign="space-evenly center" style="padding-bottom: 1em;">
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="simpleTriggerReactiveForm.enabled">
<button mat-raised-button
type="button"
(click)="onResetReactiveForm()">
<br />
<div
class="flex flex-row align-items-center justify-space-evenly"
style="padding-bottom: 1em">
@if (simpleTriggerReactiveForm.enabled) {
<div class="flex-1" style="text-align: center">
<button
mat-raised-button
type="button"
(click)="onResetReactiveForm()">
Cancel
</button>
</div>
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="simpleTriggerReactiveForm.enabled">
<button mat-raised-button
type="submit" color="primary"
[disabled]="simpleTriggerReactiveForm.invalid">
} @if (simpleTriggerReactiveForm.enabled) {
<div class="flex-1" style="text-align: center">
<button
mat-raised-button
type="submit"
color="primary"
[disabled]="simpleTriggerReactiveForm.invalid">
Submit
</button>
</div>
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="!simpleTriggerReactiveForm.enabled">
<button mat-raised-button type="button"
(click)="openTriggerForm();simpleTriggerReactiveForm.controls['triggerName'].disable();">
Reschedule
} @if (!simpleTriggerReactiveForm.enabled) {
<div class="flex-1" style="text-align: center">
<button
mat-raised-button
type="button"
(click)="
openTriggerForm();
simpleTriggerReactiveForm.controls['triggerName'].disable()
">
Reschedule
</button>
</div>
}
</div>
</form>
</div>
</mat-card-content>
}
</mat-card>

View File

@@ -1,3 +1,23 @@
:host {
display: flex;
height: 100%;
min-height: 0;
width: 100%;
}
.trigger-config-card {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.trigger-config-content {
flex: 1;
min-height: 0;
position: relative;
}
.small{
font-size: 0.8em;
}

View File

@@ -220,7 +220,7 @@ describe('SimpleTriggerConfig', () => {
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
const submitButton = componentDe.query(By.css('form button'));
const submitButton = componentDe.query(By.css('form button:not(.datetime-picker-trigger)'));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Reschedule');
});

View File

@@ -3,16 +3,16 @@ import {SchedulerService} from '../../services';
import {Scheduler} from '../../model/scheduler.model';
import {SimpleTriggerCommand} from '../../model/simple-trigger.command';
import {SimpleTrigger} from '../../model/simple-trigger.model';
import * as moment from 'moment';
import {TriggerKey} from '../../model/triggerKey.model';
import JobService from '../../services/job.service';
import {MisfireInstruction, MisfireInstructionCaption} from '../../model/misfire-instruction.model';
import {AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, Validators} from '@angular/forms';
@Component({
selector: 'qrzmng-simple-trigger-config',
templateUrl: './simple-trigger-config.component.html',
styleUrls: ['./simple-trigger-config.component.scss']
selector: 'qrzmng-simple-trigger-config',
templateUrl: './simple-trigger-config.component.html',
styleUrls: ['./simple-trigger-config.component.scss'],
standalone: false
})
export class SimpleTriggerConfigComponent implements OnInit {
@@ -22,8 +22,8 @@ export class SimpleTriggerConfigComponent implements OnInit {
triggerName: [this.trigger?.triggerKeyDTO.name, Validators.required],
jobClass: [this.trigger?.jobDetailDTO.jobClassName, Validators.required],
triggerPeriod: this.formBuilder.group({
startDate: [this.trigger?.startTime && moment(this.trigger?.startTime)],
endDate: [this.trigger?.endTime && moment(this.trigger?.endTime)]
startDate: [this.trigger?.startTime && new Date(this.trigger.startTime)],
endDate: [this.trigger?.endTime && new Date(this.trigger.endTime)]
}, {validators: this._triggerPeriodValidator}),
triggerRecurrence: this.formBuilder.group({
repeatCount: [this.trigger?.repeatCount],
@@ -170,7 +170,7 @@ export class SimpleTriggerConfigComponent implements OnInit {
const startDate = control.get('startDate');
const endDate = control.get('endDate');
if (startDate.value && endDate.value) {
return endDate.value.isBefore(startDate.value) ?
return endDate.value < startDate.value ?
<ValidationErrors>{invalidTriggerPeriod: true} : null;
}
return null;
@@ -196,8 +196,8 @@ export class SimpleTriggerConfigComponent implements OnInit {
simpleTriggerReactiveForm.jobClass = simpleTrigger.jobDetailDTO.jobClassName;
simpleTriggerReactiveForm.triggerRecurrence.repeatCount = simpleTrigger.repeatCount || null;
simpleTriggerReactiveForm.triggerRecurrence.repeatInterval = simpleTrigger.repeatInterval || null;
simpleTriggerReactiveForm.triggerPeriod.startDate = (simpleTrigger.startTime && moment(simpleTrigger.startTime)) || null;
simpleTriggerReactiveForm.triggerPeriod.endDate = (simpleTrigger.endTime && moment(simpleTrigger.endTime)) || null;
simpleTriggerReactiveForm.triggerPeriod.startDate = (simpleTrigger.startTime && new Date(simpleTrigger.startTime)) || null;
simpleTriggerReactiveForm.triggerPeriod.endDate = (simpleTrigger.endTime && new Date(simpleTrigger.endTime)) || null;
simpleTriggerReactiveForm.misfireInstruction = (simpleTrigger.misfireInstruction
&& MisfireInstruction[simpleTrigger.misfireInstruction]) || null;
return simpleTriggerReactiveForm;
@@ -210,8 +210,8 @@ export class SimpleTriggerConfigComponent implements OnInit {
simpleTriggerCommand.jobClass = reactiveFormValue.jobClass;
simpleTriggerCommand.repeatCount = reactiveFormValue.triggerRecurrence.repeatCount;
simpleTriggerCommand.repeatInterval = reactiveFormValue.triggerRecurrence.repeatInterval;
simpleTriggerCommand.startDate = reactiveFormValue.triggerPeriod.startDate?.toDate();
simpleTriggerCommand.endDate = reactiveFormValue.triggerPeriod.endDate?.toDate();
simpleTriggerCommand.startDate = reactiveFormValue.triggerPeriod.startDate;
simpleTriggerCommand.endDate = reactiveFormValue.triggerPeriod.endDate;
simpleTriggerCommand.misfireInstruction = reactiveFormValue.misfireInstruction;
return simpleTriggerCommand;
}

View File

@@ -1,19 +1,42 @@
<mat-card fxFlex="1 1 auto" style="padding-left: 0; padding-right: 0">
<mat-card-header fxLayout="row" fxLayoutAlign="space-between none" style="padding-right: 1em;" >
<mat-card class="trigger-list-card" style="padding-left: 0; padding-right: 0">
<mat-card-header
class="flex flex-row justify-space-between"
style="padding-right: 1em">
<mat-card-subtitle><b>TRIGGERS</b></mat-card-subtitle>
<button *ngIf="!triggerFormIsOpen" mat-raised-button style="top: -0.5em" color="primary" (click)="onNewTriggerBtnClicked()">
@if (!triggerFormIsOpen) {
<button
mat-raised-button
style="top: -0.5em"
color="primary"
(click)="onNewTriggerBtnClicked()">
new
</button>
}
</mat-card-header>
<mat-divider></mat-divider>
<mat-card-content style="position: relative; height: 100%">
<mat-nav-list style="overflow-y: auto; position: absolute; left: 0; right: 0; top: 0; bottom: 0; overflow: auto; height: calc(100% - 3em)">
<mat-list-item *ngFor="let triggerKey of getTriggerKeyList()" class="triggerItemList"
[ngClass]="{'selectedTrigger': selectedTrigger && selectedTrigger.name==triggerKey.name}"
(click)="selectTrigger(triggerKey)"
>
<mat-card-content class="trigger-list-content">
<mat-nav-list
style="
overflow-y: auto;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow: auto;
height: calc(100% - 3em);
">
@for (triggerKey of getTriggerKeyList(); track triggerKey) {
<mat-list-item
class="triggerItemList"
[ngClass]="{
selectedTrigger:
selectedTrigger && selectedTrigger.name == triggerKey.name
}"
(click)="selectTrigger(triggerKey)">
<a matLine>{{ triggerKey.name }}</a>
</mat-list-item>
}
</mat-nav-list>
</mat-card-content>
</mat-card>

View File

@@ -1,3 +1,23 @@
:host {
display: flex;
height: 100%;
min-height: 0;
width: 100%;
}
.trigger-list-card {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.trigger-list-content {
flex: 1;
min-height: 0;
position: relative;
}
/* ===== Scrollbar CSS ===== */
/* Firefox */
* {

View File

@@ -5,7 +5,7 @@ import {SimpleTrigger} from '../../model/simple-trigger.model';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
@Component({
template: `
template: `
<div style="padding:16px">
<h3 mat-dialog-title>Coming Soon</h3>
<div mat-dialog-content>
@@ -15,6 +15,7 @@ import {MatDialog, MatDialogRef} from '@angular/material/dialog';
<button mat-button (click)="closeDialog()" style="padding: 0.5em;width: 5em;">Ok</button>
</div>
</div>`,
standalone: false
})
// tslint:disable-next-line:component-class-suffix
export class UnsupportedMultipleJobsDialog {
@@ -26,9 +27,10 @@ export class UnsupportedMultipleJobsDialog {
}
@Component({
selector: 'qrzmng-trigger-list',
templateUrl: './trigger-list.component.html',
styleUrls: ['./trigger-list.component.scss']
selector: 'qrzmng-trigger-list',
templateUrl: './trigger-list.component.html',
styleUrls: ['./trigger-list.component.scss'],
standalone: false
})
export class TriggerListComponent implements OnInit {

View File

@@ -1,10 +1,10 @@
import {Injectable} from '@angular/core';
import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import {UserService} from '../services';
import {Observable} from 'rxjs';
@Injectable()
export class AdminGuard implements CanActivate {
export class AdminGuard {
constructor(private router: Router, private userService: UserService) {
}

View File

@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';
import { Router } from '@angular/router';
import { UserService } from '../services';
import { Observable } from 'rxjs';
@Injectable()
export class GuestGuard implements CanActivate {
export class GuestGuard {
constructor(private router: Router, private userService: UserService) {}

View File

@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';
import { Router } from '@angular/router';
import { UserService } from '../services';
import { Observable } from 'rxjs';
@Injectable()
export class LoginGuard implements CanActivate {
export class LoginGuard {
constructor(private router: Router, private userService: UserService) {}

View File

@@ -50,7 +50,8 @@ export const MisfireInstructionCaption = new Map<number, string>([
`In case of misfire event, the trigger is re-scheduled to the next scheduled time after 'now'
with the repeat count set to what it would be if it had not missed any firings.<br/>
Use this policy if no jobs must run after the end date time.<br/>
<strong>Warning</strong> The actual number of job executions could be less than initially set, because the misfired trigger are ignored.<br/>
<strong>Warning</strong> The actual number of job executions could be less than initially set,
because the misfired trigger are ignored.<br/>
This policy could cause the Trigger to go directly to the 'COMPLETE' state if all fire-times where missed.`
]
]);

View File

@@ -1,10 +1,8 @@
import {Moment} from 'moment/moment';
export class SimpleTriggerForm {
triggerName: string;
jobClass: string;
startDate: Moment;
endDate: Moment;
startDate: Date;
endDate: Date;
repeatCount: number;
repeatInterval: number;
misfireInstruction: string;

View File

@@ -1,74 +0,0 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/** *************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following to support `@angular/animation`. */
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** Evergreen browsers require these. **/
import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** *************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/** *************************************************************************************************
* APPLICATION IMPORTS
*/
/**
* Date, currency, decimal and percent pipes.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
// import 'intl'; // Run `npm install --save intl`.
/** *************************************************************************************************
* MATERIAL 2
*/
import 'hammerjs/hammer';

View File

@@ -1,4 +1,4 @@
import {HttpClient, HttpHeaders, HttpResponse, HttpRequest, HttpEventType, HttpParams} from '@angular/common/http';
import { HttpClient, HttpHeaders, HttpResponse, HttpRequest, HttpEventType, HttpParams } from '@angular/common/http';
import {Router} from '@angular/router';
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';

View File

@@ -1,5 +1,5 @@
import {Injectable} from '@angular/core';
import {HttpHeaders, HttpResponse} from '@angular/common/http';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import {ApiService} from './api.service';
import {UserService} from './user.service';
import {ConfigService} from './config.service';

View File

@@ -1,5 +1,4 @@
import {RxStomp} from '@stomp/rx-stomp';
import {RxStompConfig} from '@stomp/rx-stomp/esm6/rx-stomp-config';
import {RxStomp, RxStompConfig} from '@stomp/rx-stomp';
export class RxStompService extends RxStomp {

View File

@@ -2,9 +2,10 @@ import {Injectable} from '@angular/core';
import {ApiService} from './api.service';
import {ConfigService} from './config.service';
import {map} from 'rxjs/operators'
import {HttpErrorResponse} from '@angular/common/http';
import {Router} from '@angular/router';
import {map} from 'rxjs/operators'
import { HttpErrorResponse } from '@angular/common/http';
import {Router} from '@angular/router';
import {firstValueFrom} from 'rxjs';
@Injectable()
export class UserService {
@@ -22,7 +23,7 @@ export class UserService {
refreshToken() {
this.apiService.get(this.config.refresh_token_url).subscribe(res => {
if (res.accessToken !== null) {
return this.getUserInfo().toPromise()
return firstValueFrom(this.getUserInfo())
.then(user => {
this.currentUser = user;
});

View File

@@ -1,4 +1,4 @@
<div fxLayout="column" fxLayoutAlign="center" style="text-align: center">
<div class="flex flex-column justify-center" style="text-align: center">
<div>
<div>
<p style="font-size: 4em; margin-bottom: 0">Unexpected Error</p>

View File

@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'qrzmng-generic-error',
templateUrl: './genericError.component.html',
styleUrls: ['./genericError.component.css']
selector: 'qrzmng-generic-error',
templateUrl: './genericError.component.html',
styleUrls: ['./genericError.component.css'],
standalone: false
})
export class GenericErrorComponent implements OnInit {

View File

@@ -1,4 +1,4 @@
<div fxLayout="column" fxLayoutAlign="center" style="text-align: center">
<div class="flex flex-column justify-center" style="text-align: center">
<div>
<div>
<p style="font-size: 4em; margin-bottom: 0">Forbidden - Access Senied</p>

View File

@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-forbidden',
templateUrl: './forbidden.component.html',
styleUrls: ['./forbidden.component.css']
selector: 'app-forbidden',
templateUrl: './forbidden.component.html',
styleUrls: ['./forbidden.component.css'],
standalone: false
})
export class ForbiddenComponent implements OnInit {

View File

@@ -1,32 +1,46 @@
<div class="content" fxLayout="row" fxLayoutAlign="center" style="padding-bottom:160px;">
<mat-card elevation="5" fxFlex>
<mat-card-subtitle>
<h2>Quartz Manager</h2>
</mat-card-subtitle>
<mat-card-title>
<h2>{{title}}</h2>
</mat-card-title>
<mat-card-content>
<p [class]="notification.msgType" *ngIf="notification">{{notification.msgBody}}</p>
<form *ngIf="!submitted" [formGroup]="form" (ngSubmit)="onSubmit()" #loginForm="ngForm">
<mat-form-field>
<input matInput formControlName="username" required placeholder="user">
</mat-form-field>
<mat-form-field>
<input matInput formControlName="password" required type="password" placeholder="password">
</mat-form-field>
<button type="submit" [disabled]="!loginForm.form.valid" mat-raised-button color="primary">Login</button>
</form>
<mat-spinner *ngIf="submitted" mode="indeterminate"></mat-spinner>
</mat-card-content>
</mat-card>
</div>
<div
class="content flex flex-row justify-center"
style="padding-bottom: 160px">
<mat-card elevation="5" class="flex-1">
<mat-card-subtitle>
<h2>Quartz Manager</h2>
</mat-card-subtitle>
<mat-card-title>
<h2>{{ title }}</h2>
</mat-card-title>
<mat-card-content>
@if (notification) {
<p [class]="notification.msgType">{{ notification.msgBody }}</p>
} @if (!submitted) {
<form [formGroup]="form" (ngSubmit)="onSubmit()" #loginForm="ngForm">
<mat-form-field>
<input
matInput
formControlName="username"
required
placeholder="user" />
</mat-form-field>
<mat-form-field>
<input
matInput
formControlName="password"
required
type="password"
placeholder="password" />
</mat-form-field>
<button
type="submit"
[disabled]="!loginForm.form.valid"
mat-raised-button
color="primary">
Login
</button>
</form>
} @if (submitted) {
<mat-spinner mode="indeterminate"></mat-spinner>
}
</mat-card-content>
</mat-card>
</div>

View File

@@ -7,10 +7,11 @@ import {delay, takeUntil} from 'rxjs/operators'
import {AuthService, UserService} from '../../services';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
standalone: false
})
export class LoginComponent implements OnInit, OnDestroy {
title = 'Login';

View File

@@ -5,20 +5,20 @@
<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>
</div>
<div class="flex h-100">
<qrzmng-trigger-list
(onNewTriggerClicked)="onNewTriggerRequested()"
[openedNewTriggerForm]="newTriggerFormOpened"
(onSelectedTrigger)="setSelectedTrigger($event)"
class="h-100 w-100"></qrzmng-trigger-list>
</div>
</div>
<div class="flex-1" style="max-width: 350px">
<div fxLayout="row" fxFill>
<div fxLayout="column" fxFill>
<qrzmng-simple-trigger-config
fxFill
<div class="flex h-100">
<div class="flex flex-column h-100 w-100">
<qrzmng-simple-trigger-config
class="h-100 w-100"
[triggerKey]="selectedTriggerKey"
(triggerFormOpenChange)="setNewTriggerFormOpened($event)"
(onTriggerSubmitting)="monitorTrigger($event)"

View File

@@ -1,15 +1,16 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { AfterViewInit, 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',
templateUrl: './manager.component.html',
styleUrls: ['./manager.component.scss']
selector: 'manager',
templateUrl: './manager.component.html',
styleUrls: ['./manager.component.scss'],
standalone: false
})
export class ManagerComponent implements OnInit {
export class ManagerComponent implements OnInit, AfterViewInit {
@ViewChild(SimpleTriggerConfigComponent)
private triggerConfigComponent!: SimpleTriggerConfigComponent;
@@ -22,19 +23,34 @@ export class ManagerComponent implements OnInit {
monitoredTriggerKey: TriggerKey;
private pendingNewTriggerRequest = false;
constructor() {}
ngOnInit() {}
ngAfterViewInit() {
if (this.pendingNewTriggerRequest) {
queueMicrotask(() => this.openNewTriggerForm());
}
}
onNewTriggerRequested() {
this.selectedTriggerKey = null;
this.monitoredTriggerKey = null;
this.newTriggerFormOpened = true;
if (this.triggerConfigComponent) {
this.triggerConfigComponent.openNewTriggerForm();
this.openNewTriggerForm();
} else {
this.pendingNewTriggerRequest = true;
}
}
private openNewTriggerForm() {
this.newTriggerFormOpened = true;
this.pendingNewTriggerRequest = false;
this.triggerConfigComponent.openNewTriggerForm();
}
onNewTriggerCreated(newTrigger: SimpleTrigger) {
this.triggerListComponent.onNewTrigger(newTrigger);
this.newTriggerFormOpened = false;

View File

@@ -1,4 +1,4 @@
<div fxLayout="column" fxLayoutAlign="center" style="text-align: center">
<div class="flex flex-column justify-center" style="text-align: center">
<div>
<div>
<p style="font-size: 4em; margin-bottom: 0">Not Found!</p>

View File

@@ -1,7 +1,8 @@
import { Component } from '@angular/core';
@Component({
templateUrl: './not-found.component.html'
templateUrl: './not-found.component.html',
standalone: false
})
export class NotFoundComponent {

View File

@@ -1,4 +1,4 @@
import { enableProdMode } from '@angular/core';
import { enableProdMode, provideZoneChangeDetection } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
@@ -8,4 +8,6 @@ if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
platformBrowserDynamic().bootstrapModule(AppModule, {
applicationProviders: [provideZoneChangeDetection()],
});

View File

@@ -1,75 +1,3 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
import 'zone.js';
/** *************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following to support `@angular/animation`. */
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** Evergreen browsers require these. **/
import 'core-js/es6/reflect';
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** *************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
(window as any).global = window
/** *************************************************************************************************
* APPLICATION IMPORTS
*/
/**
* Date, currency, decimal and percent pipes.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
// import 'intl'; // Run `npm install --save intl`.
/** *************************************************************************************************
* MATERIAL 2
*/
import 'hammerjs/hammer';
(window as any).global = window;

View File

@@ -1,5 +1,6 @@
/* You can add global styles to this file, and also import other style files */
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
@import '@angular/material/prebuilt-themes/deeppurple-amber.css';
@import '@danielmoncada/angular-datetime-picker/assets/style/picker.min.css';
@import "animate.css";
html {
@@ -32,6 +33,22 @@ body {
justify-content: space-between;
}
.justify-space-around {
justify-content: space-around;
}
.justify-space-evenly {
justify-content: space-evenly;
}
.justify-center {
justify-content: center;
}
.justify-flex-end {
justify-content: flex-end;
}
.flex {
display: flex;
}
@@ -48,6 +65,10 @@ body {
flex: 1;
}
.flex-none {
flex: 0 0 auto;
}
.h-100 {
height: 100%;
}
@@ -68,6 +89,18 @@ body {
gap: 6px;
}
.gap-10 {
gap: 10px;
}
.gap-12 {
gap: 12px;
}
.gap-30 {
gap: 30px;
}
.overflow-hidden {
overflow: hidden;
}
@@ -100,3 +133,12 @@ body {
align-items: center;
}
.align-items-stretch {
align-items: stretch;
}
@media (max-width: 599px) {
.log-row {
flex-direction: column;
}
}

View File

@@ -7,17 +7,12 @@
"baseUrl": "src",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"moduleResolution": "bundler",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2022",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2016",
"dom"
],
"typeRoots": ["node_modules/@types"],
"module": "es2020",
"useDefineForClassFields": false
}