Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
488ead3e08 Bump http-cache-semantics in /quartz-manager-frontend
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-04 20:23:49 +00:00
98 changed files with 17608 additions and 6603 deletions

View File

@@ -1,2 +0,0 @@
# .dockerignore
quartz-manager-frontend/node_modules

View File

@@ -19,7 +19,7 @@ jobs:
with:
java-version: '11'
distribution: 'temurin'
server-id: maven-central-release
server-id: ossrh
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }}
@@ -31,8 +31,8 @@ jobs:
- name: Publish to maven central
run: mvn deploy --file quartz-manager-parent/pom.xml --batch-mode -P "release-maven-central,build-webjar"
env:
MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_TOKEN_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASSWORD }}
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }}
- name: Set up Java 11 for publishing to GitHub Packages

View File

@@ -7,28 +7,27 @@ on:
# paths: [ 'quartz-manager-parent/**' ]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
jobs:
build:
name: Build and analyze
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 17
uses: actions/setup-java@v3
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 17
distribution: 'zulu' # Alternative distribution options are available.
java-version: 11
- name: Cache SonarCloud packages
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache Maven packages
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}

3
.gitignore vendored
View File

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

View File

@@ -1,9 +1,3 @@
## **v4.0.10**
Migrated to the new maven central repo
## **v4.0.9**
Fixed a bug which prevented to run the liquibase migration scripts in case of usage of quartz-manager-starter-persistence
## **v4.0.8**
Upgraded the frontend to angular v14

View File

@@ -1,34 +0,0 @@
FROM maven:3.9.8-eclipse-temurin-21 AS build
# Set the working directory
WORKDIR /app
# Copy the pom.xml and download dependencies
COPY quartz-manager-parent/pom.xml ./quartz-manager-parent/
COPY quartz-manager-parent/lombok.config ./quartz-manager-parent/
COPY quartz-manager-parent/quartz-manager-common ./quartz-manager-parent/quartz-manager-common/
COPY quartz-manager-parent/quartz-manager-starter-api ./quartz-manager-parent/quartz-manager-starter-api/
COPY quartz-manager-parent/quartz-manager-starter-persistence ./quartz-manager-parent/quartz-manager-starter-persistence/
COPY quartz-manager-parent/quartz-manager-starter-security ./quartz-manager-parent/quartz-manager-starter-security/
COPY quartz-manager-parent/quartz-manager-starter-ui ./quartz-manager-parent/quartz-manager-starter-ui/
COPY quartz-manager-parent/quartz-manager-web-showcase ./quartz-manager-parent/quartz-manager-web-showcase/
COPY quartz-manager-parent/lombok.config ./quartz-manager-parent/
COPY quartz-manager-frontend ./quartz-manager-frontend/
WORKDIR /app/quartz-manager-parent
RUN mvn clean package -DskipTests -P=build-webjar
# Stage 2: Create the final image
FROM openjdk:11-jre-slim
# Set the working directory
WORKDIR /app
# Copy the JAR file from the build stage
COPY --from=build /app/quartz-manager-parent/quartz-manager-web-showcase/target/*-SNAPSHOT.jar app.jar
# Expose the application port
EXPOSE 8080
# Run the application
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -3,8 +3,6 @@
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/it.fabioformosa.quartz-manager/quartz-manager-starter-api/badge.svg)](https://maven-badges.herokuapp.com/maven-central/it.fabioformosa.quartz-manager/quartz-manager-starter-api)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=fabioformosa_quartz-manager&metric=coverage)](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=fabioformosa_quartz-manager&metric=bugs)](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=fabioformosa_quartz-manager&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=fabioformosa_quartz-manager&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager)
# Table Of Contents
[QUARTZ MANAGER](https://github.com/fabioformosa/quartz-manager#quartz-manager)
&nbsp;&nbsp;&nbsp;&nbsp;[Quartz Manager UI](https://github.com/fabioformosa/quartz-manager#quartz-manager-ui)
@@ -82,12 +80,12 @@ Add the dependency, make eligible for Quart Manager the job classes and set the
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-api</artifactId>
<version>4.0.9</version>
<version>4.0.8</version>
</dependency>
```
#### Gradle
```
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-api', version: '4.0.9'
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-api', version: '4.0.8'
```
### Step 2. Quartz Manager Job Classes
@@ -170,12 +168,12 @@ You can optionally import the following dependency to have the UI Dashboard to i
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-ui</artifactId>
<version>4.0.9</version>
<version>4.0.8</version>
</dependency>
```
#### Gradle
```
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-ui', version: '4.0.9'
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-ui', version: '4.0.8'
```
### Reach out the UI Console at URL
@@ -205,14 +203,14 @@ Future development: the Quart Manager Security OAuth2 client.
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-security</artifactId>
<version>4.0.9</version>
<version>4.0.8</version>
</dependency>
```
#### Gradle
```
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-security', version: '4.0.9'
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-security', version: '4.0.8'
```
@@ -242,14 +240,14 @@ The pre-requesite is the availability of Postgresql database where Quartz Manage
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-persistence</artifactId>
<version>4.0.9</version>
<version>4.0.8</version>
</dependency>
```
#### Gradle
```
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-persistence', version: '4.0.9'
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-persistence', version: '4.0.8'
```
### Quartz Manager Persistence Lib - App Props
@@ -271,7 +269,6 @@ You can find some examples of different scenarios of projects which import Quart
* *existing-security-header-based* - It demonstrates how Quartz Manager Security can coexists with another Spring Security Config present in your project. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Security.
* *existing-quartz* - It demonstrates how to Quartz Manager can coexist with a Quartz instance already present in your project Imported libraries: Quartz Manager API, Quartz Manager UI.
* *existing-quartz-and-storage* - It demonstrates how to Quartz Manager Persistence can coexist with a Quartz instance already present in your project. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Persistence.
* *with-persistence* - It demonstrates how to import the Quartz Manager Persistence and get created the quartz tables automatically at the bootstrap
## Limitations

View File

@@ -1,45 +0,0 @@
substitutions:
_REGION: europe-west8
steps:
# Step 1: Google Cloud Build - Docker build&push
- name: 'gcr.io/k8s-skaffold/skaffold'
entrypoint: 'sh'
args:
- -xe
- -c
- |
# Build and push images
sed -i s/_IMAGE_TAG_POLICY/$SHORT_SHA/g skaffold.yaml
sed -i s/_HELM_APP_VERSION/$SHORT_SHA/g ./quartz-manager-parent/quartz-manager-web-showcase/helm/Chart.yaml
skaffold build --file-output=/workspace/artifacts.json \
--default-repo=${_REGION}-docker.pkg.dev/quartz-manager-test/quartz-manager/quartz-manager-standalone \
--push=true
# Step 2: Google Cloud Deploy - deploy
- name: 'google/cloud-sdk:latest'
entrypoint: 'sh'
args:
- -xe
- -c
- |
sed -i s/_HELM_APP_VERSION/$SHORT_SHA/g ./quartz-manager-parent/quartz-manager-web-showcase/helm/Chart.yaml
gcloud config set deploy/region ${_REGION}
gcloud deploy apply --file ./quartz-manager-parent/quartz-manager-web-showcase/deploy/pipeline.yaml
gcloud deploy apply --file ./quartz-manager-parent/quartz-manager-web-showcase/deploy/dev.yaml
gcloud deploy apply --file ./quartz-manager-parent/quartz-manager-web-showcase/deploy/staging.yaml
gcloud deploy apply --file ./quartz-manager-parent/quartz-manager-web-showcase/deploy/prod.yaml
gcloud deploy releases create rel-${SHORT_SHA} \
--delivery-pipeline quartz-manager-pipeline \
--description "$(git log -1 --pretty='%s')" \
--build-artifacts /workspace/artifacts.json \
--verbosity=debug \
--annotations "commit_ui=https://source.cloud.google.com/$PROJECT_ID/quartz-manager-standalone/+/$COMMIT_SHA"
artifacts:
objects:
location: 'gs://$PROJECT_ID-gcdeploy-artifacts/'
paths:
- '/workspace/artifacts.json'
options:
logging: CLOUD_LOGGING_ONLY

View File

@@ -1,19 +0,0 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"plugin:sonarjs/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"plugins": [
"sonarjs"
],
"root": true
}

View File

@@ -3,7 +3,7 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"quartz-manager-ui": {
"angular-spring-starter": {
"root": "",
"prefix": "qrzmng",
"sourceRoot": "src",
@@ -19,27 +19,18 @@
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"allowedCommonJsDependencies": [
"stompjs", "sockjs-client", "moment", "angular2-uuid"
"stompjs", "sockjs-client", "moment"
],
"assets": [
"src/assets",
"src/favicon.ico"
],
"styles": [
"src/styles.css",
"node_modules/roboto-fontface/css/roboto/roboto-fontface.css"
"src/styles.css"
],
"scripts": []
},
"configurations": {
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"production": {
"budgets": [
{
@@ -67,18 +58,18 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "quartz-manager-ui:build:development"
"browserTarget": "angular-spring-starter:build"
},
"configurations": {
"production": {
"browserTarget": "quartz-manager-ui:build:production"
"browserTarget": "angular-spring-starter:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "quartz-manager-ui:build"
"browserTarget": "angular-spring-starter:build"
}
},
"lint": {
@@ -90,7 +81,7 @@
}
}
},
"quartz-manager-ui-e2e": {
"angular-spring-starter-e2e": {
"root": "e2e",
"sourceRoot": "e2e",
"projectType": "application",
@@ -99,7 +90,7 @@
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "quartz-manager-ui:serve"
"devServerTarget": "angular-spring-starter:serve"
}
},
"lint": {

File diff suppressed because it is too large Load Diff

View File

@@ -8,36 +8,33 @@
"build": "ng build --configuration production",
"test": "jest",
"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"
},
"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",
"@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",
"@auth0/angular-jwt": "5.1.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",
"@stomp/ng2-stompjs": "^0.6.3",
"core-js": "2.5.1",
"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",
@@ -45,16 +42,16 @@
"zone.js": "~0.12.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": "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",
"@types/hammerjs": "2.0.34",
"@types/jasmine": "2.5.54",
"@types/jasminewd2": "2.0.3",
@@ -63,12 +60,11 @@
"@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",
"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",
@@ -81,9 +77,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.9.5"
"typescript": "4.6.4"
},
"jest": {
"preset": "jest-preset-angular",

View File

@@ -13,8 +13,8 @@ import {
} 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;
declare var __karma__: any;
declare var require: any;
// Prevent Karma from running prematurely.
__karma__.loaded = function () {};

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 flex h-100">
<div class="content" fxFlex="100" fxFill>
<router-outlet></router-outlet>
</div>
<app-footer fxFlex="0 0 auto"></app-footer>

View File

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

View File

@@ -21,7 +21,6 @@ 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';
@@ -44,8 +43,7 @@ import {
SchedulerControlComponent,
LogsPanelComponent,
ProgressPanelComponent,
TriggerListComponent,
SimpleTriggerConfigComponent
TriggerListComponent
} from './components';
import {
@@ -54,13 +52,14 @@ import {
UserService,
SchedulerService,
ConfigService,
ProgressWebsocketService,
LogsWebsocketService,
getHtmlBaseUrl,
LogsRxWebsocketService,
ProgressRxWebsocketService,
TriggerService
} from './services';
import { ForbiddenComponent } from './views/forbidden/forbidden.component';
import { APP_BASE_HREF } from '@angular/common';
import {SimpleTriggerConfigComponent} from './components/simple-trigger-config';
import JobService from './services/job.service';
import {GenericErrorComponent} from './views/error/genericError.component';
@@ -68,6 +67,31 @@ export function initUserFactory(userService: UserService) {
return () => userService.fetchLoggedUser();
}
// const stompConfig: StompConfig = {
// // Which server?
// url: 'ws://localhost:8080/quartz-manager/progress',
// // Headers
// // Typical keys: login, passcode, host
// headers: {
// login: 'admin',
// passcode: 'admin'
// },
// // How often to heartbeat?
// // Interval in milliseconds, set to 0 to disable
// heartbeat_in: 0, // Typical value 0 - disabled
// heartbeat_out: 20000, // Typical value 20000 - every 20 seconds
// // Wait in milliseconds before attempting auto reconnect
// // Set to 0 to disable
// // Typical value 5000 (5 seconds)
// reconnect_delay: 5000,
// // Will log diagnostics on console
// debug: true
// };
export function jwtOptionsFactory(apiService: ApiService) {
return {
tokenGetter: () => {
@@ -109,7 +133,6 @@ export function jwtOptionsFactory(apiService: ApiService) {
deps: [ApiService]
}
}),
MatDialogModule,
MatMenuModule,
MatTooltipModule,
MatButtonModule,
@@ -145,13 +168,19 @@ export function jwtOptionsFactory(apiService: ApiService) {
SchedulerService,
JobService,
TriggerService,
ProgressRxWebsocketService,
LogsRxWebsocketService,
ProgressWebsocketService,
LogsWebsocketService,
AuthService,
ApiService,
UserService,
ConfigService,
MatIconRegistry
// StompService,
// ServerSocket
// {
// provide: StompConfig,
// useValue: stompConfig
// }
],
bootstrap: [AppComponent]
})

View File

@@ -1,8 +1,8 @@
<mat-toolbar id="footer" style="color: rgba(255, 255, 255, 0.541176);" fxLayout="row" fxLayoutAlign="center center">
<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>-->
<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>-->
</mat-toolbar>

View File

@@ -5,4 +5,3 @@ export * from './logs-panel';
export * from './scheduler-control';
export * from './progress-panel';
export * from './trigger-list';
export * from './simple-trigger-config';

View File

@@ -1,65 +1,35 @@
<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>
<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="!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>
</mat-card-content>
<div id="logs" style="overflow-y: auto; position: absolute; left: 0; right: 0; top: 0; bottom: 0; overflow: auto;">
<div
*ngFor = "let log of logs; let first = first" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start" fxLayoutGap="10px">
<div fxFlex="0 1 300px">
<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>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@@ -1,9 +1,3 @@
:host {
flex: 1;
display: flex;
flex-direction: column;
}
.red{
color: red;
}
@@ -15,18 +9,11 @@
color: gold;
}
#logs{
font-size: 1em;
}
.waitingLogs {
color: #6b7280;
height: 100%;
min-height: 180px;
text-align: center;
}
/* ===== Scrollbar CSS ===== */
#logs{
font-size: 1em;
}
/* ===== Scrollbar CSS ===== */
/* Firefox */
* {
scrollbar-width: auto;

View File

@@ -1,118 +0,0 @@
import {Subject} from 'rxjs';
import {LogsPanelComponent} from './logs-panel.component';
import {TriggerKey} from '../../model/triggerKey.model';
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, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
expect(logsRxWebsocketService.watch).toHaveBeenCalledWith('/topic/logs/trigger-1');
expect(component.selectedTriggerName).toEqual('trigger-1');
expect(component.isWaitingForLogs()).toBeTruthy();
const logRecord = {
date: new Date(),
type: 'INFO',
message: 'job completed',
threadName: 'worker-1'
};
messages.next({body: JSON.stringify(logRecord)});
expect(ngZone.run).toHaveBeenCalled();
expect(component.logs[0]).toEqual({
time: logRecord.date.toISOString(),
type: 'INFO',
msg: 'job completed',
threadName: 'worker-1'
});
expect(component.isWaitingForLogs()).toBeFalsy();
});
it('should unsubscribe from the previous topic when the trigger changes', () => {
const firstMessages = new Subject<any>();
const secondMessages = new Subject<any>();
const logsRxWebsocketService = {
watch: jest.fn()
.mockReturnValueOnce(firstMessages.asObservable())
.mockReturnValueOnce(secondMessages.asObservable())
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
const firstSubscription = component.topicSubscription;
jest.spyOn(firstSubscription, 'unsubscribe');
component.triggerKey = new TriggerKey('trigger-2', null);
expect(firstSubscription.unsubscribe).toHaveBeenCalled();
expect(logsRxWebsocketService.watch).toHaveBeenCalledWith('/topic/logs/trigger-2');
});
it('should clear logs when the trigger changes', () => {
const firstMessages = new Subject<any>();
const secondMessages = new Subject<any>();
const logsRxWebsocketService = {
watch: jest.fn()
.mockReturnValueOnce(firstMessages.asObservable())
.mockReturnValueOnce(secondMessages.asObservable())
.mockReturnValueOnce(firstMessages.asObservable())
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, 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'})});
expect(component.logs.length).toEqual(1);
component.triggerKey = new TriggerKey('trigger-2', null);
expect(component.logs).toEqual([]);
expect(component.selectedTriggerName).toEqual('trigger-2');
expect(component.isWaitingForLogs()).toBeTruthy();
secondMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'second log', threadName: 'worker-2'})});
expect(component.logs.length).toEqual(1);
component.triggerKey = new TriggerKey('trigger-1', null);
expect(component.logs).toEqual([]);
expect(component.selectedTriggerName).toEqual('trigger-1');
expect(component.isWaitingForLogs()).toBeTruthy();
});
it('should clear logs when no trigger is selected', () => {
const messages = new Subject<any>();
const logsRxWebsocketService = {
watch: jest.fn(() => messages.asObservable())
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, 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'})});
component.triggerKey = null;
expect(component.logs).toEqual([]);
expect(component.selectedTriggerName).toBeNull();
expect(component.isWaitingForLogs()).toBeFalsy();
});
it('should ignore destroy when no topic was selected', () => {
const logsRxWebsocketService = {
watch: jest.fn()
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
expect(() => component.ngOnDestroy()).not.toThrow();
});
});

View File

@@ -1,85 +1,42 @@
import {Component, Input, NgZone, OnDestroy, OnInit} from '@angular/core';
import {ApiService} from '../../services';
import {LogsRxWebsocketService} from '../../services/logs.rx-websocket.service';
import {map} from 'rxjs/operators';
import {TriggerKey} from '../../model/triggerKey.model';
import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core';
import {LogsWebsocketService, ApiService} from '../../services';
import {Observable} from 'rxjs';
@Component({
selector: 'logs-panel',
templateUrl: './logs-panel.component.html',
styleUrls: ['./logs-panel.component.scss']
})
export class LogsPanelComponent implements OnInit, OnDestroy {
export class LogsPanelComponent implements OnInit {
MAX_LOGS = 30;
logs = new Array();
selectedTriggerName: string;
logs = new Array();
topicSubscription;
private selectedTriggerKey: TriggerKey;
constructor(
private logsRxWebsocketService: LogsRxWebsocketService,
private apiService: ApiService,
private ngZone: NgZone
) {
}
@Input()
set triggerKey(triggerKey: TriggerKey) {
if (!triggerKey || !triggerKey.name) {
this._unsubscribeFromTopic();
this.selectedTriggerKey = null;
this.selectedTriggerName = null;
this._resetLogs();
return;
}
if (this.selectedTriggerKey?.name === triggerKey.name) {
return;
}
this._resetLogs();
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
this.selectedTriggerName = triggerKey.name;
this._subscribeToTheTopic(this.selectedTriggerKey);
}
isWaitingForLogs = (): boolean => !!this.selectedTriggerName && (!this.logs || this.logs.length === 0);
ngOnInit() {
constructor(
private logsWebsocketService: LogsWebsocketService,
private apiService: ApiService
) {
}
private _subscribeToTheTopic = (triggerKey: TriggerKey) => {
this._unsubscribeFromTopic();
this.topicSubscription = this.logsRxWebsocketService.watch(`/topic/logs/${triggerKey.name}`)
.pipe(map((msg: any) => JSON.parse(msg.body)))
.subscribe(logRecord => this.ngZone.run(() => this._showNewLog(logRecord)), (err) => {
console.log(err);
// TODO in case of 401
// this.apiService.get('/quartz-manager/session/refresh');
});
};
ngOnDestroy() {
this._unsubscribeFromTopic();
}
private _unsubscribeFromTopic() {
if (this.topicSubscription) {
this.topicSubscription.unsubscribe();
this.topicSubscription = null;
}
}
private _resetLogs() {
this.logs = [];
}
ngOnInit() {
const obs = this.logsWebsocketService.getObservable()
obs.subscribe({
'next': this.onNewLogMsg,
'error': (err) => {
console.log(err)
}
});
}
onNewLogMsg = (receivedMsg) => {
if (receivedMsg.type === 'SUCCESS') {
this._showNewLog(receivedMsg.message);
} else if (receivedMsg.type === 'ERROR') {
this._refreshSession();
} // if websocket has been closed for session expiration, try to refresh it
};
_showNewLog = (logRecord) => {
if (this.logs.length > this.MAX_LOGS) {

View File

@@ -6,8 +6,8 @@
</div>
</div> -->
<mat-card style="padding-bottom: 0" [ngClass]="{'progress-updated': progressUpdated}">
<mat-card-header style="padding-bottom: 16px;">
<mat-card style="padding-bottom: 0">
<mat-card-header>
<mat-card-subtitle><b>JOB PROGRESS</b></mat-card-subtitle>
</mat-card-header>
<mat-card-content>

View File

@@ -31,21 +31,3 @@
.fireBoxContent{
text-align: center;
}
.progress-updated {
animation: progressUpdatePulse 700ms ease-out;
}
@keyframes progressUpdatePulse {
0% {
box-shadow: 0 0 0 0 rgba(63, 81, 181, 0.35);
}
45% {
box-shadow: 0 0 0 6px rgba(63, 81, 181, 0.16);
}
100% {
box-shadow: 0 0 0 0 rgba(63, 81, 181, 0);
}
}

View File

@@ -1,107 +0,0 @@
import {Subject} from 'rxjs';
import {ProgressPanelComponent} from './progress-panel.component';
import {TriggerKey} from '../../model/triggerKey.model';
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, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
expect(progressRxWebsocketService.watch).toHaveBeenCalledWith('/topic/progress/trigger-1');
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();
jest.useRealTimers();
});
it('should unsubscribe from the previous topic when the trigger changes', () => {
const firstMessages = new Subject<any>();
const secondMessages = new Subject<any>();
const progressRxWebsocketService = {
watch: jest.fn()
.mockReturnValueOnce(firstMessages.asObservable())
.mockReturnValueOnce(secondMessages.asObservable())
};
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
const firstSubscription = component.topicSubscription;
jest.spyOn(firstSubscription, 'unsubscribe');
component.triggerKey = new TriggerKey('trigger-2', null);
expect(firstSubscription.unsubscribe).toHaveBeenCalled();
expect(progressRxWebsocketService.watch).toHaveBeenCalledWith('/topic/progress/trigger-2');
});
it('should reset progress when the trigger changes', () => {
const firstMessages = new Subject<any>();
const secondMessages = new Subject<any>();
const progressRxWebsocketService = {
watch: jest.fn()
.mockReturnValueOnce(firstMessages.asObservable())
.mockReturnValueOnce(secondMessages.asObservable())
.mockReturnValueOnce(firstMessages.asObservable())
};
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
firstMessages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
expect(component.progress.percentage).toEqual(75);
component.triggerKey = new TriggerKey('trigger-2', null);
expect(component.progress.percentage).toEqual(-1);
expect(component.percentageStr).toBeNull();
expect(component.progressUpdated).toBeFalsy();
secondMessages.next({body: JSON.stringify({percentage: 20, timesTriggered: 1})});
expect(component.progress.percentage).toEqual(20);
component.triggerKey = new TriggerKey('trigger-1', null);
expect(component.progress.percentage).toEqual(-1);
});
it('should reset progress when no trigger is selected', () => {
const messages = new Subject<any>();
const progressRxWebsocketService = {
watch: jest.fn(() => messages.asObservable())
};
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
component.triggerKey = new TriggerKey('trigger-1', null);
messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
component.triggerKey = null;
expect(component.progress.percentage).toEqual(-1);
expect(component.percentageStr).toBeNull();
expect(component.progressUpdated).toBeFalsy();
});
it('should ignore destroy when no topic was selected', () => {
const progressRxWebsocketService = {
watch: jest.fn()
};
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
expect(() => component.ngOnDestroy()).not.toThrow();
});
});

View File

@@ -1,92 +1,84 @@
import {Component, Input, NgZone, OnDestroy, OnInit} from '@angular/core'
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'
import {ProgressWebsocketService, QuartzManagerWebsocketMessage} from '../../services';
import { Observable } from 'rxjs';
import TriggerFiredBundle from '../../model/trigger-fired-bundle.model';
import {TriggerKey} from '../../model/triggerKey.model';
import {ProgressRxWebsocketService} from '../../services/progress.rx-websocket.service';
import {map} from 'rxjs/operators';
// import {Message} from '@stomp/stompjs';
// import { Subscription } from 'rxjs/Subscription';
// import {StompService} from '@stomp/ng2-stompjs';
// import { QueueingSubject } from 'queueing-subject'
// import websocketConnect from 'rxjs-websockets'
// import 'rxjs/add/operator/share'
// import {ServerSocket} from '../../services/qz.socket.service'
@Component({
selector: 'progress-panel',
templateUrl: './progress-panel.component.html',
styleUrls: ['./progress-panel.component.scss']
})
export class ProgressPanelComponent implements OnInit, OnDestroy {
progress: TriggerFiredBundle = ProgressPanelComponent._buildEmptyProgress();
percentageStr: string;
progressUpdated = false;
export class ProgressPanelComponent implements OnInit {
topicSubscription;
private selectedTriggerKey: TriggerKey;
progress: TriggerFiredBundle = new TriggerFiredBundle();
percentageStr: string;
constructor(
private progressRxWebsocketService: ProgressRxWebsocketService,
private ngZone: NgZone
) { }
// // Stream of messages
// private subscription: Subscription;
// public messages: Observable<Message>;
// // Subscription status
// public subscribed: boolean;
// // Array of historic message (bodies)
// public mq: Array<string> = [];
@Input()
set triggerKey(triggerKey: TriggerKey) {
if (!triggerKey || !triggerKey.name) {
this._unsubscribeFromTopic();
this.selectedTriggerKey = null;
this._resetProgress();
return;
}
if (this.selectedTriggerKey?.name === triggerKey.name) {
return;
}
this._resetProgress();
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
this._subscribeToTheTopic(this.selectedTriggerKey);
}
private _subscribeToTheTopic = (triggerKey: TriggerKey) => {
this._unsubscribeFromTopic();
this.topicSubscription = this.progressRxWebsocketService.watch(`/topic/progress/${triggerKey.name}`)
.pipe(map((msg: any) => JSON.parse(msg.body)))
.subscribe(progress => this.ngZone.run(() => this.onNewProgressMsg(progress)), (err) => {
console.log(err);
// TODO in case of 401
// this.apiService.get('/quartz-manager/session/refresh');
});
};
onNewProgressMsg = (receivedMsg) => {
this.progress = receivedMsg;
this.percentageStr = this.progress.percentage + '%';
this._markProgressUpdated();
}
constructor(
private progressWebsocketService: ProgressWebsocketService,
// private _stompService: StompService,
// private serverSocket : ServerSocket
) { }
onNewProgressMsg = (receivedMsg: QuartzManagerWebsocketMessage) => {
if (receivedMsg.type === 'SUCCESS') {
const newStatus = receivedMsg.message;
this.progress = newStatus;
this.percentageStr = this.progress.percentage + '%';
}
}
ngOnInit() {
}
ngOnDestroy() {
this._unsubscribeFromTopic();
}
private _unsubscribeFromTopic() {
if (this.topicSubscription) {
this.topicSubscription.unsubscribe();
this.topicSubscription = null;
}
}
private _resetProgress() {
this.progress = ProgressPanelComponent._buildEmptyProgress();
this.percentageStr = null;
this.progressUpdated = false;
}
private _markProgressUpdated() {
this.progressUpdated = false;
setTimeout(() => this.progressUpdated = true);
}
private static _buildEmptyProgress() {
const progress = new TriggerFiredBundle();
progress.percentage = -1;
return progress;
}
}
const obs = this.progressWebsocketService.getObservable()
obs.subscribe({
'next' : this.onNewProgressMsg,
'error' : (err) => {console.log(err)}
});
// this.subscribed = false;
// this.subscribe();
// this.serverSocket.connect()
// this.socketSubscription = this.serverSocket.messages.subscribe((message: string) => {
// console.log('received message from server: ', message)
// })
}
// public subscribe() {
// if (this.subscribed) {
// return;
// }
// // Stream of messages
// this.messages = this._stompService.subscribe('/topic/progress');
// // Subscribe a function to be run on_next message
// this.subscription = this.messages.subscribe(this.on_next);
// this.subscribed = true;
// }
// public on_next = (message: Message) => {
// this.mq.push(message.body + '\n');
// console.log(message);
// }
}

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" class="justify-space-between">
<div fxLayout="column">
<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" class="justify-space-between">
<div fxLayout="column">
<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,12 +11,7 @@ label{
font-size: smaller;
}
#scheduler-name{
text-transform: capitalize;
font-size: 14px;
}
#scheduler-instance {
text-transform: capitalize;
font-size: 14px;
}
#scheduler-name{
text-transform: capitalize;
font-size: larger;
}

View File

@@ -13,12 +13,6 @@ import {MatDividerModule} from '@angular/material/divider';
describe('SchedulerControlComponent', () => {
const schedulerUrl = '/quartz-manager/scheduler';
const schedulerButtonSelector = '#schedulerControllerBtn';
const schedulerName = 'test-scheduler';
const schedulerId = 'test-id';
const stoppedStatus = 'STOPPED';
let component: SchedulerControlComponent;
let fixture: ComponentFixture<SchedulerControlComponent>;
@@ -44,16 +38,16 @@ describe('SchedulerControlComponent', () => {
it('should display the play button at the beginning since the scheduler is stopped', () => {
expect(component).toBeDefined();
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
const mockScheduler = new Scheduler(schedulerName, schedulerId, stoppedStatus, []);
const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'STOPPED', []);
getSchedulerReq.flush(mockScheduler);
expect(component.scheduler).toEqual(mockScheduler);
expect(component.scheduler.status).toEqual(stoppedStatus);
expect(component.scheduler.status).toEqual('STOPPED');
fixture.detectChanges();
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
const schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
const schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
expect(schedulerBtnDe).toBeTruthy();
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
@@ -62,13 +56,13 @@ describe('SchedulerControlComponent', () => {
it('should switch the button to pause when the scheduler is started', () => {
expect(component).toBeDefined();
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
const mockScheduler = new Scheduler(schedulerName, schedulerId, stoppedStatus, []);
const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'STOPPED', []);
getSchedulerReq.flush(mockScheduler);
fixture.detectChanges();
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
let schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
let schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
expect(schedulerBtnDe).toBeTruthy();
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
expect(playIconDe).toBeTruthy();
@@ -78,7 +72,7 @@ describe('SchedulerControlComponent', () => {
startSchedulerReq.flush(null);
fixture.detectChanges();
schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
const pauseIconDe = schedulerBtnDe.query(By.css('.fa-pause'));
expect(pauseIconDe).toBeTruthy();
@@ -86,13 +80,13 @@ describe('SchedulerControlComponent', () => {
it('should switch the button to play when the scheduler is stopped', () => {
expect(component).toBeDefined();
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
const mockScheduler = new Scheduler(schedulerName, schedulerId, 'RUNNING', []);
const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'RUNNING', []);
getSchedulerReq.flush(mockScheduler);
fixture.detectChanges();
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
let schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
let schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
expect(schedulerBtnDe).toBeTruthy();
const pauseIconDe = schedulerBtnDe.query(By.css('.fa-pause'));
expect(pauseIconDe).toBeTruthy();
@@ -102,7 +96,7 @@ describe('SchedulerControlComponent', () => {
startSchedulerReq.flush(null);
fixture.detectChanges();
schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
expect(playIconDe).toBeTruthy();

View File

@@ -1,11 +1,11 @@
<mat-card fxFlex="1 1 auto">
<mat-card-header style="padding-bottom: 16px;">
<mat-card-header>
<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;">
overflow: auto;height: calc(100% - 3em); padding-top: 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,13 +14,15 @@
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%"
<form name="triggerConfigForm" 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">
@@ -31,11 +33,12 @@
<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">
<mat-option *ngFor="let job of jobs" [value]="job" class="font-13">
<mat-select id="jobClass" name="jobClass" formControlName="jobClass" [disabled]="!enabledTriggerForm">
<mat-option *ngFor="let job of jobs" [value]="job" style="font-size: 0.8em">
{{job}}
</mat-option>
</mat-select>
@@ -47,21 +50,23 @@
<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">
<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
<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
EXISTING REPEAT COUNT
</mat-option>
<mat-option class="font-13" value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT">RESCHEDULE NOW WITH
<mat-option 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
<mat-option 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
<mat-option value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT">RESCHEDULE NEXT WITH EXISTING
COUNT
</mat-option>
</mat-select>
@@ -77,10 +82,12 @@
<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">
@@ -92,10 +99,12 @@
<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"
@@ -113,10 +122,12 @@
<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"
>
@@ -127,10 +138,12 @@
</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"
>
@@ -145,23 +158,26 @@
<br/>
<div fxLayout="row" fxFlexAlign="space-evenly center" style="padding-bottom: 1em;">
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="simpleTriggerReactiveForm.enabled">
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="enabledTriggerForm">
<button mat-raised-button
type="button"
*ngIf="enabledTriggerForm"
(click)="onResetReactiveForm()">
Cancel
</button>
</div>
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="simpleTriggerReactiveForm.enabled">
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="enabledTriggerForm">
<button mat-raised-button
type="submit" color="primary"
[disabled]="simpleTriggerReactiveForm.invalid">
[disabled]="simpleTriggerReactiveForm.invalid"
*ngIf="enabledTriggerForm">
Submit
</button>
</div>
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="!simpleTriggerReactiveForm.enabled">
<div fxFlex="1 1 auto" style="text-align: center" *ngIf="!enabledTriggerForm">
<button mat-raised-button type="button"
(click)="openTriggerForm();simpleTriggerReactiveForm.controls['triggerName'].disable();">
*ngIf="!enabledTriggerForm"
(click)="enabledTriggerForm = true">
Reschedule
</button>
</div>

View File

@@ -5,22 +5,6 @@
.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, fakeAsync, flush, TestBed, waitForAsync} from '@angular/core/testing';
import {ComponentFixture, 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';
@@ -23,11 +23,6 @@ import {MisfireInstruction} from '../../model/misfire-instruction.model';
describe('SimpleTriggerConfig', () => {
const submitButtonSelector = 'form button[color="primary"]';
const repeatIntervalSelector = '#repeatInterval';
const testTriggerName = 'test-trigger';
const testJobName = 'TestJob';
let component: SimpleTriggerConfigComponent;
let fixture: ComponentFixture<SimpleTriggerConfigComponent>;
@@ -86,7 +81,7 @@ describe('SimpleTriggerConfig', () => {
const dropdownDe = componentDe.query(By.css(dropdownSelector));
dropdownDe.nativeElement.click();
fixture.detectChanges();
const matOptionDe = componentDe.query(By.css('.mat-mdc-select-panel')).queryAll(By.css('.mat-mdc-option'));
const matOptionDe = componentDe.query(By.css('.mat-select-panel')).queryAll(By.css('.mat-option'));
matOptionDe[index].nativeElement.click();
fixture.detectChanges();
}
@@ -96,16 +91,16 @@ describe('SimpleTriggerConfig', () => {
fixture.detectChanges();
const getJobsReq = httpTestingController.expectOne(`${CONTEXT_PATH}/jobs`);
getJobsReq.flush([testJobName]);
getJobsReq.flush(['TestJob']);
const componentDe: DebugElement = fixture.debugElement;
const submitButton = componentDe.query(By.css(submitButtonSelector));
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
setInputValue(componentDe, '#triggerName', testTriggerName);
expect(component.simpleTriggerReactiveForm.controls.triggerName.value).toEqual(testTriggerName);
setInputValue(componentDe, '#triggerName', 'test-trigger');
expect(component.simpleTriggerReactiveForm.controls.triggerName.value).toEqual('test-trigger');
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
setMatSelectValueByIndex(componentDe, '#misfireInstruction', 0);
expect(component.simpleTriggerReactiveForm.controls.misfireInstruction.value).toEqual('MISFIRE_INSTRUCTION_FIRE_NOW');
@@ -116,7 +111,7 @@ describe('SimpleTriggerConfig', () => {
setInputValue(componentDe, '#repeatCount', '1000');
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
setInputValue(componentDe, repeatIntervalSelector, '2000');
setInputValue(componentDe, '#repeatInterval', '2000');
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual(null);
}
@@ -124,54 +119,50 @@ describe('SimpleTriggerConfig', () => {
openFormAndFillAllMandatoryFields();
});
it('should emit an event when a new trigger is submitted', fakeAsync(() => {
it('should emit an event when a new trigger is submitted', () => {
const componentDe: DebugElement = fixture.debugElement;
const mockTrigger = new Trigger();
mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null);
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: testJobName, description: null};
mockTrigger.triggerKeyDTO = new TriggerKey('test-trigger', null);
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: 'TestJob', description: null};
mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
openFormAndFillAllMandatoryFields();
setInputValue(componentDe, repeatIntervalSelector, '2000');
setInputValue(componentDe, '#repeatInterval', '2000');
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatInterval).toEqual(2000);
setInputValue(componentDe, '#repeatCount', '100');
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatCount).toEqual(100);
const submitButton = componentDe.query(By.css(submitButtonSelector));
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
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}`);
const postSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
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);
const mockTriggerKey = new TriggerKey('test-trigger', null);
component.triggerKey = mockTriggerKey;
fixture.detectChanges();
const mockTrigger = new SimpleTrigger();
mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null);
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: testJobName, description: null};
mockTrigger.triggerKeyDTO = new TriggerKey('test-trigger', null);
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: 'TestJob', description: null};
mockTrigger.mayFireAgain = true;
mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
getSimpleTriggerReq.flush(mockTrigger);
component.simpleTriggerReactiveForm.setValue({
triggerName: testTriggerName,
jobClass: testJobName,
triggerName: 'test-trigger',
jobClass: 'TestJob',
triggerRecurrence: {
repeatInterval: 2000,
repeatCount: 100,
@@ -187,10 +178,10 @@ describe('SimpleTriggerConfig', () => {
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
setInputValue(componentDe, repeatIntervalSelector, '4000');
setInputValue(componentDe, '#repeatInterval', '4000');
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatInterval).toEqual(4000);
const submitButton = componentDe.query(By.css(submitButtonSelector));
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
let actualNewTrigger;
@@ -198,7 +189,7 @@ describe('SimpleTriggerConfig', () => {
submitButton.nativeElement.click();
const putSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
const putSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
putSimpleTriggerReq.flush(mockTrigger);
expect(actualNewTrigger).toBeUndefined();
@@ -211,14 +202,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');
@@ -229,44 +220,8 @@ describe('SimpleTriggerConfig', () => {
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
const submitButton = componentDe.query(By.css(submitButtonSelector));
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
expect(component.simpleTriggerReactiveForm.value.triggerName).toBeNull();
});
it('should reset the form when a new trigger is selected', () => {
const mockTriggerKey = new TriggerKey(testTriggerName, null);
component.triggerKey = mockTriggerKey;
const mockTrigger = new SimpleTrigger();
mockTrigger.triggerKeyDTO = mockTriggerKey;
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: testJobName, description: null};
mockTrigger.mayFireAgain = true;
mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
getSimpleTriggerReq.flush(mockTrigger);
expect(component.simpleTriggerReactiveForm.value.triggerName).toEqual(testTriggerName);
component.openNewTriggerForm();
expect(component.simpleTriggerReactiveForm.value.triggerName).toBeNull();
expect(component.simpleTriggerReactiveForm.value.jobClass).toBeNull();
expect(component.shouldShowTheTriggerCardContent()).toBeTruthy();
});
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', () => {

View File

@@ -34,23 +34,20 @@ export class SimpleTriggerConfigComponent implements OnInit {
scheduler: Scheduler;
triggerLoading = false;
triggerLoading = true;
private fetchedTriggers = false;
private triggerInProgress = false;
private selectedTriggerKey: TriggerKey;
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,
@@ -59,7 +56,6 @@ export class SimpleTriggerConfigComponent implements OnInit {
}
ngOnInit() {
this.simpleTriggerReactiveForm.disable();
this.fetchJobs();
}
@@ -68,38 +64,19 @@ export class SimpleTriggerConfigComponent implements OnInit {
}
openTriggerForm() {
this.simpleTriggerReactiveForm.enable();
this.triggerFormOpenChange.emit(true);
this.enabledTriggerForm = true;
}
private closeTriggerForm() {
this.simpleTriggerReactiveForm.disable();
this.triggerFormOpenChange.emit(false);
this.enabledTriggerForm = false;
}
@Input()
set triggerKey(triggerKey: TriggerKey) {
if (!triggerKey) {
return;
} else if (!this.selectedTriggerKey || this.selectedTriggerKey.name !== triggerKey.name) {
this._resetTheTrigger();
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
this.fetchSelectedTrigger();
this.simpleTriggerReactiveForm.disable();
}
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
this.fetchSelectedTrigger();
}
openNewTriggerForm() {
this._resetTheTrigger();
this.openTriggerForm();
}
private _resetTheTrigger() {
this.trigger = null;
this.triggerInProgress = false;
this.selectedTriggerKey = null;
this.simpleTriggerReactiveForm.reset(new SimpleTriggerReactiveForm());
}
fetchSelectedTrigger = () => {
this.triggerLoading = true;
@@ -109,20 +86,15 @@ 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.simpleTriggerReactiveForm.enabled;
shouldShowTheTriggerCardContent = (): boolean => this.trigger !== null || this.enabledTriggerForm;
existsATriggerInProgress = (): boolean => this.trigger && this.triggerInProgress;
onResetReactiveForm = () => {
if (this.trigger) {
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
} else {
this.simpleTriggerReactiveForm.reset(new SimpleTriggerReactiveForm());
}
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
this.closeTriggerForm();
};
@@ -131,24 +103,13 @@ 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) => {
this.trigger = retTrigger;
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(retTrigger));
this.fetchedTriggers = true;
this.triggerInProgress = this.trigger.mayFireAgain;
if (schedulerServiceCall === this.schedulerService.saveSimpleTriggerConfig) {
@@ -157,13 +118,9 @@ export class SimpleTriggerConfigComponent implements OnInit {
this.closeTriggerForm();
}, error => {
if (this.trigger) {
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
}
this.triggerLoading = false;
}, () => {
this.triggerLoading = false;
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
});
}
private _triggerPeriodValidator(control: AbstractControl): ValidationErrors | null {
@@ -204,7 +161,7 @@ export class SimpleTriggerConfigComponent implements OnInit {
};
private _fromReactiveFormToCommand = (): SimpleTriggerCommand => {
const reactiveFormValue = this.simpleTriggerReactiveForm.getRawValue();
const reactiveFormValue = this.simpleTriggerReactiveForm.value;
const simpleTriggerCommand = new SimpleTriggerCommand();
simpleTriggerCommand.triggerName = reactiveFormValue.triggerName;
simpleTriggerCommand.jobClass = reactiveFormValue.jobClass;

View File

@@ -9,9 +9,7 @@
<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)"
>
[ngClass]="{'selectedTrigger': selectedTrigger && selectedTrigger.name==triggerKey.name}">
<a matLine>{{ triggerKey.name }}</a>
</mat-list-item>
</mat-nav-list>

View File

@@ -6,14 +6,12 @@ import {MatDialog, MatDialogRef} from '@angular/material/dialog';
@Component({
template: `
<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>
<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>`,
})
// tslint:disable-next-line:component-class-suffix
@@ -83,17 +81,16 @@ export class TriggerListComponent implements OnInit {
}
onNewTriggerBtnClicked() {
this.onNewTriggerClicked.emit();
// if (this.getTriggerKeyList() && this.getTriggerKeyList().length > 0) {
// this.dialog.open(UnsupportedMultipleJobsDialog)
// } else {
// this.onNewTriggerClicked.emit();
// }
if (this.getTriggerKeyList() && this.getTriggerKeyList().length > 0) {
this.dialog.open(UnsupportedMultipleJobsDialog)
} else {
this.onNewTriggerClicked.emit();
}
}
onNewTrigger(newTrigger: SimpleTrigger) {
this.newTriggers = [newTrigger, ...this.newTriggers];
this.selectTrigger(newTrigger.triggerKeyDTO);
this.selectedTrigger = newTrigger.triggerKeyDTO;
}
}

View File

@@ -14,7 +14,7 @@
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/** *************************************************************************************************
/***************************************************************************************************
* BROWSER POLYFILLS
*/
@@ -51,14 +51,14 @@ import 'core-js/es7/reflect';
/** *************************************************************************************************
/***************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/** *************************************************************************************************
/***************************************************************************************************
* APPLICATION IMPORTS
*/
@@ -68,7 +68,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
*/
// import 'intl'; // Run `npm install --save intl`.
/** *************************************************************************************************
/***************************************************************************************************
* MATERIAL 2
*/
import 'hammerjs/hammer';

View File

@@ -3,8 +3,9 @@ export * from './user.service';
export * from './config.service';
export * from './auth.service';
export * from './scheduler.service';
export * from './progress.rx-websocket.service';
export * from './logs.rx-websocket.service';
export * from './websocket.service';
export * from './progress.websocket.service';
export * from './logs.websocket.service';
export * from './trigger.service'
export * from './job.service'

View File

@@ -1,39 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { LogsRxWebsocketService } from './logs.rx-websocket.service';
import {ApiService} from './api.service';
import {RxStomp} from '@stomp/rx-stomp';
import {jest} from '@jest/globals';
describe('LogsRxWebsocketService', () => {
let service: LogsRxWebsocketService;
let configureSpy;
let activateSpy;
beforeEach(() => {
configureSpy = jest.spyOn(RxStomp.prototype, 'configure');
activateSpy = jest.spyOn(RxStomp.prototype, 'activate').mockImplementation(() => undefined);
TestBed.configureTestingModule({
providers: [
{provide: ApiService, useValue: {getToken: () => 'test-token'}}
]
});
service = TestBed.inject(LogsRxWebsocketService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should configure rx-stomp with the logs websocket endpoint', () => {
expect(configureSpy).toHaveBeenCalled();
expect(activateSpy).toHaveBeenCalled();
const config = configureSpy.mock.calls[configureSpy.mock.calls.length - 1][0];
expect(config.heartbeatIncoming).toEqual(0);
expect(config.heartbeatOutgoing).toEqual(20000);
expect(config.reconnectDelay).toEqual(200);
expect(config.webSocketFactory.toString()).toContain('/logs?access_token=');
});
});

View File

@@ -1,23 +0,0 @@
import { Injectable } from '@angular/core';
import {RxStompService} from './rx-stomp.service';
import {ApiService} from './api.service';
import SockJS from 'sockjs-client';
import {CONTEXT_PATH, getBaseUrl} from './config.service';
@Injectable({
providedIn: 'root'
})
export class LogsRxWebsocketService extends RxStompService {
constructor(private apiService: ApiService) {
super({
webSocketFactory: () => new SockJS(`${getBaseUrl()}${CONTEXT_PATH}/logs?access_token=${this.apiService.getToken()}`),
heartbeatIncoming: 0,
heartbeatOutgoing: 20000,
reconnectDelay: 200,
debug: (msg: string): void => {
console.log(new Date(), msg);
}
});
}
}

View File

@@ -0,0 +1,12 @@
import {Injectable} from '@angular/core';
import {WebsocketService, ApiService, getBaseUrl, CONTEXT_PATH} from '.';
import {SocketOption} from '../model/SocketOption.model';
@Injectable()
export class LogsWebsocketService extends WebsocketService {
constructor(private apiService: ApiService) {
super(new SocketOption(getBaseUrl() + `${CONTEXT_PATH}/logs`, '/topic/logs', apiService.getToken))
}
}

View File

@@ -1,39 +0,0 @@
import { TestBed } from '@angular/core/testing';
import {RxStomp} from '@stomp/rx-stomp';
import {jest} from '@jest/globals';
import { ProgressRxWebsocketService } from './progress.rx-websocket.service';
import {ApiService} from './api.service';
describe('ProgressRxWebsocketService', () => {
let service: ProgressRxWebsocketService;
let configureSpy;
let activateSpy;
beforeEach(() => {
configureSpy = jest.spyOn(RxStomp.prototype, 'configure');
activateSpy = jest.spyOn(RxStomp.prototype, 'activate').mockImplementation(() => undefined);
TestBed.configureTestingModule({
providers: [
{provide: ApiService, useValue: {getToken: () => 'test-token'}}
]
});
service = TestBed.inject(ProgressRxWebsocketService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should configure rx-stomp with the progress websocket endpoint', () => {
expect(configureSpy).toHaveBeenCalled();
expect(activateSpy).toHaveBeenCalled();
const config = configureSpy.mock.calls[configureSpy.mock.calls.length - 1][0];
expect(config.heartbeatIncoming).toEqual(0);
expect(config.heartbeatOutgoing).toEqual(20000);
expect(config.reconnectDelay).toEqual(200);
expect(config.webSocketFactory.toString()).toContain('/progress?access_token=');
});
});

View File

@@ -1,23 +0,0 @@
import { Injectable } from '@angular/core';
import {RxStompService} from './rx-stomp.service';
import {ApiService} from './api.service';
import SockJS from 'sockjs-client';
import {CONTEXT_PATH, getBaseUrl} from './config.service';
@Injectable({
providedIn: 'root'
})
export class ProgressRxWebsocketService extends RxStompService {
constructor(private apiService: ApiService) {
super({
webSocketFactory: () => new SockJS(`${getBaseUrl()}${CONTEXT_PATH}/progress?access_token=${this.apiService.getToken()}`),
heartbeatIncoming: 0,
heartbeatOutgoing: 20000,
reconnectDelay: 200,
debug: (msg: string): void => {
console.log(new Date(), msg);
}
});
}
}

View File

@@ -0,0 +1,12 @@
import {Injectable} from '@angular/core';
import {WebsocketService, ApiService, getBaseUrl, CONTEXT_PATH} from '.';
import {SocketOption} from '../model/SocketOption.model';
@Injectable()
export class ProgressWebsocketService extends WebsocketService {
constructor(private apiService: ApiService) {
super(new SocketOption(getBaseUrl() + `${CONTEXT_PATH}/progress`, '/topic/progress', apiService.getToken))
}
}

View File

@@ -1,12 +0,0 @@
import {RxStomp} from '@stomp/rx-stomp';
import {RxStompConfig} from '@stomp/rx-stomp/esm6/rx-stomp-config';
export class RxStompService extends RxStomp {
constructor(rxStompConfig: RxStompConfig) {
super();
super.configure(rxStompConfig);
super.activate();
}
}

View File

@@ -35,7 +35,7 @@ export class UserService {
this.currentUser = user;
this.router.initialNavigation();
}, err => {
console.log('error retrieving current user due to ' + JSON.stringify(err));
console.log(`error retrieving current user due to ` + JSON.stringify(err));
const httpErrorResponse = err as HttpErrorResponse;
if (httpErrorResponse.status === 404) {
this.isAnAnonymousUser = true;

View File

@@ -0,0 +1,136 @@
import {Observable, Subscriber} from 'rxjs';
import {SocketEndpoint} from '../model/SocketEndpoint.model'
import Stomp from 'stompjs';
import SockJS from 'sockjs-client';
import {SocketOption} from '../model/SocketOption.model';
interface WebsocketSubscriber {
index: number,
observer: Subscriber<any>
}
export interface QuartzManagerWebsocketMessage {
type: string;
message: any;
headers: any;
self: boolean;
}
export class WebsocketService {
_options: SocketOption;
_socket: SocketEndpoint = new SocketEndpoint();
observableStompConnection: Observable<any>;
subscribers: Array<WebsocketSubscriber> = [];
subscriberIndex = 0;
_messageIds: Array<any> = [];
reconnectionPromise: any;
constructor(options: SocketOption) {
this._options = options
this.createObservableSocket();
this.connect();
}
getOptions = () => {
}
private createObservableSocket = () => {
this.observableStompConnection = new Observable((observer) => {
const subscriberIndex = this.subscriberIndex++;
this.addToSubscribers({index: subscriberIndex, observer});
return () => this.removeFromSubscribers(subscriberIndex);
});
}
private addToSubscribers = (subscriber) => {
this.subscribers.push(subscriber);
}
private removeFromSubscribers = (index) => {
this.subscribers = this.subscribers.filter(subscriber => subscriber.index !== index);
}
getObservable = () => {
return this.observableStompConnection;
};
getMessage = function (data): QuartzManagerWebsocketMessage {
const out: QuartzManagerWebsocketMessage = <QuartzManagerWebsocketMessage>{};
out.type = 'SUCCESS';
out.message = JSON.parse(data.body);
out.headers = {};
out.headers.messageId = data.headers['message-id'];
const messageIdIndex = this._messageIds.indexOf(out.headers.messageId);
if (messageIdIndex > -1) {
out.self = true;
this._messageIds = this._messageIds.splice(messageIdIndex, 1);
}
return out;
};
_socketListener = (frame) => {
console.log('Connected: ' + frame);
this._socket.stomp.subscribe(
this._options.topicName,
data => this.subscribers.forEach(subscriber => subscriber.observer.next(this.getMessage(data)))
);
}
_onSocketError = (errorMsg) => {
const out: any = {};
out.type = 'ERROR';
out.message = errorMsg;
this.subscribers.forEach(subscriber => subscriber.observer.error(out));
this.scheduleReconnection();
}
scheduleReconnection = () => {
this.reconnectionPromise = setTimeout(() => {
console.log('Socket reconnecting... (if it fails, next attempt in ' + this._options.reconnectionTimeout + ' msec)');
this.connect();
}, this._options.reconnectionTimeout);
}
reconnectNow = function () {
this._socket.stomp.disconnect();
if (this.reconnectionPromise && this.reconnectionPromise.cancel) {
this.reconnectionPromise.cancel();
}
this.connect();
};
send = (message) => {
const id = Math.floor(Math.random() * 1000000);
this._socket.stomp.send(this._options.brokerName, {
priority: 9
}, JSON.stringify({
message: message,
id: id
}));
this._messageIds.push(id);
};
connect = () => {
const headers = {};
let socketUrl = this._options.socketUrl;
if (this._options.getAccessToken()) {
socketUrl += `?access_token=${this._options.getAccessToken()}`;
}
this._socket.client = new SockJS(socketUrl);
this._socket.stomp = Stomp.over(this._socket.client);
this._socket.stomp.connect(headers, this._socketListener, this._onSocketError);
this._socket.stomp.onclose = this.scheduleReconnection;
}
}

View File

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

View File

@@ -1,48 +1,40 @@
<div id="managerViewContainer" class="flex flex-column flex-1 gap-6 h-100">
<div id="schedulerBarContainer">
<div id="managerViewContainer" fxLayout="column" fxLayoutAlign="left stretch" fxLayoutGap="10px" fxFill>
<div id="schedulerBarContainer" fxLayout="column" fxLayoutAlign="left stretch">
<qrzmng-scheduler-control></qrzmng-scheduler-control>
</div>
<div id="manager-content-container" class="flex flex-row flex-1 gap-6">
<div class="flex-1" style="max-width: 250px">
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="center stretch" fxFlex="1 1 auto">
<div fxFlex="0 0 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 class="flex-1" style="max-width: 350px">
<div fxLayout="row" fxFill>
<div fxFlex="1 1 350px">
<div fxLayout="row" fxFill>
<div fxLayout="column" fxFill>
<qrzmng-simple-trigger-config
fxFill
[triggerKey]="selectedTriggerKey"
(triggerFormOpenChange)="setNewTriggerFormOpened($event)"
(onTriggerSubmitting)="monitorTrigger($event)"
(onNewTrigger)="onNewTriggerCreated($event)">
</qrzmng-simple-trigger-config>
<qrzmng-simple-trigger-config fxFill
[triggerKey]="selectedTriggerKey"
(onNewTrigger)="onNewTriggerCreated($event)">
</qrzmng-simple-trigger-config>
</div>
</div>
</div>
</div>
<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 fxFlex="1 1 auto" style="margin-left: 20px;">
<div fxFlex="1 1 auto" fxLayout="column" fxLayoutAlign="start stretch" fxLayoutGap="6px">
<progress-panel></progress-panel>
<logs-panel fxFlex="1 1 auto" fxFill></logs-panel>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,10 +1 @@
: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,12 @@
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 {
ConfigService,
UserService
} from '../../services';
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,6 +14,7 @@ import { TriggerListComponent } from '../../components';
styleUrls: ['./manager.component.scss']
})
export class ManagerComponent implements OnInit {
@ViewChild(SimpleTriggerConfigComponent)
private triggerConfigComponent!: SimpleTriggerConfigComponent;
@@ -20,37 +25,22 @@ export class ManagerComponent implements OnInit {
selectedTriggerKey: TriggerKey;
monitoredTriggerKey: TriggerKey;
constructor(
) { }
constructor() {}
ngOnInit() {}
ngOnInit() {
}
onNewTriggerRequested() {
this.selectedTriggerKey = null;
this.monitoredTriggerKey = null;
this.newTriggerFormOpened = true;
if (this.triggerConfigComponent) {
this.triggerConfigComponent.openNewTriggerForm();
}
this.triggerConfigComponent.openTriggerForm();
}
onNewTriggerCreated(newTrigger: SimpleTrigger) {
this.triggerListComponent.onNewTrigger(newTrigger);
this.newTriggerFormOpened = false;
}
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

@@ -14,7 +14,7 @@
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/** *************************************************************************************************
/***************************************************************************************************
* BROWSER POLYFILLS
*/
@@ -50,7 +50,7 @@ import 'core-js/es6/reflect';
/** *************************************************************************************************
/***************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
@@ -59,7 +59,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
/** *************************************************************************************************
/***************************************************************************************************
* APPLICATION IMPORTS
*/
@@ -69,7 +69,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
*/
// import 'intl'; // Run `npm install --save intl`.
/** *************************************************************************************************
/***************************************************************************************************
* MATERIAL 2
*/
import 'hammerjs/hammer';

View File

@@ -12,91 +12,3 @@ 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

@@ -1,5 +1,5 @@
/* SystemJS module definition */
declare let module: NodeModule;
declare var module: NodeModule;
interface NodeModule {
id: string;
}

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
<packaging>pom</packaging>
@@ -43,7 +43,6 @@
<properties>
<java.version>9</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<org.projectlombok.version>1.18.30</org.projectlombok.version>
<maven-surefire-plugin.version>2.22.0</maven-surefire-plugin.version>
<maven-failsafe-plugin.version>2.22.0</maven-failsafe-plugin.version>
<jacoco-maven-plugin.version>0.8.8</jacoco-maven-plugin.version>
@@ -51,7 +50,6 @@
<nexus-staging-maven-plugin.version>1.6.7</nexus-staging-maven-plugin.version>
<maven-release-plugin.version>2.5.3</maven-release-plugin.version>
<maven-gpg-plugin.version>3.0.1</maven-gpg-plugin.version>
<sonar-maven-plugin.version>3.11.0.3922</sonar-maven-plugin.version>
<sonar.organization>fabioformosa</sonar.organization>
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
<sonar.exclusions>
@@ -80,32 +78,27 @@
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-common</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-api</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-security</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-persistence</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-ui</artifactId>
<version>4.0.11-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<version>4.0.8</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -140,11 +133,6 @@
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>${sonar-maven-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
@@ -252,25 +240,26 @@
</activation>
<distributionManagement>
<snapshotRepository>
<id>maven-central-release</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>maven-central-release</id>
<url>https://central.sonatype.com</url>
<id>ossrh</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/
</url>
</repository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.7.0</version>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${nexus-staging-maven-plugin.version}</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>maven-central-release</publishingServerId>
<autoPublish>true</autoPublish>
<waitUntil>published</waitUntil>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-common</artifactId>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-starter-api</artifactId>

View File

@@ -55,8 +55,7 @@ public class SchedulerConfig {
if (quartzProperties != null && quartzProperties.size() > 0)
mergedProperties.putAll(quartzProperties);
factory.setQuartzProperties(mergedProperties);
boolean isAutoStartup = mergedProperties.getProperty("org.quartz.scheduler.isAutoStartup") != null && mergedProperties.getProperty("org.quartz.scheduler.isAutoStartup").equals("true");
factory.setAutoStartup(isAutoStartup);
factory.setAutoStartup(false);
return factory;
}
}

View File

@@ -1,6 +1,5 @@
package it.fabioformosa.quartzmanager.api.configuration;
import it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
@@ -15,16 +14,14 @@ public class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic"); //enable a simple memory-based message broker
// on destinations prefixed with /topic
config.setApplicationDestinationPrefixes("/job"); // it designates the prefix for messages
// that are bound for methods annotated with @MessageMapping
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/job");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/logs").setAllowedOrigins("/**").withSockJS();
registry.addEndpoint(QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/progress").setAllowedOrigins("/**").withSockJS();
registry.addEndpoint("/quartz-manager/logs").setAllowedOrigins("/**").withSockJS();
registry.addEndpoint("/quartz-manager/progress").setAllowedOrigins("/**").withSockJS();
}
}

View File

@@ -8,10 +8,7 @@ import org.springframework.stereotype.Controller;
@Controller
public class WebsocketController {
@MessageMapping({
QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/logs",
QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/progress"
})
@MessageMapping({ QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/logs", QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/progress" })
@SendTo("/topic/logs")
public String subscribe() {
return "subscribed";

View File

@@ -2,7 +2,6 @@ package it.fabioformosa.quartzmanager.api.converters;
import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerCommandDTO;
import org.quartz.JobDataMap;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
@@ -20,8 +19,6 @@ public class SimpleTriggerCommandDTOToSimpleTrigger implements Converter<SimpleT
if (triggerCommandDTO.getSimpleTriggerInputDTO().getEndDate() != null)
triggerTriggerBuilder.endAt(triggerCommandDTO.getSimpleTriggerInputDTO().getEndDate());
if (triggerCommandDTO.getSimpleTriggerInputDTO().getJobDataMap() != null)
triggerTriggerBuilder.usingJobData(new JobDataMap(triggerCommandDTO.getSimpleTriggerInputDTO().getJobDataMap()));
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
if (triggerCommandDTO.getSimpleTriggerInputDTO().getRepeatInterval() != null)

View File

@@ -14,7 +14,6 @@ public class SimpleTriggerToSimpleTriggerDTO extends TriggerToTriggerDTO<SimpleT
target.setRepeatCount(source.getRepeatCount());
target.setRepeatInterval(source.getRepeatInterval());
target.setMisfireInstruction(source.getMisfireInstruction());
target.setJobDataMap(source.getJobDataMap());
}
@Override

View File

@@ -3,9 +3,8 @@ package it.fabioformosa.quartzmanager.api.dto;
import it.fabioformosa.quartzmanager.api.validators.ValidTriggerRepetition;
import lombok.*;
import lombok.experimental.SuperBuilder;
import javax.annotation.Nullable;
import javax.validation.constraints.Positive;
import java.util.Map;
@ValidTriggerRepetition
@SuperBuilder
@@ -19,7 +18,4 @@ public class SimpleTriggerInputDTO extends TriggerCommandDTO implements TriggerR
@Positive
private Long repeatInterval;
@Nullable
private Map<String, ?> jobDataMap;
}

View File

@@ -4,7 +4,6 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.quartz.JobDataMap;
import java.util.Date;
@@ -24,5 +23,4 @@ public class TriggerDTO {
private JobKeyDTO jobKeyDTO;
private JobDetailDTO jobDetailDTO;
private boolean mayFireAgain;
private JobDataMap jobDataMap;
}

View File

@@ -38,13 +38,11 @@ public abstract class AbstractQuartzManagerJob implements Job {
LogRecord logMsg = doIt(jobExecutionContext);
log.info(logMsg.getMessage());
String triggerName = jobExecutionContext.getTrigger().getKey().getName();
logMsg.setThreadName(Thread.currentThread().getName());
webSocketLogsNotifier.send(triggerName, logMsg);
webSocketLogsNotifier.send(logMsg);
TriggerFiredBundleDTO triggerFiredBundleDTO = WebSocketProgressNotifier.buildTriggerFiredBundle(jobExecutionContext);
webSocketProgressNotifier.send(triggerName, triggerFiredBundleDTO);
webSocketProgressNotifier.send(triggerFiredBundleDTO);
}
}

View File

@@ -14,7 +14,7 @@ public class WebSocketLogsNotifier implements WebhookSender<LogRecord> {
private SimpMessageSendingOperations messagingTemplate;
@Override
public void send(String triggerName, LogRecord logRecord) {
messagingTemplate.convertAndSend(TOPIC_LOGS + "/" + triggerName, logRecord);
public void send(LogRecord logRecord) {
messagingTemplate.convertAndSend(TOPIC_LOGS, logRecord);
}
}

View File

@@ -20,8 +20,8 @@ public class WebSocketProgressNotifier implements WebhookSender<TriggerFiredBund
private SimpMessageSendingOperations messagingTemplate;
@Override
public void send(String triggerName, TriggerFiredBundleDTO triggerFiredBundleDTO) {
messagingTemplate.convertAndSend(TOPIC_PROGRESS + "/" + triggerName, triggerFiredBundleDTO);
public void send(TriggerFiredBundleDTO triggerFiredBundleDTO) {
messagingTemplate.convertAndSend(TOPIC_PROGRESS, triggerFiredBundleDTO);
}
public static TriggerFiredBundleDTO buildTriggerFiredBundle(JobExecutionContext jobExecutionContext) {

View File

@@ -9,6 +9,6 @@ package it.fabioformosa.quartzmanager.api.websockets;
*/
public interface WebhookSender<T> {
void send(String triggerName, T message);
void send(T message);
}

View File

@@ -21,8 +21,7 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.util.Date;
import java.util.Map;
import static java.util.Map.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
@@ -69,10 +68,6 @@ class SimpleTriggerControllerTest {
}
private SimpleTriggerInputDTO buildACompleteSimpleTriggerCommandDTO() {
Map<String, ?> triggerJobDataMap = Map.ofEntries(
entry("customTriggerData1", "value1"),
entry("customTriggerData2", "value2")
);
return SimpleTriggerInputDTO.builder()
.jobClass("it.fabioformosa.quartzmanager.api.jobs.SampleJob")
.startDate(new Date())
@@ -80,7 +75,6 @@ class SimpleTriggerControllerTest {
.misfireInstruction(MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW)
.repeatCount(5)
.repeatInterval(1000L * 60 * 60)
.jobDataMap(triggerJobDataMap)
.build();
}

View File

@@ -2,23 +2,12 @@ package it.fabioformosa.quartzmanager.api.controllers.utils;
import it.fabioformosa.quartzmanager.api.common.utils.DateUtils;
import it.fabioformosa.quartzmanager.api.dto.*;
import org.quartz.JobDataMap;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;
import static java.util.Map.entry;
public class TriggerUtils {
static public TriggerDTO getTriggerInstance(String triggerName) {
static public TriggerDTO getTriggerInstance(String triggerName){
return TriggerDTO.builder()
.description("sample trigger")
.endTime(DateUtils.addHoursToNow(2L))
@@ -39,7 +28,7 @@ public class TriggerUtils {
.build();
}
static public SimpleTriggerDTO getSimpleTriggerInstance(String triggerName, SimpleTriggerInputDTO simpleTriggerInputDTO) {
static public SimpleTriggerDTO getSimpleTriggerInstance(String triggerName, SimpleTriggerInputDTO simpleTriggerInputDTO){
return SimpleTriggerDTO.builder()
.description("simple trigger")
.repeatCount(simpleTriggerInputDTO.getRepeatCount())
@@ -59,11 +48,10 @@ public class TriggerUtils {
.nextFireTime(DateUtils.addHoursToNow(1L))
.priority(1)
.startTime(DateUtils.fromLocalDateTimeToDate(LocalDateTime.now()))
.jobDataMap(new JobDataMap(simpleTriggerInputDTO.getJobDataMap()))
.build();
}
static public SimpleTriggerDTO getSimpleTriggerInstance(String triggerName) {
static public SimpleTriggerDTO getSimpleTriggerInstance(String triggerName){
return SimpleTriggerDTO.builder()
.description("simple trigger")
.repeatCount(2)
@@ -83,44 +71,6 @@ public class TriggerUtils {
.nextFireTime(DateUtils.addHoursToNow(1L))
.priority(1)
.startTime(DateUtils.fromLocalDateTimeToDate(LocalDateTime.now()))
.jobDataMap(new JobDataMap(Map.ofEntries(entry("customTriggerData1", "value1"))))
.build();
}
static public SimpleTrigger buildSimpleTrigger() {
TriggerBuilder<Trigger> triggerTriggerBuilder = TriggerBuilder.newTrigger();
triggerTriggerBuilder.startAt(new Date());
triggerTriggerBuilder.endAt(DateUtils.addHoursToNow(1));
triggerTriggerBuilder.usingJobData(new JobDataMap(Map.ofEntries(entry("data", "value"))));
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
scheduleBuilder.withIntervalInMilliseconds(1000);
scheduleBuilder.withRepeatCount(1);
scheduleBuilder.withMisfireHandlingInstructionFireNow();
return triggerTriggerBuilder.withSchedule(
scheduleBuilder
)
.withIdentity("simpleTrigger").build();
}
static public SimpleTriggerCommandDTO buildSimpleTriggerCommandDTO(String triggerName) throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date startDate = dateFormat.parse("2024-02-02");
Date endDate = dateFormat.parse("2024-03-02");
SimpleTriggerInputDTO triggerInputDTO = SimpleTriggerInputDTO.builder()
.misfireInstruction(MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW)
.jobClass("sample.jobClass")
.repeatCount(1)
.repeatInterval(1000L)
.startDate(startDate)
.endDate(endDate)
.jobDataMap(Map.ofEntries(entry("data", "value")))
.build();
return SimpleTriggerCommandDTO.builder()
.triggerName(triggerName)
.simpleTriggerInputDTO(triggerInputDTO)
.build();
}

View File

@@ -1,41 +0,0 @@
package it.fabioformosa.quartzmanager.api.converters;
import it.fabioformosa.quartzmanager.api.controllers.utils.TriggerUtils;
import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerCommandDTO;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.quartz.SimpleTrigger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.convert.ConversionService;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest
class SimpleTriggerCommandDTOToSimpleTriggerTest {
@Autowired
private ConversionService conversionService;
@Order(1)
@Test
void givenSimpleTriggerCommandDTO_whenItIsConverted_thenASimpleTriggerIsReturned() throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date startDate = dateFormat.parse("2024-02-02");
Date endDate = dateFormat.parse("2024-03-02");
SimpleTriggerCommandDTO simpleTriggerCommandDTO = TriggerUtils.buildSimpleTriggerCommandDTO("mytrigger");
SimpleTrigger simpleTrigger = conversionService.convert(simpleTriggerCommandDTO, SimpleTrigger.class);
Assertions.assertThat(simpleTrigger).isNotNull();
Assertions.assertThat(simpleTrigger.getRepeatCount()).isEqualTo(simpleTriggerCommandDTO.getSimpleTriggerInputDTO().getRepeatCount());
Assertions.assertThat(simpleTrigger.getRepeatInterval()).isEqualTo(simpleTriggerCommandDTO.getSimpleTriggerInputDTO().getRepeatInterval());
Assertions.assertThat(simpleTrigger.getJobDataMap()).containsEntry("data", "value");
Assertions.assertThat(simpleTrigger.getStartTime()).isEqualTo(startDate);
Assertions.assertThat(simpleTrigger.getEndTime()).isEqualTo(endDate);
}
}

View File

@@ -1,36 +0,0 @@
package it.fabioformosa.quartzmanager.api.converters;
import it.fabioformosa.quartzmanager.api.controllers.utils.TriggerUtils;
import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerDTO;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.quartz.SimpleTrigger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.convert.ConversionService;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest
class SimpleTriggerToSimpleTriggerDTOTest {
@Autowired
private ConversionService conversionService;
@Order(1)
@Test
void givenSimpleTrigger_whenItIsConverted_thenADtoIsReturned() {
SimpleTrigger simpleTrigger = TriggerUtils.buildSimpleTrigger();
SimpleTriggerDTO simpleTriggerDTO = conversionService.convert(simpleTrigger, SimpleTriggerDTO.class);
Assertions.assertThat(simpleTriggerDTO).isNotNull();
Assertions.assertThat(simpleTriggerDTO.getRepeatCount()).isEqualTo(simpleTrigger.getRepeatCount());
Assertions.assertThat(simpleTriggerDTO.getRepeatInterval()).isEqualTo(simpleTrigger.getRepeatInterval());
Assertions.assertThat(simpleTriggerDTO.getJobDataMap()).containsEntry("data", "value");
Assertions.assertThat(simpleTriggerDTO.getStartTime()).isEqualTo(simpleTrigger.getStartTime());
Assertions.assertThat(simpleTriggerDTO.getEndTime()).isEqualTo(simpleTrigger.getEndTime());
}
}

View File

@@ -4,26 +4,16 @@ import it.fabioformosa.quartzmanager.api.dto.TriggerFiredBundleDTO;
import it.fabioformosa.quartzmanager.api.jobs.entities.LogRecord;
import it.fabioformosa.quartzmanager.api.websockets.WebhookSender;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.ScheduleBuilder;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.mockito.MockitoAnnotations;
import org.quartz.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.argThat;
@ExtendWith(MockitoExtension.class)
class SampleJobTest {
@InjectMocks
@@ -34,16 +24,14 @@ class SampleJobTest {
@Mock
private WebhookSender<LogRecord> webSocketLogsNotifier;
@Captor
private ArgumentCaptor<LogRecord> logRecordCaptor;
@Captor
private ArgumentCaptor<TriggerFiredBundleDTO> triggerFiredBundleDTOCaptor;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void givenASampleJob_whenTheJobIsExecuted_thenTheWebhookSendersAreCalled() {
JobExecutionContext jobExecutionContext = Mockito.mock(JobExecutionContext.class);
String triggerName = "test-trigger";
ScheduleBuilder schedulerBuilder = SimpleScheduleBuilder.simpleSchedule()
.withRepeatCount(5)
@@ -52,7 +40,6 @@ class SampleJobTest {
.newJob(SampleJob.class).withIdentity(JobKey.jobKey("test-job"))
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(triggerName)
.forJob(jobDetail)
.withSchedule(schedulerBuilder)
.build();
@@ -60,23 +47,24 @@ class SampleJobTest {
Mockito.when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail);
sampleJob.execute(jobExecutionContext);
Mockito.verify(webSocketLogsNotifier).send(eq(triggerName), logRecordCaptor.capture());
LogRecord actualLogRecord = logRecordCaptor.getValue();
Assertions.assertThat(actualLogRecord.getMessage()).isEqualTo("Hello!");
Assertions.assertThat(actualLogRecord.getType()).isEqualTo(LogRecord.LogType.INFO);
Assertions.assertThat(actualLogRecord.getDate()).isNotNull();
Assertions.assertThat(actualLogRecord.getThreadName()).isNotNull();
Mockito.verify(webSocketProgressNotifier).send(eq(triggerName), triggerFiredBundleDTOCaptor.capture());
TriggerFiredBundleDTO triggerFiredBundleDTO = triggerFiredBundleDTOCaptor.getValue();
Assertions.assertThat(triggerFiredBundleDTO.getJobKey()).isEqualTo("test-job");
Assertions.assertThat(triggerFiredBundleDTO.getRepeatCount()).isEqualTo(6);
Assertions.assertThat(triggerFiredBundleDTO.getJobClass()).isEqualTo(SampleJob.class.getName());
Assertions.assertThat(triggerFiredBundleDTO.getTimesTriggered()).isZero();
Assertions.assertThat(triggerFiredBundleDTO.getNextFireTime()).isNull();
Assertions.assertThat(triggerFiredBundleDTO.getPercentage()).isZero();
Assertions.assertThat(triggerFiredBundleDTO.getFinalFireTime()).isNotNull();
Assertions.assertThat(triggerFiredBundleDTO.getPreviousFireTime()).isNull();
Mockito.verify(webSocketLogsNotifier).send(argThat(actualLogRecord -> {
Assertions.assertThat(actualLogRecord.getMessage()).isEqualTo("Hello!");
Assertions.assertThat(actualLogRecord.getType()).isEqualTo(LogRecord.LogType.INFO);
Assertions.assertThat(actualLogRecord.getDate()).isNotNull();
Assertions.assertThat(actualLogRecord.getThreadName()).isNotNull();
return true;
}));
Mockito.verify(webSocketProgressNotifier).send(argThat(triggerFiredBundleDTO -> {
Assertions.assertThat(triggerFiredBundleDTO.getJobKey()).isEqualTo("test-job");
Assertions.assertThat(triggerFiredBundleDTO.getRepeatCount()).isEqualTo(6);
Assertions.assertThat(triggerFiredBundleDTO.getJobClass()).isEqualTo(SampleJob.class.getName());
Assertions.assertThat(triggerFiredBundleDTO.getTimesTriggered()).isZero();
Assertions.assertThat(triggerFiredBundleDTO.getNextFireTime()).isNull();
Assertions.assertThat(triggerFiredBundleDTO.getPercentage()).isZero();
Assertions.assertThat(triggerFiredBundleDTO.getFinalFireTime()).isNotNull();
Assertions.assertThat(triggerFiredBundleDTO.getPreviousFireTime()).isNull();
return true;
}));
}
@Test

View File

@@ -18,17 +18,13 @@ import java.util.Date;
@SpringBootTest
class SimpleTriggerServiceIntegrationTest {
private static final String SAMPLE_JOB_CLASS = "it.fabioformosa.quartzmanager.api.jobs.SampleJob";
private static final String SAMPLE_EXTRA_JOB_CLASS = "it.fabioformosa.samplepackage.SampleExtraJob";
private static final String FIRST_TRIGGER_SUFFIX = "A";
private static final String SECOND_TRIGGER_SUFFIX = "B";
@Autowired
private SimpleTriggerService simpleTriggerService;
@Test
void givenASimpleTriggerCommandDTOWithAllData_whenANewSimpleTriggerIsScheduled_thenShouldGetATriggertDTO() throws SchedulerException, ClassNotFoundException {
String simpleTriggerTestName = "simpleTriggerWithAllData";
String jobClass = "it.fabioformosa.quartzmanager.api.jobs.SampleJob";
Date startDate = new Date();
Date endDate = DateUtils.addHoursToNow(5);
int repeatCount = 3;
@@ -45,7 +41,7 @@ class SimpleTriggerServiceIntegrationTest {
.repeatCount(repeatCount)
.repeatInterval(repeatInterval)
.misfireInstruction(misfireInstructionFireNow)
.jobClass(SAMPLE_JOB_CLASS)
.jobClass(jobClass)
.build())
.build();
SimpleTriggerDTO simpleTriggerDTO = simpleTriggerService.scheduleSimpleTrigger(simpleTriggerCommand);
@@ -65,11 +61,12 @@ class SimpleTriggerServiceIntegrationTest {
@Test
void givenASimpleTriggerCommandDTOWithMissingOptionalField_whenANewSimpleTriggerIsScheduled_thenShouldGetATriggertDTO() throws SchedulerException, ClassNotFoundException {
String simpleTriggerTestName = "simpleTriggerWithoutOptionalData";
String jobClass = "it.fabioformosa.quartzmanager.api.jobs.SampleJob";
SimpleTriggerCommandDTO simpleTriggerCommand = SimpleTriggerCommandDTO.builder()
.triggerName(simpleTriggerTestName)
.simpleTriggerInputDTO(SimpleTriggerInputDTO.builder()
.jobClass(SAMPLE_JOB_CLASS)
.jobClass(jobClass)
.build())
.build();
SimpleTriggerDTO simpleTriggerDTO = simpleTriggerService.scheduleSimpleTrigger(simpleTriggerCommand);
@@ -84,49 +81,4 @@ class SimpleTriggerServiceIntegrationTest {
Assertions.assertThat(simpleTriggerDTO.getRepeatInterval()).isZero();
}
@Test
void givenTwoSimpleTriggerCommandDTOsForTheSameJob_whenScheduled_thenShouldCreateTwoTriggers() throws SchedulerException, ClassNotFoundException {
String triggerNamePrefix = "sameJobTrigger" + System.nanoTime();
SimpleTriggerDTO firstTrigger = simpleTriggerService.scheduleSimpleTrigger(
buildSimpleTriggerCommand(triggerNamePrefix + FIRST_TRIGGER_SUFFIX, SAMPLE_JOB_CLASS)
);
SimpleTriggerDTO secondTrigger = simpleTriggerService.scheduleSimpleTrigger(
buildSimpleTriggerCommand(triggerNamePrefix + SECOND_TRIGGER_SUFFIX, SAMPLE_JOB_CLASS)
);
Assertions.assertThat(firstTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + FIRST_TRIGGER_SUFFIX);
Assertions.assertThat(secondTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + SECOND_TRIGGER_SUFFIX);
Assertions.assertThat(firstTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_JOB_CLASS);
Assertions.assertThat(secondTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_JOB_CLASS);
Assertions.assertThat(firstTrigger.getJobKeyDTO().getName()).isNotEqualTo(secondTrigger.getJobKeyDTO().getName());
}
@Test
void givenTwoSimpleTriggerCommandDTOsForDifferentJobs_whenScheduled_thenShouldCreateTwoTriggers() throws SchedulerException, ClassNotFoundException {
String triggerNamePrefix = "differentJobTrigger" + System.nanoTime();
SimpleTriggerDTO firstTrigger = simpleTriggerService.scheduleSimpleTrigger(
buildSimpleTriggerCommand(triggerNamePrefix + FIRST_TRIGGER_SUFFIX, SAMPLE_JOB_CLASS)
);
SimpleTriggerDTO secondTrigger = simpleTriggerService.scheduleSimpleTrigger(
buildSimpleTriggerCommand(triggerNamePrefix + SECOND_TRIGGER_SUFFIX, SAMPLE_EXTRA_JOB_CLASS)
);
Assertions.assertThat(firstTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + FIRST_TRIGGER_SUFFIX);
Assertions.assertThat(secondTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + SECOND_TRIGGER_SUFFIX);
Assertions.assertThat(firstTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_JOB_CLASS);
Assertions.assertThat(secondTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_EXTRA_JOB_CLASS);
}
private static SimpleTriggerCommandDTO buildSimpleTriggerCommand(String triggerName, String jobClass) {
return SimpleTriggerCommandDTO.builder()
.triggerName(triggerName)
.simpleTriggerInputDTO(SimpleTriggerInputDTO.builder()
.jobClass(jobClass)
.startDate(DateUtils.addHoursToNow(1))
.build())
.build();
}
}

View File

@@ -1,48 +0,0 @@
package it.fabioformosa.quartzmanager.api.websockets;
import it.fabioformosa.quartzmanager.api.dto.TriggerFiredBundleDTO;
import it.fabioformosa.quartzmanager.api.jobs.entities.LogRecord;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import static org.mockito.MockitoAnnotations.openMocks;
class WebSocketNotifierTest {
@InjectMocks
private WebSocketLogsNotifier webSocketLogsNotifier;
@InjectMocks
private WebSocketProgressNotifier webSocketProgressNotifier;
@Mock
private SimpMessageSendingOperations messagingTemplate;
@BeforeEach
void setUp() {
openMocks(this);
}
@Test
void givenATriggerName_whenALogIsSent_thenShouldSendItToTheTriggerLogsTopic() {
LogRecord logRecord = new LogRecord(LogRecord.LogType.INFO, "Hello!");
webSocketLogsNotifier.send("trigger-1", logRecord);
Mockito.verify(messagingTemplate).convertAndSend("/topic/logs/trigger-1", logRecord);
}
@Test
void givenATriggerName_whenProgressIsSent_thenShouldSendItToTheTriggerProgressTopic() {
TriggerFiredBundleDTO triggerFiredBundleDTO = new TriggerFiredBundleDTO();
webSocketProgressNotifier.send("trigger-1", triggerFiredBundleDTO);
Mockito.verify(messagingTemplate).convertAndSend("/topic/progress/trigger-1", triggerFiredBundleDTO);
}
}

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-starter-persistence</artifactId>

View File

@@ -11,6 +11,7 @@ import org.springframework.context.annotation.*;
import javax.sql.DataSource;
@Configuration
@PropertySource("classpath:quartz-persistence.properties")
public class PersistenceConfig {
@Value("${quartz-manager.persistence.quartz.datasource.url}")
@@ -56,7 +57,7 @@ public class PersistenceConfig {
@Primary
@Bean
public DataSource quartzManagerDatasource() {
public DataSource quartzManagerDatasource(PersistenceDatasourceProps persistenceDatasourceProps) {
return DataSourceBuilder.create()
.url(quartzDatasourceUrl)
.driverClassName("org.postgresql.Driver")

View File

@@ -5,6 +5,6 @@
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
<includeAll path="classpath:db/quartz-scheduler/migrations" relativeToChangelogFile="false" errorIfMissingOrEmpty="true"/>
<includeAll path="./migrations" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-starter-security</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
</parent>
<artifactId>quartz-manager-starter-ui</artifactId>

View File

@@ -1,9 +0,0 @@
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: dev
annotations: {}
labels: {}
description: dev
gke:
cluster: projects/quartz-manager-test/locations/europe-west8-a/clusters/gke-cluster

View File

@@ -1,18 +0,0 @@
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
name: quartz-manager-pipeline
labels:
app: quartz-manager-standalone
description: quartz-manager-standalone delivery pipeline
serialPipeline:
stages:
- targetId: dev
profiles:
- dev
- targetId: staging
profiles:
- staging
- targetId: prod
profiles:
- prod

View File

@@ -1,10 +0,0 @@
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: prod
annotations: {}
labels: {}
description: prod
requireApproval: true
gke:
cluster: projects/quartz-manager-test/locations/europe-west8-a/clusters/gke-cluster

View File

@@ -1,10 +0,0 @@
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: staging
annotations: {}
labels: {}
description: staging
requireApproval: true
gke:
cluster: projects/quartz-manager-test/locations/europe-west8-a/clusters/gke-cluster

View File

@@ -1,24 +0,0 @@
apiVersion: v2
name: quartz-manager-standalone
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "_HELM_APP_VERSION"

View File

@@ -1,22 +0,0 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "quartzmanager-standalone.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "quartzmanager-standalone.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "quartzmanager-standalone.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "quartzmanager-standalone.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -1,62 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "quartzmanager-standalone.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "quartzmanager-standalone.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "quartzmanager-standalone.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "quartzmanager-standalone.labels" -}}
helm.sh/chart: {{ include "quartzmanager-standalone.chart" . }}
{{ include "quartzmanager-standalone.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "quartzmanager-standalone.selectorLabels" -}}
app.kubernetes.io/name: {{ include "quartzmanager-standalone.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "quartzmanager-standalone.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "quartzmanager-standalone.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -1,9 +0,0 @@
apiVersion: "v1"
kind: "ConfigMap"
metadata:
name: {{ include "quartzmanager-standalone.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
app: {{ .Chart.Name }}
data:
envName: {{ .Values.config.envName | quote }}

View File

@@ -1,68 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "quartzmanager-standalone.fullname" . }}
labels:
{{- include "quartzmanager-standalone.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "quartzmanager-standalone.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "quartzmanager-standalone.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "quartzmanager-standalone.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
{{/* livenessProbe:*/}}
{{/* initialDelaySeconds: 30*/}}
{{/* timeoutSeconds: 10*/}}
{{/* httpGet:*/}}
{{/* path: /*/}}
{{/* port: 8080*/}}
{{/* readinessProbe:*/}}
{{/* initialDelaySeconds: 30*/}}
{{/* timeoutSeconds: 10*/}}
{{/* httpGet:*/}}
{{/* path: /*/}}
{{/* port: 8080*/}}
resources:
{{- toYaml .Values.resources | nindent 12 }}
envFrom:
- configMapRef:
name: {{ include "quartzmanager-standalone.fullname" . }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -1,28 +0,0 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "quartzmanager-standalone.fullname" . }}
labels:
{{- include "quartzmanager-standalone.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "quartzmanager-standalone.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -1,61 +0,0 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "quartzmanager-standalone.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "quartzmanager-standalone.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -1,15 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "quartzmanager-standalone.fullname" . }}
labels:
{{- include "quartzmanager-standalone.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "quartzmanager-standalone.selectorLabels" . | nindent 4 }}

View File

@@ -1,12 +0,0 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "quartzmanager-standalone.serviceAccountName" . }}
labels:
{{- include "quartzmanager-standalone.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -1,83 +0,0 @@
# Default values for hello-world.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: "europe-west8-docker.pkg.dev/quartz-manager-test/quartz-manager/quartz-manager-standalone/quartz-manager-standalone"
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: false
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
config:
envName: NA

View File

@@ -5,13 +5,13 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.11-SNAPSHOT</version>
<version>4.0.8</version>
</parent>
<packaging>jar</packaging>
<artifactId>quartz-manager-web-showcase</artifactId>
<packaging>war</packaging>
<name>Quartz Manager Web Showcase</name>
<description>A webapp that imports Quartz Manager API lib and the frontend webjar</description>
@@ -49,10 +49,15 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -118,28 +123,30 @@
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

View File

@@ -0,0 +1,21 @@
package it.fabioformosa;
import lombok.Generated;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
* ServletInitializer needs to deploy quartz-manager into a servlet container as a war file
*
* @author Fabio Formosa
*
*/
@Generated
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(QuartzManagerDemoApplication.class);
}
}

View File

@@ -1,46 +0,0 @@
apiVersion: skaffold/v4beta7
kind: Config
build:
tagPolicy:
envTemplate:
template: "_IMAGE_TAG_POLICY"
artifacts:
- image: quartz-manager-standalone
context: ./
profiles:
- name: dev
deploy:
helm:
releases:
- name: quartzmanager-standalone
createNamespace: true
namespace: quartzmanager-dev
chartPath: quartz-manager-parent/quartz-manager-web-showcase/helm
# valuesFiles:
# - helm/envs/dev/values.yaml
setValueTemplates:
image.tag: "_IMAGE_TAG_POLICY"
- name: staging
deploy:
helm:
releases:
- name: quartzmanager-standalone
createNamespace: true
namespace: quartzmanager-staging
chartPath: quartz-manager-parent/quartz-manager-web-showcase/helm
# valuesFiles:
# - helm/envs/dev/values.yaml
setValueTemplates:
image.tag: "_IMAGE_TAG_POLICY"
- name: prod
deploy:
helm:
releases:
- name: quartzmanager-standalone
createNamespace: true
namespace: quartzmanager-prod
chartPath: quartz-manager-parent/quartz-manager-web-showcase/helm
# valuesFiles:
# - helm/envs/dev/values.yaml
setValueTemplates:
image.tag: "_IMAGE_TAG_POLICY"