Compare commits

..

164 Commits

Author SHA1 Message Date
Fabio Formosa
068b0eed34 #103 fixed a test 2026-05-06 23:43:35 +02:00
Fabio Formosa
b2f9692815 #103 fixed a test 2026-05-06 23:41:08 +02:00
Fabio Formosa
5fc6c56409 Merge branch 'develop' into feature/#103_multiple_triggers
# Conflicts:
#	quartz-manager-parent/.gitignore
2026-05-06 23:26:40 +02:00
Fabio Formosa
d7a78c57ae #103 minor enhancements 2026-05-06 23:13:44 +02:00
Fabio Formosa
31658416f5 #103 final step into multi-trigger feature 2026-05-06 01:22:30 +02:00
Fabio Formosa
1d81e88684 Merge remote-tracking branch 'origin/master' into develop 2026-04-27 23:13:05 +02:00
Fabio Formosa
f55a58b100 Add workflow_dispatch trigger to sonar-java.yml
added the workflow_dispatch to the GitHub Action
2026-04-23 23:26:20 +02:00
Fabio Formosa
902542e480 Update sonar-java.yml
added the workflow_dispatch trigger for the sonar action
2026-04-23 22:39:34 +02:00
Fabio Formosa
c198d32bd5 bump to version 4.0.11-SNAPSHOT 2025-05-23 23:06:34 +02:00
Fabio Formosa
56d9f5d94f bump to version 4.0.10 2025-05-23 23:04:28 +02:00
Fabio Formosa
45f66d51fe Merge remote-tracking branch 'origin/master' into develop 2025-05-23 23:00:37 +02:00
Fabio Formosa
95769248a3 migrated to the new maven central repo 2025-05-23 21:52:30 +02:00
Fabio Formosa
dcbf3eb277 migrated to the new maven central repo 2025-05-23 20:10:53 +02:00
Fabio Formosa
fab977fd7d added the staging and prod env to the delivery pipeline 2024-07-27 17:41:31 +02:00
Fabio Formosa
9d2a01ebbe added the staging and prod env to the delivery pipeline 2024-07-27 17:33:31 +02:00
Fabio Formosa
9a0789cab0 minor commit 2024-07-27 17:08:11 +02:00
Fabio Formosa
e5a6b8b32b minor commit 2024-07-27 16:33:24 +02:00
Fabio Formosa
4537c8e304 minor commit 2024-07-18 23:53:27 +02:00
Fabio Formosa
82ac821b34 minor commit 2024-07-17 00:30:49 +02:00
Fabio Formosa
0a4a31ae65 fixed the chart path 2024-07-17 00:21:24 +02:00
Fabio Formosa
3b325536e8 fixed the dev cluster reference 2024-07-17 00:06:58 +02:00
Fabio Formosa
307c6eab98 fixed the docker image in the cloud deploy pipeline 2024-07-16 23:49:38 +02:00
Fabio Formosa
b1ff70265f added verbosity to cloudbuild.yaml 2024-07-16 23:15:54 +02:00
Fabio Formosa
226296737d added verbosity to cloudbuild.yaml 2024-07-16 23:05:34 +02:00
Fabio Formosa
a5750d1d0d duplicated the helm props replacements 2024-07-16 00:24:10 +02:00
Fabio Formosa
f71c9b20ab minor commit 2024-07-16 00:15:05 +02:00
Fabio Formosa
9cc55492dc fixed the pipeline name 2024-07-15 23:58:08 +02:00
Fabio Formosa
6d36e4620c fixed the path of the pipeline file 2024-07-15 23:47:32 +02:00
Fabio Formosa
4d5e8f62c3 added a google deploy pipeline 2024-07-15 23:39:51 +02:00
Fabio Formosa
8ba33f25b4 fixed copy path in the Dockerfile 2024-07-13 16:00:44 +02:00
Fabio Formosa
7a742d5aea fixed copy path in the Dockerfile 2024-07-13 15:52:40 +02:00
Fabio Formosa
abd25d6158 fixed a wrong path in cloudbuild.yaml 2024-07-13 15:33:10 +02:00
Fabio Formosa
9f46e52564 added a logging policy to cloudbuild.yaml and pushed skaffold.yaml 2024-07-13 15:28:24 +02:00
Fabio Formosa
e6927209e5 added a helm chart to the web-showcase module and cloudbuild.yaml and skaffold.yaml 2024-07-13 13:00:12 +02:00
Fabio Formosa
63fbedbdc8 moved the web-showcase from war to jar and added a dockerfile 2024-07-12 00:36:47 +02:00
Fabio Formosa
0148056b1f Update README.md 2024-03-07 23:32:51 +01:00
Fabio Formosa
ad6a61f3df Update README.md
added a new example for the repo quartz-manager-use-cases
2024-02-07 00:24:21 +01:00
Fabio Formosa
5cb73019de bump version to 4.0.10-SNAPSHOT 2024-02-07 00:19:17 +01:00
Fabio Formosa
8a8e878e47 Update README.md
updated all references to the last release
2024-02-07 00:16:40 +01:00
Fabio Formosa
fae82e1d4e Merge pull request #118 from fabioformosa/develop
Develop to Master
2024-02-07 00:15:44 +01:00
Fabio Formosa
e0011913c2 bump version to 4.0.9 2024-02-06 23:58:07 +01:00
Fabio Formosa
a59b6a6c96 #115 fixed the liquibase DB migration execution 2024-02-06 23:43:54 +01:00
Fabio Formosa
68aaab6ac4 #115 fixed the liquibase DB migration execution 2024-02-06 23:42:38 +01:00
Fabio Formosa
a1d8b12e98 Merge pull request #112 from fabioformosa/feature/#109_configure_trigger_job_data
Feature/#109 configure trigger job data
2024-02-02 23:59:20 +01:00
Fabio Formosa
45d6a4c59a Merge branch 'feature/#109_configure_trigger_job_data' of github.com:fabioformosa/quartz-manager into feature/#109_configure_trigger_job_data 2024-02-02 23:54:31 +01:00
Midhun A Darvin
e6f6fb5f06 feat: configure Trigger JobData
- add `jobDataMap` property to SimpleTriggerInputDTO
- update SimpleTriggerCommandDTOToSimpleTrigger converter
- update SimpleTriggerToSimpleTriggerDTO converter
- update unit tests
- add .idea and .iml to .gitignore for ignoring intellij files
2024-02-02 23:43:27 +01:00
Fabio Formosa
13c438d097 Merge branch 'master' into develop 2024-02-02 23:38:22 +01:00
Fabio Formosa
412e455907 #103 step into accomodating the new trigger logic 2024-02-02 22:58:56 +01:00
Fabio Formosa
75d630aad0 Merge pull request #110 from midhunadarvin/feat/configure-trigger-job-data
feat: configure `Trigger` JobDataMap
2024-02-02 22:56:20 +01:00
Midhun A Darvin
9eddc0b1fd feat: configure Trigger JobData
- add `jobDataMap` property to SimpleTriggerInputDTO
- update SimpleTriggerCommandDTOToSimpleTrigger converter
- update SimpleTriggerToSimpleTriggerDTO converter
- update unit tests
- add .idea and .iml to .gitignore for ignoring intellij files
2024-02-02 13:11:00 +05:30
Fabio Formosa
fa4ede5179 Merge pull request #108 from midhunadarvin/feat/configure-auto-startup
feat: make autoStartup configurable for SchedulerFactoryBean
2024-02-01 00:24:21 +01:00
Fabio Formosa
3aa672031a Updated the sonarcloud.io integration 2024-01-31 23:40:26 +01:00
Midhun A Darvin
82ca186bff feat: make autoStartup configurable for SchedulerFactoryBean
- check property `org.quartz.scheduler.isAutoStartup` and enable autoStartup if it is true
2024-01-31 13:28:15 +05:30
Fabio Formosa
727a11fcea #103 fixed a test 2022-12-21 00:03:39 +01:00
Fabio Formosa
7106dc0fbb #103 clean up 2022-12-21 00:00:29 +01:00
Fabio Formosa
a2c8ecb227 #103 migrated the progress websocket to rx-stomp 2022-12-20 23:59:30 +01:00
Fabio Formosa
e4c771e364 #103 migrated the progress websocket to rx-stomp 2022-12-20 23:58:51 +01:00
Fabio Formosa
261dd8b624 #103 appended the trigger key to the websocket topic 2022-12-20 20:51:10 +01:00
Fabio Formosa
ac63576704 #103 migrated the logs websocket to rx-stomp 2022-12-18 11:58:24 +01:00
Fabio Formosa
a3b92443c4 #103 enabled a dev profile in the angular.json 2022-12-18 11:53:32 +01:00
Fabio Formosa
527ee1200a #103 added some comments 2022-12-11 20:38:55 +01:00
Fabio Formosa
82a60eb651 Update README.md 2022-12-10 16:59:31 +01:00
Fabio Formosa
7fd174b313 bump version to 4.0.9-SNAPSHOT 2022-12-10 16:56:05 +01:00
Fabio Formosa
7c910196e1 upgraded README and CHANGELOG 2022-12-10 16:54:50 +01:00
Fabio Formosa
554e7e5e25 #80 bump version to 4.0.8 2022-12-10 16:39:26 +01:00
Fabio Formosa
8b70319436 #80 upgraded node and npm into the quartz-manager-starter-ui 2022-12-10 16:38:34 +01:00
Fabio Formosa
053f196b6b Merge pull request #100 from fabioformosa/develop
v4.0.7
2022-12-10 16:29:16 +01:00
Fabio Formosa
86f8cc8347 bump version to 4.0.7 2022-12-10 16:25:17 +01:00
Fabio Formosa
e91d02ba9f #80 added moment to the allowed commonJS module 2022-12-10 16:19:39 +01:00
Fabio Formosa
109d2868d9 #80 upgraded to the new linter and added prettier 2022-12-10 16:15:00 +01:00
Fabio Formosa
4673e41fc5 #80 removed unnecessary asterisks 2022-12-10 14:53:35 +01:00
Fabio Formosa
a2122351d6 #80 removed extractCss 2022-12-10 13:31:45 +01:00
Fabio Formosa
375aaf71d3 #80 fixed the build command as angular v14 2022-12-10 13:25:14 +01:00
Fabio Formosa
b752af948d #80 downgraded jest to v28 2022-12-10 13:21:18 +01:00
Fabio Formosa
52ed016073 #80 fixed tests 2022-12-10 12:20:57 +01:00
Fabio Formosa
632288726b #80 upgraded some deps 2022-12-10 11:55:00 +01:00
Fabio Formosa
1dc9bee987 #80 upgraded to angular v14 2022-12-10 11:27:14 +01:00
Fabio Formosa
387c440e06 #80 step into angular v14 2022-12-10 11:20:01 +01:00
Fabio Formosa
136afa0fca #80 step into angular v14 2022-12-10 11:17:39 +01:00
Fabio Formosa
8ba3daa2f0 #80 upgraded angular to v13 2022-12-08 00:15:48 +01:00
Fabio Formosa
b3fe337203 #80 upgraded angular to v13 2022-12-08 00:15:39 +01:00
Fabio Formosa
da7375ea0d #80 first step to angular v13 2022-12-07 00:38:30 +01:00
Fabio Formosa
d9487d1106 #80 completed the migration to angular12 2022-12-07 00:37:25 +01:00
Fabio Formosa
3110496630 #80 completed the migration to angular12 2022-12-07 00:36:59 +01:00
Fabio Formosa
22e90dc7d1 #80 step into angular v12 2022-12-07 00:29:20 +01:00
Fabio Formosa
5943243e62 #80 step into angular v12 2022-12-07 00:27:20 +01:00
Fabio Formosa
1244a64d34 #80 fixes for angular v11 2022-12-07 00:25:50 +01:00
Fabio Formosa
40a6ecd159 #80 fixes for angular v11 2022-12-07 00:24:56 +01:00
Fabio Formosa
9df68dccac #80 first step to angular v11 2022-12-06 23:35:35 +01:00
Fabio Formosa
72bba6f80c #80 reverted deps to angular 10 2022-12-06 23:29:29 +01:00
Fabio Formosa
cf803bb1dd #80 reverted deps to angular 10 2022-12-06 23:29:13 +01:00
Fabio Formosa
13c78ed5d3 #80 removed the package-lock.json 2022-12-06 23:19:08 +01:00
Fabio Formosa
3bd878a978 #80 reverted deps to angular 10 2022-12-06 23:14:45 +01:00
Fabio Formosa
c9d3528f4b #80 reverted deps to angular 10 2022-12-06 23:14:32 +01:00
Fabio Formosa
3b2f8fc780 #80 reverted deps to angular 10 2022-12-06 23:08:17 +01:00
Fabio Formosa
bc3da09d60 #80 step into angular upgrade to v11 2022-12-06 19:33:42 +01:00
Fabio Formosa
284f56302c #80 fixed tests 2022-12-06 01:08:17 +01:00
Fabio Formosa
cdd5047bbc #80 jest setup fix for angular 10 2022-12-06 00:39:45 +01:00
Fabio Formosa
5d3ba95bd9 #80 upgraded angular to v10 2022-12-05 23:42:36 +01:00
Fabio Formosa
ec384113eb angular material upgrade to v4 2022-12-05 23:36:36 +01:00
Fabio Formosa
8b2651876c new package-lock.json 2022-12-05 23:31:55 +01:00
Fabio Formosa
a3852c421e bump version to 4.0.7-SNAPSHOT 2022-12-05 23:09:51 +01:00
Fabio Formosa
e1e1bdbd01 bump version to 4.0.6 2022-12-04 12:15:51 +01:00
Fabio Formosa
6757511de3 #62 added a missing test 2022-12-04 12:07:20 +01:00
Fabio Formosa
f5b717ec36 Update README.md 2022-12-04 11:54:06 +01:00
Fabio Formosa
6b491d9949 Update README.md 2022-12-04 11:52:54 +01:00
Fabio Formosa
cf382db49f Update README.md 2022-12-04 11:52:24 +01:00
Fabio Formosa
ee2f80e582 Update README.md
added a table of contents
2022-12-04 11:49:26 +01:00
Fabio Formosa
4453d62515 Update README.md
Added sonar quality badges
2022-12-04 11:33:47 +01:00
Fabio Formosa
72068818d7 #62 fixed sonar smells 2022-12-04 11:25:21 +01:00
Fabio Formosa
92bb94b9fa #62 added coverage exclusion 2022-12-04 11:15:53 +01:00
Fabio Formosa
7a2098e9ce #62 added coverage exclusion 2022-12-04 11:08:48 +01:00
Fabio Formosa
56b81f4f48 #62 added some exclusion from coverage 2022-12-03 19:54:07 +01:00
Fabio Formosa
82c060c2a7 #62 set jacoco properties 2022-12-03 19:46:14 +01:00
Fabio Formosa
fba35b796d #62 set jacoco properties 2022-12-03 19:42:12 +01:00
Fabio Formosa
5f6fc1fa6f #62 set jacoco properties 2022-12-03 19:16:28 +01:00
Fabio Formosa
a9c227cd05 Merge branch 'develop' 2022-12-03 19:11:49 +01:00
Fabio Formosa
a8330e062f #62 set jacoco properties 2022-12-03 19:07:29 +01:00
Fabio Formosa
30d0cbf6de Merge pull request #97 from fabioformosa/develop
v4.0.6
2022-12-03 17:17:00 +01:00
Fabio Formosa
3063e08eb3 #62 solved a couple of sonar smells 2022-12-03 17:10:34 +01:00
Fabio Formosa
f7054b160f #62 added missing tests 2022-12-03 16:46:27 +01:00
Fabio Formosa
98b5d0e37a #62 fixed the job class name in the TriggerFiredBundleDTO 2022-12-03 16:21:13 +01:00
Fabio Formosa
6348bac11a #62 added missing tests 2022-12-03 12:19:41 +01:00
Fabio Formosa
c2a26c97a8 #62 added some coverage exclusion 2022-12-02 00:37:56 +01:00
Fabio Formosa
cd6e01109b #62 added some coverage exclusion 2022-12-02 00:32:52 +01:00
Fabio Formosa
7553efdc3b #62 added missing tests 2022-12-02 00:21:23 +01:00
Fabio Formosa
6578dc312a #62 added missing tests 2022-12-01 23:52:48 +01:00
Fabio Formosa
c490a7ab28 #62 added missing tests 2022-12-01 23:48:16 +01:00
Fabio Formosa
0969a406c6 #62 fixed a failing test 2022-12-01 00:55:38 +01:00
Fabio Formosa
eed3021373 #62 fixed a failing test 2022-12-01 00:35:40 +01:00
Fabio Formosa
5b33bd4dca #62 added missing tests 2022-12-01 00:30:47 +01:00
Fabio Formosa
6d22207e27 #62 added missing tests 2022-11-29 23:39:14 +01:00
Fabio Formosa
249cf49873 #62 added missing tests 2022-11-29 23:12:54 +01:00
Fabio Formosa
b17d487c8b #62 added missing tests 2022-11-29 20:29:55 +01:00
Fabio Formosa
fedb2b50b6 #62 clean up 2022-11-29 18:44:46 +01:00
Fabio Formosa
4b18313e2d #62 removed unused security filter 2022-11-23 20:54:37 +01:00
Fabio Formosa
8387f587b3 bump version to 4.0.6-SNAPSHOT 2022-11-20 14:02:45 +01:00
Fabio Formosa
c7b64dbdf3 Update README.md 2022-11-20 14:01:24 +01:00
Fabio Formosa
085d61cf29 Merge pull request #95 from fabioformosa/develop
v4.0.5
2022-11-20 14:00:34 +01:00
Fabio Formosa
e6cf2e9390 bump version to 4.0.5 2022-11-20 13:43:07 +01:00
Fabio Formosa
4bcea96789 Merge remote-tracking branch 'origin/master' into develop 2022-11-20 13:42:52 +01:00
Fabio Formosa
866062bdcb #62 added a missing test for the MisfireTestJob 2022-11-20 13:36:25 +01:00
Fabio Formosa
53a54ddbda #62 added a missing test to the UserController 2022-11-20 13:16:42 +01:00
Fabio Formosa
8bdb85b878 Update README.md 2022-11-20 12:50:26 +01:00
Fabio Formosa
9a26be41a8 #78 excluded lombok annotations from the coverage 2022-11-19 18:59:24 +01:00
Fabio Formosa
5cf39b3861 #78 fixed sonar errors 2022-11-19 18:53:13 +01:00
Fabio Formosa
99d87636d2 #78 removed an unused CORS config 2022-11-19 18:45:47 +01:00
Fabio Formosa
bccd50ac4a #78 put a secureRandom value as default value for the JWT token 2022-11-19 18:28:39 +01:00
Fabio Formosa
3bb30accfd #78 fixed some sonar warnings 2022-11-19 16:34:36 +01:00
Fabio Formosa
5d2b71652c #78 fixed 2 sonar warnings 2022-11-19 16:25:53 +01:00
Fabio Formosa
b70af4dafe Merge remote-tracking branch 'origin/master' into develop 2022-11-19 16:21:07 +01:00
Fabio Formosa
a95cf20c6b bump version to 4.0.5-SNAPSHOT 2022-11-19 13:54:25 +01:00
Fabio Formosa
b58a5dbe9f Update README.md 2022-11-19 13:31:52 +01:00
Fabio Formosa
f7222d65ae moved the nexus plugin under the maven profile 2022-11-19 13:29:14 +01:00
Fabio Formosa
ff43103f37 Merge pull request #89 from fabioformosa/develop
v4.0.4
2022-11-19 13:01:44 +01:00
Fabio Formosa
93ab9c55bc bump version to 4.0.4 2022-11-19 13:00:39 +01:00
Fabio Formosa
28715cdf62 enhanced the info message within the UnsupportedMultipleJobsDialog 2022-11-19 12:40:23 +01:00
Fabio Formosa
bd5276116b fixed the redirect to the login in case of security layer enabled 2022-11-19 12:18:36 +01:00
Fabio Formosa
b20fb9e9c3 bump version to 4.0.4-SNAPSHOT 2022-11-19 12:09:41 +01:00
Fabio Formosa
d3a406a382 Update maven-release.yml 2022-11-19 11:38:35 +01:00
Fabio Formosa
c70fe687e1 Update README.md 2022-11-19 11:37:55 +01:00
Fabio Formosa
fc81685044 Update README.md 2022-11-19 11:22:00 +01:00
Fabio Formosa
6e24d7caf5 bump version to 4.0.3 2022-11-19 11:05:57 +01:00
Fabio Formosa
6786cffb4d fixed the maven-release.yml 2022-11-19 11:05:23 +01:00
153 changed files with 30581 additions and 13311 deletions

2
.dockerignore Normal file
View File

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

View File

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

View File

@@ -7,27 +7,28 @@ 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@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 11
uses: actions/setup-java@v1
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: 11
java-version: 17
distribution: 'zulu' # Alternative distribution options are available.
- name: Cache SonarCloud packages
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache Maven packages
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}

2
.gitignore vendored
View File

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

View File

@@ -1,3 +1,26 @@
## **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
## **v4.0.6**
Minor bug fixes
## **v4.0.5**
Fixed potential security issues
## **v4.0.4**
* Conformed the trigger configuration to the Simple Trigger of Quartz
* **BREAKING CHANGE** Changed accordingly the API and the UI
* Made Quartz Manager embeddable in projects with existing quartz instance, security layer, swagger ui.
## **v3.1.0**
* Added a new persistence module to persist the quartz triggers in a postgresql database
## **v3.0.1**
Quartz-Manager is now publicly available into the maven central repo into 3 different packages.

34
Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
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

@@ -1,6 +1,25 @@
[![Java CI with Maven](https://github.com/fabioformosa/quartz-manager/actions/workflows/maven.yml/badge.svg)](https://github.com/fabioformosa/quartz-manager/actions/workflows/maven.yml)
[![npm CI](https://github.com/fabioformosa/quartz-manager/actions/workflows/npm.yml/badge.svg)](https://github.com/fabioformosa/quartz-manager/actions/workflows/npm.yml)
[![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)
[![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)
    [Quartz Manager UI](https://github.com/fabioformosa/quartz-manager#quartz-manager-ui)
    [Quartz Manager API](https://github.com/fabioformosa/quartz-manager#quartz-manager-api)
[HOW IT WORKS](https://github.com/fabioformosa/quartz-managerhttps://github.com/fabioformosa/quartz-manager#get-started)
    [Quartz Manager Starter API Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-api-lib)
    [Quartz Manager Starter UI Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-ui-lib)
    [Quartz Manager Starter Security Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-security-lib)
    [Quartz Manager Persistence Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-persistence-lib)
[EXAMPLES](https://github.com/fabioformosa/quartz-manager#examples)
[LIMITATIONS](https://github.com/fabioformosa/quartz-manager#limitations)
[ROADMAP](https://github.com/fabioformosa/quartz-manager#roadmap)
[REPOSITORY](https://github.com/fabioformosa/quartz-manager#repository)
[HOW TO CONTRIBUTE](https://github.com/fabioformosa/quartz-manager#how-to-contribute)
[SUPPORT THE PROJECT](https://github.com/fabioformosa/quartz-manager#%EF%B8%8F-support-the-project-%EF%B8%8F)
# QUARTZ MANAGER
In the last decade, the [Quartz Scheduler](http://www.quartz-scheduler.org/) has become the most adopted opensource job scheduling library for Java applications.
@@ -14,13 +33,13 @@ Quartz Manager is a Java library you can import in your Spring-Based Web Applica
The **Quartz Manager UI** is a dashboard in the form of a single-page-application provided by the Quartz Manager Java library itself. You can have it embedded in your project, as well as you get embedded the Swagger UI.
It leverages the websockets to receive in real-time the trigger updates and the outcomes of the job executions.
![](https://github.com/fabioformosa/quartz-manager/blob/develop/quartz-manager-parent/quartz-manager-web-showcase/src/main/resources/quartz-manager-4-screenshot.png)
![](https://github.com/fabioformosa/quartz-manager/blob/master/quartz-manager-parent/quartz-manager-web-showcase/src/main/resources/quartz-manager-4-screenshot.png)
## QUARTZ MANAGER API
Quart-Manager exposes REST endpoints to interact with the Quartz Scheduler. This endpoints are invoked by Quartz Manager UI also.
The REST API are documented by an OpenAPI Specification interface.
![](https://github.com/fabioformosa/quartz-manager/blob/develop/quartz-manager-parent/quartz-manager-web-showcase/src/main/resources/quartz-manager-4-swagger.png)
![](https://github.com/fabioformosa/quartz-manager/blob/master/quartz-manager-parent/quartz-manager-web-showcase/src/main/resources/quartz-manager-4-swagger.png)
# HOW IT WORKS
@@ -37,7 +56,7 @@ In the latter case, in which there isn't an existing quartz instance, you can re
* You can enable a secure layer, if your project doesn't have any, to give access at the API and the UI only to authenticated users.
* You can enable a persistent layer, to persist the config and the progress of your trigger, in a postgresql database.
# QUICK START
# GET STARTED
**Requirements**
Java 9+, Spring Framework 5+ (Spring Boot 2.x)
@@ -52,7 +71,7 @@ In order to decrease the overall configuration time for the project, all modules
Below the list of the quartz-manager modules you can import.
## Quartz Manager API Lib
## Quartz Manager Starter API Lib
This is the only mandatory module of the library.
Add the dependency, make eligible for Quart Manager the job classes and set the props in your `application.properties` file.
@@ -63,12 +82,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.0</version>
<version>4.0.9</version>
</dependency>
```
#### Gradle
```
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-api', version: '4.0.0'
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-api', version: '4.0.9'
```
### Step 2. Quartz Manager Job Classes
@@ -96,11 +115,17 @@ In this way, Quartz Manager is able to collect and display the outcomes at the U
### REST API & OpenAPI Specification
Set the app prop `quartz-manager.oas.enabled=true` you want to expose the OpenApi Specification of the Quartz Manager APIs.
Set the app prop `quartz-manager.oas.enabled=true` if you want to expose the OpenApi Specification of the Quartz Manager APIs.
Reach out the swagger-ui at the URL:
[http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html)
If your project has already an OpenAPI instance and you've set `quartz-manager.oas.enabled=true`, then make sure to add an OpenApiGroup to group the API of your application. Quart Manager exposes its API in group called "Quartz Manager".
If your project has already an OpenAPI instance and you've set `quartz-manager.oas.enabled=true`, then make sure to add an OpenApiGroup to group the API of your application. Quart Manager exposes its API in group called "Quartz Manager". If you use OAS Annotations:
```
@Bean
public GroupedOpenApi simplySpringDemoGroupedOpenApi() {
return GroupedOpenApi.builder().group("myapp").packagesToScan("com.example.myapp").build();
}
```
### QUARTZ SETTINGS
Quartz Manager creates its own instance of a [Quartz Scheduler](http://www.quartz-scheduler.org/).
@@ -122,7 +147,7 @@ The prerequesite is that you've imported a quartz scheduler ver 2.3.x.
You can configure the Quartz instance managed by Quartz Manager through the file `managed-quartz.properties` and your own Quartz instance through the file `quartz.properties`.
If you've created a `SchedulerFactoryBean`, tag it as @Primary to avoid conflicts in-type, since Quartz Manager creates another bean of the same type.
If you've created a `SchedulerFactoryBean`, tag it as `@Primary` to avoid conflicts in-type, since Quartz Manager creates another bean of the same type.
```
@Primary
@@ -135,7 +160,7 @@ If you've created a `SchedulerFactoryBean`, tag it as @Primary to avoid conflict
```
## Quartz Manager UI Lib
## Quartz Manager Starter UI Lib
You can optionally import the following dependency to have the UI Dashboard to interact with the Quartz Manager API.
### Dependency
@@ -145,12 +170,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.0</version>
<version>4.0.9</version>
</dependency>
```
#### Gradle
```
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-ui', version: '4.0.0'
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-ui', version: '4.0.9'
```
### Reach out the UI Console at URL
@@ -158,7 +183,7 @@ if you run locally [http://localhost:8080/quartz-manager-ui/index.html](http://l
## Quartz Manager Security Lib
## Quartz Manager Starter Security Lib
Import this optional dependency, if you want to enable a security layer and allow the access to the REST API and UI only to authenticated users.
The authentication model of Quartz Manager is based on [JWT](https://jwt.io/).
@@ -180,14 +205,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.0</version>
<version>4.0.9</version>
</dependency>
```
#### Gradle
```
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-security', version: '4.0.0'
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-security', version: '4.0.9'
```
@@ -203,11 +228,11 @@ compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-s
|quartz-manager.security.accounts.in-memory.users[0].roles[0] | string | yes | | set the value ADMIN |
## Quart Manager Persistence Lib
## Quart Manager Starter Persistence Lib
By default, Quartz Manager runs with a `org.quartz.simpl.RAMJobStore` that stores your managed scheduler in memory. The RAMJobStore is the simplest store and also the most performant. However it comes with the drawback that all scheduling data are lost if your applications ends or crashes. In case of a restarting of your app, you can't resume the scheduler from the point it stopped.
Import the Quartz Manager Persistence Module if you want to persist your scheduler data.
The pre-requesite is the availability of Postgresql database where Quartz Manager can store its information. You have to provide it a bare database and a postresql user granted for DDL and DML queries. About the DDL, consider that Quartz Manager Persistence will create all tables it needs to work at the bootstrap.
By default, Quartz Manager runs with a `org.quartz.simpl.RAMJobStore` that stores your managed scheduler in memory. The RAMJobStore is the simplest store and also the most performant. However it comes with the drawback that all scheduling data are lost if your applications ends or crashes. In case of a restarting of your app, you can't resume the scheduler from the point it stopped.
Import the Quartz Manager Persistence Module if you want to persist your scheduler data.
The pre-requesite is the availability of Postgresql database where Quartz Manager can store its information. You have to provide it a bare database and a postresql user granted for DDL and DML queries. About the DDL, consider that Quartz Manager Persistence will create all tables, it needs to work, at the bootstrap.
### Dependency
@@ -217,14 +242,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.0</version>
<version>4.0.9</version>
</dependency>
```
#### Gradle
```
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-persistence', version: '4.0.0'
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-persistence', version: '4.0.9'
```
### Quartz Manager Persistence Lib - App Props
@@ -239,13 +264,14 @@ compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-p
## Examples
You can find some examples of different scenarios of projects which import Quartz Manager at the repository [quartz-manager-use-cases] (https://github.com/fabioformosa/quartz-manager-use-cases)
* *simply-spring* - tipical scenario in which you create a minimal spring project from scratch dedicated to launch your scheduled jobs. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager security.
* *simply-spring-no-security* - as simple-spring, without the security
* *existing-security-cookie-based* - It demonstrates how Quartz Manager stays under the security of your project, in case of an auth model based on cookies
* *existing-security-header-based* - It demonstrates how Quartz Manager Security can coexists with another Spring Security Config present in your project
* *existing-quartz* - It demonstrates how to Quartz Manager can coexist with a Quartz instance already present in your project
* *existing-quartz-and-storage* - It demonstrates how to Quartz Manager Persistence can coexist with a Quartz instance already present in your project
You can find some examples of different scenarios of projects which import Quartz Manager at the repository [quartz-manager-use-cases](https://github.com/fabioformosa/quartz-manager-use-cases)
* *simply-spring* - tipical scenario in which you create a minimal spring project from scratch dedicated to launch your scheduled jobs. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Security.
* *simply-spring-no-security* - as simple-spring, without the security. Imported libraries: Quartz Manager API, Quartz Manager UI.
* *existing-security-cookie-based* - It demonstrates how Quartz Manager stays under the security of your project, in case of an auth model based on cookies. Imported libraries: Quartz Manager API, Quartz Manager UI.
* *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

45
cloudbuild.yaml Normal file
View File

@@ -0,0 +1,45 @@
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

@@ -0,0 +1,214 @@
/*
👋 Hi! This file was autogenerated by tslint-to-eslint-config.
https://github.com/typescript-eslint/tslint-to-eslint-config
It represents the closest reasonable ESLint configuration to this
project's original TSLint configuration.
We recommend eventually switching this configuration to extend from
the recommended rulesets in typescript-eslint.
https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md
Happy linting! 💖
*/
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"plugins": [
"eslint-plugin-import",
"@angular-eslint/eslint-plugin",
"@typescript-eslint",
"@typescript-eslint/tslint"
],
"root": true,
"rules": {
"@angular-eslint/component-class-suffix": "error",
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"@angular-eslint/directive-class-suffix": "error",
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/no-host-metadata-property": "error",
"@angular-eslint/no-input-rename": "error",
"@angular-eslint/no-inputs-metadata-property": "error",
"@angular-eslint/no-output-rename": "error",
"@angular-eslint/no-outputs-metadata-property": "error",
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/indent": "off",
"@typescript-eslint/member-delimiter-style": [
"off",
{
"multiline": {
"delimiter": "none",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "variable",
"format": [
"camelCase",
"UPPER_CASE"
],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid"
}
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-inferrable-types": [
"error",
{
"ignoreParameters": true
}
],
"@typescript-eslint/no-shadow": [
"error",
{
"hoist": "all"
}
],
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/quotes": [
"error",
"single"
],
"@typescript-eslint/semi": [
"off",
null
],
"@typescript-eslint/tslint/config": [
"error",
{
"rules": {
"import-spacing": true,
"invoke-injectable": true,
"no-access-missing-member": true,
"templates-use-public": true,
"whitespace": true
}
}
],
"@typescript-eslint/type-annotation-spacing": "off",
"@typescript-eslint/unified-signatures": "error",
"brace-style": [
"error",
"1tbs"
],
"curly": "error",
"dot-notation": "off",
"eol-last": "off",
"eqeqeq": [
"error",
"smart"
],
"guard-for-in": "error",
"id-denylist": "off",
"id-match": "off",
"import/no-deprecated": "warn",
"indent": "off",
"max-len": [
"error",
{
"code": 140
}
],
"no-bitwise": "error",
"no-caller": "error",
"no-console": [
"error",
{
"allow": [
"log",
"warn",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupEnd",
"table",
"dirxml",
"error",
"groupCollapsed",
"Console",
"profile",
"profileEnd",
"timeStamp",
"context"
]
}
],
"no-debugger": "error",
"no-empty": "off",
"no-empty-function": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-redeclare": "error",
"no-restricted-imports": "error",
"no-shadow": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "off",
"no-underscore-dangle": "off",
"no-unused-expressions": "off",
"no-unused-labels": "error",
"no-var": "error",
"prefer-const": "error",
"quotes": "off",
"radix": "error",
"semi": "off",
"spaced-comment": [
"error",
"always",
{
"markers": [
"/"
]
}
],
"valid-typeof": "error"
}
}

View File

@@ -0,0 +1,19 @@
{
"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

@@ -40,3 +40,4 @@ testem.log
# System Files
.DS_Store
Thumbs.db
/.angular/

View File

@@ -0,0 +1,47 @@
src/polyfills.ts
src/typings.d.ts
_test.ts
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store
Thumbs.db
/.angular/

View File

@@ -0,0 +1,11 @@
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"trailingComma": "es5",
"bracketSameLine": true,
"printWidth": 80
}

View File

@@ -3,7 +3,7 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"angular-spring-starter": {
"quartz-manager-ui": {
"root": "",
"prefix": "qrzmng",
"sourceRoot": "src",
@@ -18,6 +18,9 @@
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"allowedCommonJsDependencies": [
"stompjs", "sockjs-client", "moment", "angular2-uuid"
],
"assets": [
"src/assets",
"src/favicon.ico"
@@ -28,6 +31,14 @@
"scripts": []
},
"configurations": {
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"production": {
"budgets": [
{
@@ -38,7 +49,6 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
@@ -56,33 +66,30 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "angular-spring-starter:build"
"browserTarget": "quartz-manager-ui:build:development"
},
"configurations": {
"production": {
"browserTarget": "angular-spring-starter:build:production"
"browserTarget": "quartz-manager-ui:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "angular-spring-starter:build"
"browserTarget": "quartz-manager-ui:build"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"builder": "@angular-eslint/builder:lint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": []
"eslintConfig": ".eslintrc.json",
"lintFilePatterns": ["**/*.spec.ts", "**/*.ts"]
}
}
}
},
"angular-spring-starter-e2e": {
"quartz-manager-ui-e2e": {
"root": "e2e",
"sourceRoot": "e2e",
"projectType": "application",
@@ -91,7 +98,7 @@
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "angular-spring-starter:serve"
"devServerTarget": "quartz-manager-ui:serve"
}
},
"lint": {
@@ -106,7 +113,6 @@
}
}
},
"defaultProject": "angular-spring-starter",
"schematics": {
"@schematics/angular:component": {
"prefix": "qrzmng",
@@ -115,5 +121,8 @@
"@schematics/angular:directive": {
"prefix": "qrzmng"
}
},
"cli": {
"analytics": false
}
}

View File

@@ -1 +1 @@
import 'jest-preset-angular';
import 'jest-preset-angular/setup-jest';

File diff suppressed because it is too large Load Diff

View File

@@ -5,67 +5,84 @@
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build --prod",
"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": "2.0.4",
"@angular-material-components/moment-adapter": "2.0.2",
"@angular/animations": "9.1.4",
"@angular/cdk": "9.2.1",
"@angular/common": "9.1.4",
"@angular/compiler": "9.1.4",
"@angular/core": "9.1.4",
"@angular/flex-layout": "9.0.0-beta.29",
"@angular/forms": "9.1.4",
"@angular/material": "9.2.1",
"@angular/platform-browser": "9.1.4",
"@angular/platform-browser-dynamic": "9.1.4",
"@angular/platform-server": "9.1.4",
"@angular/router": "9.1.4",
"@auth0/angular-jwt": "^4.0.0",
"@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/ng2-stompjs": "^0.6.3",
"@types/jest": "^27.0.2",
"@stomp/rx-stomp": "1.2.0",
"core-js": "2.5.1",
"hammerjs": "2.0.8",
"moment": "^2.29.1",
"net": "^1.0.2",
"rxjs": "6.5.5",
"sockjs-client": "^1.1.1",
"stompjs": "^2.3.3",
"tslib": "^1.10.0",
"zone.js": "~0.10.2"
"tslib": "~2.4.1",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.4",
"@angular-devkit/core": "^9.1.4",
"@angular/cli": "9.1.4",
"@angular/compiler-cli": "9.1.4",
"@angular/language-service": "9.1.4",
"@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",
"@types/jest": "28.1.1",
"@types/node": "^12.11.1",
"codelyzer": "^5.1.2",
"jasmine-core": "2.6.4",
"jasmine-spec-reporter": "4.1.1",
"jest": "^26.0.1",
"jest-preset-angular": "^8.2.0",
"karma": "1.7.1",
"karma-chrome-launcher": "2.1.1",
"karma-cli": "1.0.1",
"karma-coverage-istanbul-reporter": "1.3.0",
"karma-jasmine": "1.1.0",
"karma-jasmine-html-reporter": "0.2.2",
"protractor": "5.1.2",
"ts-node": "3.0.6",
"tslint": "5.7.0",
"typescript": "3.8.3"
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/eslint-plugin-tslint": "^5.46.0",
"@typescript-eslint/parser": "5.43.0",
"codelyzer": "~6.0.2",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-sonarjs": "^0.16.0",
"jasmine-core": "~4.5.0",
"jasmine-spec-reporter": "~7.0.0",
"jest": "28.1.3",
"jest-preset-angular": "~12.2.3",
"karma": "~6.4.1",
"karma-chrome-launcher": "~3.1.1",
"karma-cli": "2.0.0",
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"prettier": "^2.8.1",
"prettier-eslint": "^15.0.1",
"protractor": "~7.0.0",
"ts-node": "10.9.1",
"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 var __karma__: any;
declare var require: any;
declare let __karma__: any;
declare let require: any;
// Prevent Karma from running prematurely.
__karma__.loaded = function () {};

View File

@@ -46,7 +46,7 @@ export const routes: Routes = [
@NgModule({
imports: [RouterModule.forRoot(routes, {
initialNavigation: false
initialNavigation: 'disabled'
})],
exports: [RouterModule],
providers: []

View File

@@ -1,4 +1,4 @@
import { TestBed, async } from '@angular/core/testing';
import {TestBed, async, waitForAsync} from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
@@ -18,7 +18,7 @@ import {
} from './services';
describe('AppComponent', () => {
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,

View File

@@ -43,7 +43,8 @@ import {
SchedulerControlComponent,
LogsPanelComponent,
ProgressPanelComponent,
TriggerListComponent
TriggerListComponent,
SimpleTriggerConfigComponent
} from './components';
import {
@@ -52,14 +53,13 @@ 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';
@@ -67,31 +67,6 @@ 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: () => {
@@ -168,19 +143,13 @@ export function jwtOptionsFactory(apiService: ApiService) {
SchedulerService,
JobService,
TriggerService,
ProgressWebsocketService,
LogsWebsocketService,
ProgressRxWebsocketService,
LogsRxWebsocketService,
AuthService,
ApiService,
UserService,
ConfigService,
MatIconRegistry
// StompService,
// ServerSocket
// {
// provide: StompConfig,
// useValue: stompConfig
// }
],
bootstrap: [AppComponent]
})

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { ComponentFixture, TestBed, inject, waitForAsync} from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
@@ -18,7 +18,7 @@ describe('AccountMenuComponent', () => {
let component: AccountMenuComponent;
let fixture: ComponentFixture<AccountMenuComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,113 @@
import {Subject} from 'rxjs';
import {LogsPanelComponent} from './logs-panel.component';
import {TriggerKey} from '../../model/triggerKey.model';
import {jest} from '@jest/globals';
describe('LogsPanelComponent', () => {
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);
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(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);
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);
component.triggerKey = new TriggerKey('trigger-1', null);
firstMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})});
expect(component.logs.length).toEqual(1);
component.triggerKey = new TriggerKey('trigger-2', null);
expect(component.logs).toEqual([]);
expect(component.selectedTriggerName).toEqual('trigger-2');
expect(component.isWaitingForLogs()).toBeTruthy();
secondMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'second log', threadName: 'worker-2'})});
expect(component.logs.length).toEqual(1);
component.triggerKey = new TriggerKey('trigger-1', null);
expect(component.logs).toEqual([]);
expect(component.selectedTriggerName).toEqual('trigger-1');
expect(component.isWaitingForLogs()).toBeTruthy();
});
it('should clear logs when no trigger is selected', () => {
const messages = new Subject<any>();
const logsRxWebsocketService = {
watch: jest.fn(() => messages.asObservable())
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
component.triggerKey = new TriggerKey('trigger-1', null);
messages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})});
component.triggerKey = null;
expect(component.logs).toEqual([]);
expect(component.selectedTriggerName).toBeNull();
expect(component.isWaitingForLogs()).toBeFalsy();
});
it('should ignore destroy when no topic was selected', () => {
const logsRxWebsocketService = {
watch: jest.fn()
};
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
expect(() => component.ngOnDestroy()).not.toThrow();
});
});

View File

@@ -1,42 +1,84 @@
import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core';
import {Component, Input, 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 {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 {
export class LogsPanelComponent implements OnInit, OnDestroy {
MAX_LOGS = 30;
logs = new Array();
logs = new Array();
selectedTriggerName: string;
topicSubscription;
private selectedTriggerKey: TriggerKey;
constructor(
private logsWebsocketService: LogsWebsocketService,
private logsRxWebsocketService: LogsRxWebsocketService,
private apiService: ApiService
) {
}
@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() {
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
};
private _subscribeToTheTopic = (triggerKey: TriggerKey) => {
this._unsubscribeFromTopic();
this.topicSubscription = this.logsRxWebsocketService.watch(`/topic/logs/${triggerKey.name}`)
.pipe(map((msg: any) => JSON.parse(msg.body)))
.subscribe(this._showNewLog, (err) => {
console.log(err);
// 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 = [];
}
_showNewLog = (logRecord) => {
if (this.logs.length > this.MAX_LOGS) {

View File

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

View File

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

View File

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

View File

@@ -1,84 +1,91 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'
import {ProgressWebsocketService, QuartzManagerWebsocketMessage} from '../../services';
import { Observable } from 'rxjs';
import {Component, Input, OnDestroy, OnInit} from '@angular/core'
import TriggerFiredBundle from '../../model/trigger-fired-bundle.model';
// 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'
import {TriggerKey} from '../../model/triggerKey.model';
import {ProgressRxWebsocketService} from '../../services/progress.rx-websocket.service';
import {map} from 'rxjs/operators';
@Component({
selector: 'progress-panel',
templateUrl: './progress-panel.component.html',
styleUrls: ['./progress-panel.component.scss']
})
export class ProgressPanelComponent implements OnInit {
progress: TriggerFiredBundle = new TriggerFiredBundle();
percentageStr: string;
// // Stream of messages
// private subscription: Subscription;
// public messages: Observable<Message>;
// // Subscription status
// public subscribed: boolean;
// // Array of historic message (bodies)
// public mq: Array<string> = [];
export class ProgressPanelComponent implements OnInit, OnDestroy {
progress: TriggerFiredBundle = ProgressPanelComponent._buildEmptyProgress();
percentageStr: string;
progressUpdated = false;
topicSubscription;
private selectedTriggerKey: TriggerKey;
constructor(
private progressWebsocketService: ProgressWebsocketService,
// private _stompService: StompService,
// private serverSocket : ServerSocket
private progressRxWebsocketService: ProgressRxWebsocketService
) { }
onNewProgressMsg = (receivedMsg: QuartzManagerWebsocketMessage) => {
if (receivedMsg.type === 'SUCCESS') {
const newStatus = receivedMsg.message;
this.progress = newStatus;
this.percentageStr = this.progress.percentage + '%';
}
}
@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(this.onNewProgressMsg, (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();
}
ngOnInit() {
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);
// }
}
}
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;
}
}

View File

@@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {SchedulerControlComponent} from './scheduler-control.component';
import {ApiService, ConfigService, SchedulerService, UserService} from '../../services';
import {HttpClient} from '@angular/common/http';
@@ -13,13 +13,19 @@ 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>;
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatCardModule, MatDividerModule, MatIconModule, HttpClientTestingModule, RouterTestingModule],
declarations: [SchedulerControlComponent],
@@ -38,16 +44,16 @@ describe('SchedulerControlComponent', () => {
it('should display the play button at the beginning since the scheduler is stopped', () => {
expect(component).toBeDefined();
const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'STOPPED', []);
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
const mockScheduler = new Scheduler(schedulerName, schedulerId, stoppedStatus, []);
getSchedulerReq.flush(mockScheduler);
expect(component.scheduler).toEqual(mockScheduler);
expect(component.scheduler.status).toEqual('STOPPED');
expect(component.scheduler.status).toEqual(stoppedStatus);
fixture.detectChanges();
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
const schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
const schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
expect(schedulerBtnDe).toBeTruthy();
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
@@ -56,13 +62,13 @@ describe('SchedulerControlComponent', () => {
it('should switch the button to pause when the scheduler is started', () => {
expect(component).toBeDefined();
const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'STOPPED', []);
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
const mockScheduler = new Scheduler(schedulerName, schedulerId, stoppedStatus, []);
getSchedulerReq.flush(mockScheduler);
fixture.detectChanges();
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
let schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
let schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
expect(schedulerBtnDe).toBeTruthy();
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
expect(playIconDe).toBeTruthy();
@@ -72,7 +78,7 @@ describe('SchedulerControlComponent', () => {
startSchedulerReq.flush(null);
fixture.detectChanges();
schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
const pauseIconDe = schedulerBtnDe.query(By.css('.fa-pause'));
expect(pauseIconDe).toBeTruthy();
@@ -80,13 +86,13 @@ describe('SchedulerControlComponent', () => {
it('should switch the button to play when the scheduler is stopped', () => {
expect(component).toBeDefined();
const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'RUNNING', []);
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
const mockScheduler = new Scheduler(schedulerName, schedulerId, 'RUNNING', []);
getSchedulerReq.flush(mockScheduler);
fixture.detectChanges();
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
let schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
let schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
expect(schedulerBtnDe).toBeTruthy();
const pauseIconDe = schedulerBtnDe.query(By.css('.fa-pause'));
expect(pauseIconDe).toBeTruthy();
@@ -96,7 +102,7 @@ describe('SchedulerControlComponent', () => {
startSchedulerReq.flush(null);
fixture.detectChanges();
schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
expect(playIconDe).toBeTruthy();

View File

@@ -20,7 +20,7 @@
<mat-form-field
[appearance]="enabledTriggerForm && !trigger ? 'standard': 'none'"
class="full-size-input">
<mat-label>Trigger Name *</mat-label>
<mat-label>Trigger Name</mat-label>
<input id="triggerName"
[readonly]="!enabledTriggerForm || trigger"
matInput placeholder="name of the trigger (unique)"
@@ -36,7 +36,7 @@
[appearance]="enabledTriggerForm ? 'standard': 'none'"
class="full-size-input"
>
<mat-label>Job Class *</mat-label>
<mat-label>Job Class</mat-label>
<mat-select id="jobClass" name="jobClass" formControlName="jobClass" [disabled]="!enabledTriggerForm">
<mat-option *ngFor="let job of jobs" [value]="job" style="font-size: 0.8em">
{{job}}
@@ -53,7 +53,7 @@
[appearance]="enabledTriggerForm ? 'standard': 'none'"
class="full-size-input"
>
<mat-label>Misfire Instruction *</mat-label>
<mat-label>Misfire Instruction</mat-label>
<mat-select id="misfireInstruction" name="misfireInstruction" formControlName="misfireInstruction"
[disabled]="!enabledTriggerForm" style="font-size: 0.8em">
<mat-option value="MISFIRE_INSTRUCTION_FIRE_NOW">FIRE NOW</mat-option>
@@ -93,9 +93,6 @@
formControlName="startDate" name="startDate">
<mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #startDatePicker showSpinners="true" showSeconds="true">
<!-- [stepHour]="stepHour"-->
<!-- [stepMinute]="stepMinute" [stepSecond]="stepSecond" [touchUi]="touchUi" [color]="color"-->
<!-- [enableMeridian]="enableMeridian" [disableMinute]="disableMinute" [hideTime]="hideTime">-->
</ngx-mat-datetime-picker>
</mat-form-field>
</div>
@@ -114,9 +111,6 @@
>
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #endDatePicker showSpinners="true" showSeconds="true">
<!-- [stepHour]="stepHour"-->
<!-- [stepMinute]="stepMinute" [stepSecond]="stepSecond" [touchUi]="touchUi" [color]="color"-->
<!-- [enableMeridian]="enableMeridian" [disableMinute]="disableMinute" [hideTime]="hideTime">-->
</ngx-mat-datetime-picker>
</mat-form-field>
<mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerPeriod.errors?.invalidTriggerPeriod" style="font-size: small">
@@ -141,9 +135,6 @@
repeatCount and repeatInterval must be <strong>both</strong> set or unset
</mat-error>
</mat-form-field>
<!-- <mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerRecurrence.errors?.invalidTriggerRecurrence" style="font-size: small">-->
<!-- repeatCount and repeatInterval must be <strong>both</strong> set or unset-->
<!-- </mat-error>-->
</div>
<div>
<mat-form-field

View File

@@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} 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';
@@ -12,7 +12,7 @@ import {FormBuilder, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatNativeDateModule} from '@angular/material/core';
import {MatInputModule} from '@angular/material/input';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {TriggerKey} from '../../model/triggerKey.model';
import {Trigger} from '../../model/trigger.model';
import {JobDetail} from '../../model/jobDetail.model';
@@ -23,15 +23,20 @@ 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>;
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(async( () => {
beforeEach(waitForAsync( () => {
TestBed.configureTestingModule({
imports: [FormsModule, MatFormFieldModule, MatFormFieldModule, MatSelectModule, MatInputModule, BrowserAnimationsModule,
imports: [FormsModule, MatFormFieldModule, MatFormFieldModule, MatSelectModule, MatInputModule, NoopAnimationsModule,
MatNativeDateModule, ReactiveFormsModule,
MatCardModule, MatIconModule, HttpClientTestingModule, RouterTestingModule],
declarations: [SimpleTriggerConfigComponent],
@@ -91,16 +96,16 @@ describe('SimpleTriggerConfig', () => {
fixture.detectChanges();
const getJobsReq = httpTestingController.expectOne(`${CONTEXT_PATH}/jobs`);
getJobsReq.flush(['TestJob']);
getJobsReq.flush([testJobName]);
const componentDe: DebugElement = fixture.debugElement;
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
const submitButton = componentDe.query(By.css(submitButtonSelector));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
setInputValue(componentDe, '#triggerName', 'test-trigger');
expect(component.simpleTriggerReactiveForm.controls.triggerName.value).toEqual('test-trigger');
setInputValue(componentDe, '#triggerName', testTriggerName);
expect(component.simpleTriggerReactiveForm.controls.triggerName.value).toEqual(testTriggerName);
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
setMatSelectValueByIndex(componentDe, '#misfireInstruction', 0);
expect(component.simpleTriggerReactiveForm.controls.misfireInstruction.value).toEqual('MISFIRE_INSTRUCTION_FIRE_NOW');
@@ -111,7 +116,7 @@ describe('SimpleTriggerConfig', () => {
setInputValue(componentDe, '#repeatCount', '1000');
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
setInputValue(componentDe, '#repeatInterval', '2000');
setInputValue(componentDe, repeatIntervalSelector, '2000');
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual(null);
}
@@ -122,18 +127,18 @@ describe('SimpleTriggerConfig', () => {
it('should emit an event when a new trigger is submitted', () => {
const componentDe: DebugElement = fixture.debugElement;
const mockTrigger = new Trigger();
mockTrigger.triggerKeyDTO = new TriggerKey('test-trigger', null);
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: 'TestJob', description: null};
mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null);
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: testJobName, description: null};
mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
openFormAndFillAllMandatoryFields();
setInputValue(componentDe, '#repeatInterval', '2000');
setInputValue(componentDe, repeatIntervalSelector, '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('form button[color="primary"]'));
const submitButton = componentDe.query(By.css(submitButtonSelector));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
let actualNewTrigger;
@@ -141,28 +146,28 @@ describe('SimpleTriggerConfig', () => {
submitButton.nativeElement.click();
const postSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
const postSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
postSimpleTriggerReq.flush(mockTrigger);
expect(actualNewTrigger).toEqual(mockTrigger);
});
it('should not emit an event when an existing trigger is edited', () => {
const mockTriggerKey = new TriggerKey('test-trigger', null);
const mockTriggerKey = new TriggerKey(testTriggerName, null);
component.triggerKey = mockTriggerKey;
fixture.detectChanges();
const mockTrigger = new SimpleTrigger();
mockTrigger.triggerKeyDTO = new TriggerKey('test-trigger', null);
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: 'TestJob', description: null};
mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null);
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/test-trigger`);
const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
getSimpleTriggerReq.flush(mockTrigger);
component.simpleTriggerReactiveForm.setValue({
triggerName: 'test-trigger',
jobClass: 'TestJob',
triggerName: testTriggerName,
jobClass: testJobName,
triggerRecurrence: {
repeatInterval: 2000,
repeatCount: 100,
@@ -178,10 +183,10 @@ describe('SimpleTriggerConfig', () => {
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
setInputValue(componentDe, '#repeatInterval', '4000');
setInputValue(componentDe, repeatIntervalSelector, '4000');
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatInterval).toEqual(4000);
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
const submitButton = componentDe.query(By.css(submitButtonSelector));
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
let actualNewTrigger;
@@ -189,7 +194,7 @@ describe('SimpleTriggerConfig', () => {
submitButton.nativeElement.click();
const putSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
const putSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
putSimpleTriggerReq.flush(mockTrigger);
expect(actualNewTrigger).toBeUndefined();
@@ -198,6 +203,10 @@ describe('SimpleTriggerConfig', () => {
it('should fetch and display the trigger when the triggerKey is passed as input', () => {
const mockTriggerKey = new TriggerKey('my-simple-trigger', null);
component.triggerKey = mockTriggerKey;
component.trigger = new SimpleTrigger();
component.trigger.triggerKeyDTO = mockTriggerKey;
fixture.detectChanges();
const mockTrigger = new Trigger();
@@ -216,8 +225,34 @@ describe('SimpleTriggerConfig', () => {
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
const submitButton = componentDe.query(By.css(submitButtonSelector));
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.triggerKey = null;
expect(component.simpleTriggerReactiveForm.value.triggerName).toBeNull();
expect(component.simpleTriggerReactiveForm.value.jobClass).toBeNull();
expect(component.shouldShowTheTriggerCardContent()).toBeTruthy();
});
it('should display the warning if there are no eligible jobs', () => {
@@ -226,6 +261,9 @@ describe('SimpleTriggerConfig', () => {
getJobsReq.flush([]);
fixture.detectChanges();
component.openTriggerForm();
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
const warningCard = componentDe.query(By.css('#noEligibleJobsAlert'));
expect(warningCard).toBeTruthy();
@@ -237,6 +275,9 @@ describe('SimpleTriggerConfig', () => {
getJobsReq.flush(['sampleJob']);
fixture.detectChanges();
component.openTriggerForm();
fixture.detectChanges();
const componentDe: DebugElement = fixture.debugElement;
const warningCard = componentDe.query(By.css('#noEligibleJobsAlert'));
expect(warningCard).toBeFalsy();

View File

@@ -3,12 +3,11 @@ import {SchedulerService} from '../../services';
import {Scheduler} from '../../model/scheduler.model';
import {SimpleTriggerCommand} from '../../model/simple-trigger.command';
import {SimpleTrigger} from '../../model/simple-trigger.model';
import {SimpleTriggerForm} from '../../model/simple-trigger.form';
import * as moment from 'moment';
import {TriggerKey} from '../../model/triggerKey.model';
import JobService from '../../services/job.service';
import {MisfireInstruction, MisfireInstructionCaption} from '../../model/misfire-instruction.model';
import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators} from '@angular/forms';
import {AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, Validators} from '@angular/forms';
@Component({
selector: 'qrzmng-simple-trigger-config',
@@ -17,9 +16,9 @@ import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors,
})
export class SimpleTriggerConfigComponent implements OnInit {
trigger: SimpleTrigger;
trigger: SimpleTrigger = null;
simpleTriggerReactiveForm: FormGroup = this.formBuilder.group({
simpleTriggerReactiveForm: UntypedFormGroup = this.formBuilder.group({
triggerName: [this.trigger?.triggerKeyDTO.name, Validators.required],
jobClass: [this.trigger?.jobDetailDTO.jobClassName, Validators.required],
triggerPeriod: this.formBuilder.group({
@@ -35,9 +34,8 @@ export class SimpleTriggerConfigComponent implements OnInit {
scheduler: Scheduler;
triggerLoading = true;
triggerLoading = false;
private fetchedTriggers = false;
private triggerInProgress = false;
private selectedTriggerKey: TriggerKey;
@@ -49,8 +47,11 @@ export class SimpleTriggerConfigComponent implements OnInit {
@Output()
onNewTrigger = new EventEmitter<SimpleTrigger>();
@Output()
triggerFormOpenChange = new EventEmitter<boolean>();
constructor(
private formBuilder: FormBuilder,
private formBuilder: UntypedFormBuilder,
private schedulerService: SchedulerService,
private jobService: JobService
) {
@@ -66,18 +67,37 @@ export class SimpleTriggerConfigComponent implements OnInit {
openTriggerForm() {
this.enabledTriggerForm = true;
this.triggerFormOpenChange.emit(this.enabledTriggerForm);
}
private closeTriggerForm() {
this.enabledTriggerForm = false;
this.triggerFormOpenChange.emit(this.enabledTriggerForm);
}
@Input()
set triggerKey(triggerKey: TriggerKey) {
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
this.fetchSelectedTrigger();
if (!triggerKey) {
this.openNewTriggerForm();
} else if (!this.selectedTriggerKey || this.selectedTriggerKey.name !== triggerKey.name) {
this._resetTheTrigger();
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
this.fetchSelectedTrigger();
this.closeTriggerForm();
}
}
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;
@@ -95,7 +115,11 @@ export class SimpleTriggerConfigComponent implements OnInit {
existsATriggerInProgress = (): boolean => this.trigger && this.triggerInProgress;
onResetReactiveForm = () => {
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
if (this.trigger) {
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
} else {
this.simpleTriggerReactiveForm.reset(new SimpleTriggerReactiveForm());
}
this.closeTriggerForm();
};
@@ -104,13 +128,13 @@ export class SimpleTriggerConfigComponent implements OnInit {
this.schedulerService.updateSimpleTriggerConfig : this.schedulerService.saveSimpleTriggerConfig;
const simpleTriggerCommand = this._fromReactiveFormToCommand();
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) {
@@ -119,8 +143,13 @@ export class SimpleTriggerConfigComponent implements OnInit {
this.closeTriggerForm();
}, error => {
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
});
if (this.trigger) {
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
}
this.triggerLoading = false;
}, () => {
this.triggerLoading = false
});
}

View File

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

View File

@@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {ApiService, ConfigService, CONTEXT_PATH, TriggerService} from '../../services';
import {HttpClient} from '@angular/common/http';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
@@ -21,7 +21,7 @@ describe('TriggerListComponent', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatCardModule, MatDialogModule, MatDividerModule,
MatIconModule, MatListModule, HttpClientTestingModule, RouterTestingModule],

View File

@@ -2,13 +2,25 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {TriggerService} from '../../services/trigger.service';
import {TriggerKey} from '../../model/triggerKey.model';
import {SimpleTrigger} from '../../model/simple-trigger.model';
import {MatDialog} from '@angular/material/dialog';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
@Component({
template: 'Multiple jobs not supported yet - Coming Soon...',
template: `
<h3 mat-dialog-title>Coming Soon</h3>
<div mat-dialog-content>
<p>This feature is in roadmap and it will come with the next releases</p>
</div>
<div mat-dialog-actions>
<button mat-button (click)="closeDialog()" style="padding: 0.5em;width: 5em;">Ok</button>
</div>`,
})
// tslint:disable-next-line:component-class-suffix
export class UnsupportedMultipleJobsDialog {
constructor(public dialogRef: MatDialogRef<UnsupportedMultipleJobsDialog>) {
}
closeDialog(): void {
this.dialogRef.close();
}
}
@Component({
@@ -69,16 +81,17 @@ export class TriggerListComponent implements OnInit {
}
onNewTriggerBtnClicked() {
if (this.triggerKeys && this.triggerKeys.length > 0) {
this.dialog.open(UnsupportedMultipleJobsDialog)
} else {
this.onNewTriggerClicked.emit();
}
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.selectedTrigger = newTrigger.triggerKeyDTO;
this.selectTrigger(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,9 +3,8 @@ export * from './user.service';
export * from './config.service';
export * from './auth.service';
export * from './scheduler.service';
export * from './websocket.service';
export * from './progress.websocket.service';
export * from './logs.websocket.service';
export * from './progress.rx-websocket.service';
export * from './logs.rx-websocket.service';
export * from './trigger.service'
export * from './job.service'

View File

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,23 @@
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

@@ -1,12 +0,0 @@
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

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,23 @@
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

@@ -1,12 +0,0 @@
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

@@ -0,0 +1,12 @@
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,15 +35,16 @@ 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;
this.router.initialNavigation();
return;
}
if (httpErrorResponse.status < 200 || httpErrorResponse.status > 399)
if (httpErrorResponse.status !== 401 && (httpErrorResponse.status < 200 || httpErrorResponse.status > 399)) {
this.router.navigateByUrl('/error');
}
});
}

View File

@@ -1,136 +0,0 @@
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,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {GenericErrorComponent} from './genericError.component';
@@ -6,7 +6,7 @@ describe('GenericComponent', () => {
let component: GenericErrorComponent;
let fixture: ComponentFixture<GenericErrorComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ GenericErrorComponent ]
})

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import { ForbiddenComponent } from './forbidden.component';
@@ -6,7 +6,7 @@ describe('ForbiddenComponent', () => {
let component: ForbiddenComponent;
let fixture: ComponentFixture<ForbiddenComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ForbiddenComponent ]
})

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import { LoginComponent } from './login.component';
import { RouterTestingModule } from '@angular/router/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@@ -16,7 +16,7 @@ describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [LoginComponent],
imports: [

View File

@@ -1,5 +1,5 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {UntypedFormBuilder, UntypedFormGroup, Validators} from '@angular/forms';
import {ActivatedRoute, Router} from '@angular/router';
import {DisplayMessage} from '../../shared/models/display-message';
import {Subject} from 'rxjs';
@@ -15,7 +15,7 @@ import {AuthService, UserService} from '../../services';
export class LoginComponent implements OnInit, OnDestroy {
title = 'Login';
githubLink = 'https://github.com/fabioformosa/quartz-manager';
form: FormGroup;
form: UntypedFormGroup;
submitted = false;
@@ -29,7 +29,7 @@ export class LoginComponent implements OnInit, OnDestroy {
private authService: AuthService,
private router: Router,
private route: ActivatedRoute,
private formBuilder: FormBuilder
private formBuilder: UntypedFormBuilder
) {
}

View File

@@ -19,18 +19,25 @@
<div fxFlex="1 1 350px">
<div fxLayout="row" fxFill>
<div fxLayout="column" fxFill>
<qrzmng-simple-trigger-config fxFill
[triggerKey]="selectedTriggerKey"
(onNewTrigger)="onNewTriggerCreated($event)">
</qrzmng-simple-trigger-config>
<qrzmng-simple-trigger-config fxFill
[triggerKey]="selectedTriggerKey"
(triggerFormOpenChange)="setNewTriggerFormOpened($event)"
(onNewTrigger)="onNewTriggerCreated($event)">
</qrzmng-simple-trigger-config>
</div>
</div>
</div>
<div fxFlex="1 1 auto" style="margin-left: 20px;">
<div fxFlex="1 1 auto" fxLayout="column" fxLayoutAlign="start stretch" fxLayoutGap="6px">
<progress-panel></progress-panel>
<logs-panel fxFlex="1 1 auto" fxFill></logs-panel>
<progress-panel
[triggerKey]=selectedTriggerKey
>
</progress-panel>
<logs-panel fxFlex="1 1 auto" fxFill
[triggerKey]=selectedTriggerKey
>
</logs-panel>
</div>
</div>

View File

@@ -1,8 +1,4 @@
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';
@@ -32,15 +28,25 @@ export class ManagerComponent implements OnInit {
}
onNewTriggerRequested() {
this.triggerConfigComponent.openTriggerForm();
this.selectedTriggerKey = null;
this.newTriggerFormOpened = true;
if (this.triggerConfigComponent) {
this.triggerConfigComponent.openNewTriggerForm();
}
}
onNewTriggerCreated(newTrigger: SimpleTrigger) {
this.triggerListComponent.onNewTrigger(newTrigger);
this.newTriggerFormOpened = false;
}
setSelectedTrigger(triggerKey: TriggerKey) {
this.selectedTriggerKey = triggerKey;
this.newTriggerFormOpened = false;
}
setNewTriggerFormOpened(opened: boolean) {
this.newTriggerFormOpened = opened;
}
}

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import { NotFoundComponent } from './not-found.component';
@@ -6,7 +6,7 @@ describe('NotFoundComponent', () => {
let component: NotFoundComponent;
let fixture: ComponentFixture<NotFoundComponent>;
beforeEach(async(() => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [NotFoundComponent]
})

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

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

View File

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

View File

@@ -12,6 +12,9 @@
"curly": true,
"eofline": true,
"forin": true,
"deprecation": {
"severity": "warning"
},
"import-blacklist": [true],
"import-spacing": true,
"indent": [
@@ -53,7 +56,6 @@
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [

View File

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

View File

@@ -58,13 +58,13 @@ Replace the dummy job (class: `it.fabioformosa.quartzmanager.jobs.SampleJob`) wi
**Backend Stack** Java 9, Spring Boot 2.5.6 (Spring MVC 5.3.12, Spring Security 5.5.3), Quartz Scheduler 2.3.2
**Frontend** Angular 9.1.4, Web-Socket (stompjs 2.3.3)
**Frontend** Angular 14.2.12, Web-Socket (stompjs 2.3.3)
**Style** Angular Material 9, FontAwesome 5
**Style** Angular Material 14, FontAwesome 5
Starting from Quartz Manager v2.x.x, the new structure of project is:
* Multi-module maven project: REST API backend
* Angular 9: Single Page Application frontend
* Angular 14: Single Page Application frontend
(The first version of quartz manager was a monolithic backend that provided also frontend developed with angularjs 1.6.x. You can find it at the branch 1.x.x)

View File

@@ -0,0 +1,2 @@
config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true

View File

@@ -10,7 +10,7 @@
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.2</version>
<version>4.0.11-SNAPSHOT</version>
<packaging>pom</packaging>
@@ -41,9 +41,9 @@
</developers>
<properties>
<sonar.organization>fabioformosa</sonar.organization>
<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,8 +51,19 @@
<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>**/SpringApplicationTest.java, **/QuartManagerApplicationTests.java</sonar.exclusions>
<sonar.exclusions>
**/SpringApplicationTest.java, **/QuartManagerApplicationTests.java, **/MisfireTestJob.java
</sonar.exclusions>
<sonar.coverage.exclusions>
OpenApiConfig.java,
**/SecurityOpenApiConfig.java, **/QuartzModuleProperties.java, **/QuartzManagerDemoApplication.java,
**/ServletInitializer.java, **/SessionController.java, **/HealthCheckController.java,
**/WebShowcaseOpenApiConfig.java, **/MisfireTestJob.java, **/PersistenceConfig.java,
**/QuartzManagerSecurityConfig.java
</sonar.coverage.exclusions>
</properties>
<modules>
@@ -69,27 +80,32 @@
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-common</artifactId>
<version>4.0.2</version>
<version>4.0.11-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-api</artifactId>
<version>4.0.2</version>
<version>4.0.11-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-security</artifactId>
<version>4.0.2</version>
<version>4.0.11-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-persistence</artifactId>
<version>4.0.2</version>
<version>4.0.11-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-starter-ui</artifactId>
<version>4.0.2</version>
<version>4.0.11-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -124,10 +140,28 @@
<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>
<version>${jacoco-maven-plugin.version}</version>
<configuration>
<excludes>
<exclude>**/OpenApiConfig.class</exclude>
<exclude>**/SecurityOpenApiConfig.class</exclude>
<exclude>**/QuartzModuleProperties.class</exclude>
<exclude>**/QuartzManagerDemoApplication.class</exclude>
<exclude>**/ServletInitializer.class</exclude>
<exclude>**/SessionController.class</exclude>
<exclude>**/HealthCheckController.class</exclude>
<exclude>**/WebShowcaseOpenApiConfig.class</exclude>
<exclude>**/MisfireTestJob.class</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
@@ -141,6 +175,13 @@
<goal>report</goal>
</goals>
</execution>
<execution>
<id>report-aggregate</id>
<goals>
<goal>report-aggregate</goal>
</goals>
<phase>verify</phase>
</execution>
</executions>
</plugin>
@@ -179,17 +220,6 @@
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>${nexus-staging-maven-plugin.version}</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
@@ -222,17 +252,27 @@
</activation>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<id>maven-central-release</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/
</url>
<id>maven-central-release</id>
<url>https://central.sonatype.com</url>
</repository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.7.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>maven-central-release</publishingServerId>
<autoPublish>true</autoPublish>
<waitUntil>published</waitUntil>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>

View File

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

View File

@@ -1,10 +1,12 @@
package it.fabioformosa.quartzmanager.api.common.properties;
import lombok.Data;
import lombok.Generated;
import java.util.Properties;
@Data
@Generated
public class QuartzModuleProperties {
private Properties properties = new Properties();

View File

@@ -1,13 +1,20 @@
package it.fabioformosa.quartzmanager.api.common.utils;
import lombok.Getter;
import java.util.function.Function;
/**
*
* @param <R> success type
*/
@Getter
public class Try<R> {
private final Throwable failure;
private final R success;
public Try(Throwable failure, R success) {
private Try(Throwable failure, R success) {
this.failure = failure;
this.success = success;
}
@@ -16,11 +23,11 @@ public class Try<R> {
return success;
}
public static <R> Try<R> success(R r){
private static <R> Try<R> success(R r){
return new Try<>(null, r);
}
public static <R> Try<R> failure(Throwable e){
private static <R> Try<R> failure(Throwable e){
return new Try<>(e, null);
}
@@ -38,14 +45,6 @@ public class Try<R> {
return t -> Try.with(checkedFunction).apply(t).getSuccess();
}
public boolean isSuccess(){
return this.failure == null;
}
public boolean isFailure(){
return this.failure != null;
}
@FunctionalInterface
public static interface CheckedFunction<T, R> {
R apply(T t) throws java.lang.Exception;

View File

@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.Date;
class DateUtilsTest {
@@ -17,4 +18,20 @@ class DateUtilsTest {
Assertions.assertThat(convertedLocalDateTime).isEqualTo(originalLocalDateTime);
}
@Test
void givenALocalDatetime_whenTheAddHoursToNowIsCalled_shouldReturnAFutureDate(){
Calendar calendar = Calendar.getInstance();
Date futureDate = DateUtils.addHoursToNow(1);
calendar.add(Calendar.HOUR_OF_DAY, 1);
calendar.add(Calendar.MINUTE, -1);
Date hourStartingAround = calendar.getTime();
calendar.add(Calendar.HOUR_OF_DAY, 1);
calendar.add(Calendar.MINUTE, 2);
Date hourEndingAround = calendar.getTime();
Assertions.assertThat(futureDate).isBetween(hourStartingAround, hourEndingAround);
}
}

View File

@@ -0,0 +1,42 @@
package it.fabioformosa.quartzmanager.api.common.utils;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Optional;
class TryTest {
String raiseExceptionIfHello(String greetings) throws Exception {
if("hello".equals(greetings))
throw new Exception("hello");
return greetings;
}
@Test
void givenAFunctionWhichRaisesAnException_whenSneakyThrowIsCalled_thenItReturnsNull(){
String hello = Optional.of("hello").map(Try.sneakyThrow(this::raiseExceptionIfHello)).orElse(null);
Assertions.assertThat(hello).isNull();
}
@Test
void givenAFunctionWhichDoesntRaiseAnException_whenSneakyThrowIsCalled_thenItReturnsTheValue(){
String hello = Optional.of("not hello").map(Try.sneakyThrow(this::raiseExceptionIfHello)).orElse(null);
Assertions.assertThat(hello).isEqualTo("not hello");
}
@Test
void givenAFunctionWhichRaisesAnException_whenTryWithIsCalled_thenItReturnsAFailureObj(){
Try<String> aTry = Optional.of("hello").map(greet -> Try.with(this::raiseExceptionIfHello).apply(greet)).get();
Assertions.assertThat(aTry.getFailure()).isNotNull();
Assertions.assertThat(aTry.getFailure().getMessage()).isEqualTo("hello");
}
@Test
void givenAFunctionWhichDoesntRaiseAnException_whenTryWithIsCalled_thenItReturnsTheValue(){
Try<String> aTry = Optional.of("not hello").map(greet -> Try.with(this::raiseExceptionIfHello).apply(greet)).get();
Assertions.assertThat(aTry.getFailure()).isNull();
Assertions.assertThat(aTry.getSuccess()).isEqualTo("not hello");
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>it.fabioformosa.quartz-manager</groupId>
<artifactId>quartz-manager-parent</artifactId>
<version>4.0.2</version>
<version>4.0.11-SNAPSHOT</version>
</parent>
<artifactId>quartz-manager-starter-api</artifactId>
@@ -20,7 +20,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<springdoc-openapi.version>1.5.12</springdoc-openapi.version>
<java.version>9</java.version>
<sonar.exclusions>**/QuartManagerApplicationTests.java</sonar.exclusions>
<sonar.exclusions>**/QuartManagerApplicationTests.java, **/OpenApiConfig.java</sonar.exclusions>
</properties>
<dependencies>
@@ -106,6 +106,10 @@
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<!-- QUARTZ -->
<dependency>
@@ -118,34 +122,6 @@
<version>1.3.2</version>
</dependency>
<!-- Reactor -->
<!-- <dependency>-->
<!-- <groupId>io.projectreactor</groupId>-->
<!-- <artifactId>reactor-core</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>io.projectreactor</groupId>-->
<!-- <artifactId>reactor-net</artifactId>-->
<!-- <version>2.0.8.RELEASE</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>io.projectreactor.spring</groupId>-->
<!-- <artifactId>reactor-spring-context</artifactId>-->
<!-- <version>2.0.7.RELEASE</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>io.netty</groupId>-->
<!-- <artifactId>netty-all</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-aop</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<!-- OAS -->
<dependency>
<groupId>org.springdoc</groupId>

View File

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths;
import lombok.Generated;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.GroupedOpenApi;
import org.springdoc.core.customizers.OpenApiCustomiser;
@@ -19,6 +20,7 @@ import java.util.Optional;
@Slf4j
@Configuration
@Generated
public class OpenApiConfig {
@ConditionalOnProperty(name = "quartz-manager.oas.enabled")

View File

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

View File

@@ -1,5 +1,6 @@
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;
@@ -14,14 +15,16 @@ public class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/job");
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
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/quartz-manager/logs").setAllowedOrigins("/**").withSockJS();
registry.addEndpoint("/quartz-manager/progress").setAllowedOrigins("/**").withSockJS();
registry.addEndpoint(QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/logs").setAllowedOrigins("/**").withSockJS();
registry.addEndpoint(QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/progress").setAllowedOrigins("/**").withSockJS();
}
}

View File

@@ -16,10 +16,11 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.stream.Collectors;
@RequestMapping(QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/jobs")
@RequestMapping(JobController.JOB_CONTROLLER_BASE_URL)
@SecurityRequirement(name = OpenAPIConfigConsts.QUARTZ_MANAGER_SEC_OAS_SCHEMA)
@RestController
public class JobController {
public static final String JOB_CONTROLLER_BASE_URL = QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/jobs";
private final JobService jobService;
public JobController(JobService jobService) {

View File

@@ -8,7 +8,10 @@ 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

@@ -23,6 +23,5 @@ public class JobKeyToJobDetailDTO extends AbstractBaseConverterToDTO<JobKey, Job
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
jobDetailDTO.setJobClassName(jobDetail.getJobClass().getName());
jobDetailDTO.setDescription(jobDetail.getDescription());
//jobDetail.getJobDataMap();
}
}

View File

@@ -17,7 +17,8 @@ public class SchedulerToSchedulerDTO extends AbstractBaseConverterToDTO<Schedule
protected void convert(Scheduler source, SchedulerDTO target) {
target.setName(source.getSchedulerName());
target.setInstanceId(source.getSchedulerInstanceId());
target.setTriggerKeys(source.getTriggerKeys(GroupMatcher.anyTriggerGroup()));
if(!source.isShutdown())
target.setTriggerKeys(source.getTriggerKeys(GroupMatcher.anyTriggerGroup()));
target.setStatus(buildTheSchedulerStatus(source));
}

View File

@@ -2,6 +2,7 @@ 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;
@@ -19,6 +20,8 @@ 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,6 +14,7 @@ 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

@@ -2,6 +2,7 @@ package it.fabioformosa.quartzmanager.api.dto;
import it.fabioformosa.quartzmanager.api.enums.SchedulerStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.quartz.TriggerKey;
@@ -11,6 +12,7 @@ import java.util.Set;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class SchedulerDTO {
private String name;
private String instanceId;

View File

@@ -3,8 +3,9 @@ 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
@@ -18,4 +19,7 @@ public class SimpleTriggerInputDTO extends TriggerCommandDTO implements TriggerR
@Positive
private Long repeatInterval;
@Nullable
private Map<String, ?> jobDataMap;
}

View File

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

View File

@@ -1,7 +1,13 @@
package it.fabioformosa.quartzmanager.api.dto;
import lombok.*;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TriggerFiredBundleDTO {
private int timesTriggered;
@@ -18,22 +24,6 @@ public class TriggerFiredBundleDTO {
private String jobClass;
public Date getFinalFireTime() {
return finalFireTime;
}
public String getJobClass() {
return jobClass;
}
public String getJobKey() {
return jobKey;
}
public Date getNextFireTime() {
return nextFireTime;
}
public int getPercentage() {
if (this.repeatCount <= 0)
return -1;
@@ -41,44 +31,4 @@ public class TriggerFiredBundleDTO {
* 100);
}
public Date getPreviousFireTime() {
return previousFireTime;
}
public int getRepeatCount() {
return repeatCount;
}
public int getTimesTriggered() {
return timesTriggered;
}
public void setFinalFireTime(Date finalFireTime) {
this.finalFireTime = finalFireTime;
}
public void setJobClass(String jobClass) {
this.jobClass = jobClass;
}
public void setJobKey(String jobKey) {
this.jobKey = jobKey;
}
public void setNextFireTime(Date nextFireTime) {
this.nextFireTime = nextFireTime;
}
public void setPreviousFireTime(Date previousFireTime) {
this.previousFireTime = previousFireTime;
}
public void setRepeatCount(int repeatCount) {
this.repeatCount = repeatCount;
}
public void setTimesTriggered(int timesTriggered) {
this.timesTriggered = timesTriggered;
}
}

View File

@@ -8,11 +8,8 @@ public class ResourceConflictException extends RuntimeException {
private static final long serialVersionUID = 1791564636123821405L;
private final Long resourceId;
public ResourceConflictException(Long resourceId, String message) {
super(message);
this.resourceId = resourceId;
super("Conflict on resourceID " + resourceId + " " + message);
}
}

View File

@@ -1,16 +1,7 @@
package it.fabioformosa.quartzmanager.api.exceptions;
import lombok.Getter;
import lombok.ToString;
@ToString
@Getter
public class TriggerNotFoundException extends Exception {
private final String name;
public TriggerNotFoundException(String name) {
super("Trigger with name " + name + " not found!");
this.name = name;
}
}

View File

@@ -38,11 +38,13 @@ 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(logMsg);
webSocketLogsNotifier.send(triggerName, logMsg);
TriggerFiredBundleDTO triggerFiredBundleDTO = WebSocketProgressNotifier.buildTriggerFiredBundle(jobExecutionContext);
webSocketProgressNotifier.send(triggerFiredBundleDTO);
webSocketProgressNotifier.send(triggerName, triggerFiredBundleDTO);
}
}

View File

@@ -1,5 +1,8 @@
package it.fabioformosa.quartzmanager.api.jobs.entities;
import lombok.Data;
import lombok.ToString;
import java.util.Date;
/**
@@ -8,6 +11,8 @@ import java.util.Date;
* @author Fabio.Formosa
*
*/
@Data
@ToString
public class LogRecord {
public enum LogType {
@@ -27,41 +32,4 @@ public class LogRecord {
date = new Date();
}
public Date getDate() {
return date;
}
public String getMessage() {
return message;
}
public String getThreadName() {
return threadName;
}
public LogType getType() {
return type;
}
public void setDate(Date date) {
this.date = date;
}
public void setMessage(String msg) {
message = msg;
}
public void setThreadName(String threadName) {
this.threadName = threadName;
}
public void setType(LogType type) {
this.type = type;
}
@Override
public String toString() {
return "LogRecord [date=" + date + ", type=" + type + ", message=" + message + "]";
}
}

View File

@@ -1,7 +1,6 @@
package it.fabioformosa.quartzmanager.api.services;
import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerCommandDTO;
import it.fabioformosa.quartzmanager.api.dto.TriggerDTO;
import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerDTO;
import it.fabioformosa.quartzmanager.api.exceptions.TriggerNotFoundException;
import org.quartz.*;
@@ -34,7 +33,7 @@ public class SimpleTriggerService extends AbstractSchedulerService {
return conversionService.convert(newSimpleTrigger, SimpleTriggerDTO.class);
}
public TriggerDTO rescheduleSimpleTrigger(SimpleTriggerCommandDTO triggerCommandDTO) throws SchedulerException {
public SimpleTriggerDTO rescheduleSimpleTrigger(SimpleTriggerCommandDTO triggerCommandDTO) throws SchedulerException {
SimpleTrigger newSimpleTrigger = conversionService.convert(triggerCommandDTO, SimpleTrigger.class);
TriggerKey triggerKey = TriggerKey.triggerKey(triggerCommandDTO.getTriggerName());

View File

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

View File

@@ -20,8 +20,8 @@ public class WebSocketProgressNotifier implements WebhookSender<TriggerFiredBund
private SimpMessageSendingOperations messagingTemplate;
@Override
public void send(TriggerFiredBundleDTO triggerFiredBundleDTO) {
messagingTemplate.convertAndSend(TOPIC_PROGRESS, triggerFiredBundleDTO);
public void send(String triggerName, TriggerFiredBundleDTO triggerFiredBundleDTO) {
messagingTemplate.convertAndSend(TOPIC_PROGRESS + "/" + triggerName, triggerFiredBundleDTO);
}
public static TriggerFiredBundleDTO buildTriggerFiredBundle(JobExecutionContext jobExecutionContext) {
@@ -43,7 +43,7 @@ public class WebSocketProgressNotifier implements WebhookSender<TriggerFiredBund
JobDetail jobDetail = jobExecutionContext.getJobDetail();
triggerFiredBundleDTO.setJobKey(jobDetail.getKey().getName());
triggerFiredBundleDTO.setJobClass(trigger.getClass().getSimpleName());
triggerFiredBundleDTO.setJobClass(jobDetail.getJobClass().getName());
return triggerFiredBundleDTO;
}

View File

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

View File

@@ -7,8 +7,10 @@ import org.quartz.Scheduler;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
class SchedulerConfigTest {
@@ -16,11 +18,8 @@ class SchedulerConfigTest {
public static final String QUARTZ_SCHEDULER_DEFAULT_NAME = "QuartzScheduler";
@Test
void givenASchedulerName_whenTheSchedulerIsInstatiated_thenTheSchedulerHasThatName() throws Exception {
QuartzModuleProperties quartzModuleProperties = new QuartzModuleProperties();
quartzModuleProperties.getProperties().put("org.quartz.scheduler.instanceName", TEST_SCHEDULER_NAME);
List<QuartzModuleProperties> quartzModulePropertiesList = new ArrayList<>();
quartzModulePropertiesList.add(quartzModuleProperties);
void givenASchedulerName_whenTheSchedulerIsInstantiated_thenTheSchedulerHasThatName() throws Exception {
List<QuartzModuleProperties> quartzModulePropertiesList = getQuartzModulePropertiesWithASchedulerName(TEST_SCHEDULER_NAME);
SchedulerConfig schedulerConfig = new SchedulerConfig(quartzModulePropertiesList);
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.refresh();
@@ -31,8 +30,16 @@ class SchedulerConfigTest {
Assertions.assertThat(scheduler.getSchedulerName()).isEqualTo(TEST_SCHEDULER_NAME);
}
private static List<QuartzModuleProperties> getQuartzModulePropertiesWithASchedulerName(String schedulerName) {
QuartzModuleProperties quartzModuleProperties = new QuartzModuleProperties();
quartzModuleProperties.getProperties().put("org.quartz.scheduler.instanceName", schedulerName);
List<QuartzModuleProperties> quartzModulePropertiesList = new ArrayList<>();
quartzModulePropertiesList.add(quartzModuleProperties);
return quartzModulePropertiesList;
}
@Test
void givenNoSchedulerName_whenTheSchedulerIsInstatiated_thenTheSchedulerHasTheDefaultName() throws Exception {
void givenNoSchedulerName_whenTheSchedulerIsInstantiated_thenTheSchedulerHasTheDefaultName() throws Exception {
QuartzModuleProperties quartzModuleProperties = new QuartzModuleProperties();
List<QuartzModuleProperties> quartzModulePropertiesList = new ArrayList<>();
quartzModulePropertiesList.add(quartzModuleProperties);
@@ -46,4 +53,32 @@ class SchedulerConfigTest {
Assertions.assertThat(scheduler.getSchedulerName()).isEqualTo(QUARTZ_SCHEDULER_DEFAULT_NAME);
}
@Test
void givenAManagedProperties_whenTheSchedulerIsInstantiated_thenTheManagedPropsHavePriority() throws Exception {
List<QuartzModuleProperties> quartzModulePropertiesList = getQuartzModulePropertiesWithASchedulerName(TEST_SCHEDULER_NAME);
SchedulerConfig schedulerConfig = new SchedulerConfig(quartzModulePropertiesList);
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.refresh();
Properties managedProps = new Properties();
String overridden_scheduler_name = "OVERRIDDEN_SCHEDULER_NAME";
managedProps.put("org.quartz.scheduler.instanceName", overridden_scheduler_name);
SchedulerFactoryBean schedulerFactoryBean = schedulerConfig.schedulerFactoryBean(schedulerConfig.jobFactory(applicationContext), managedProps);
schedulerFactoryBean.afterPropertiesSet();
Scheduler scheduler = schedulerFactoryBean.getScheduler();
Assertions.assertThat(scheduler.getSchedulerName()).isEqualTo(overridden_scheduler_name);
}
@Test
void givenAnEmptyManagedPropFile_whenSchedulerConfigRuns_thenItReturnsAnEmptyPropList() throws IOException {
List<QuartzModuleProperties> quartzModulePropertiesList = getQuartzModulePropertiesWithASchedulerName(TEST_SCHEDULER_NAME);
SchedulerConfig schedulerConfig = new SchedulerConfig(quartzModulePropertiesList);
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.refresh();
Properties properties = schedulerConfig.quartzProperties();
Assertions.assertThat(properties).isEmpty();
}
}

View File

@@ -0,0 +1,46 @@
package it.fabioformosa.quartzmanager.api.controllers;
import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests;
import it.fabioformosa.quartzmanager.api.controllers.utils.TestUtils;
import it.fabioformosa.quartzmanager.api.jobs.SampleJob;
import it.fabioformosa.quartzmanager.api.services.JobService;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
@WebMvcTest(controllers = SimpleTriggerController.class, properties = {
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
})
class JobControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private JobService jobService;
@Test
void whenGetListIsCalled_thenTheSimpleJobIsReturned() throws Exception {
Mockito.when(jobService.getJobClasses()).thenReturn(List.of(SampleJob.class));
List<String> expectedJobs = List.of(SampleJob.class.getName());
mockMvc.perform(get(JobController.JOB_CONTROLLER_BASE_URL)
.contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(expectedJobs)));
Mockito.verify(jobService, Mockito.times(1)).getJobClasses();
}
}

View File

@@ -0,0 +1,28 @@
package it.fabioformosa.quartzmanager.api.controllers;
import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
@WebMvcTest(controllers = SimpleTriggerController.class, properties = {
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
})
class ResourceConflictControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void whenAResourceConflictExceptionIsRaised_thenTheExceptionHandlerReturns409() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders.post(TestController.TEST_CONTROLLER_BASE_URL + "/test-conflict")
.contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isConflict());
}
}

View File

@@ -0,0 +1,89 @@
package it.fabioformosa.quartzmanager.api.controllers;
import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests;
import it.fabioformosa.quartzmanager.api.controllers.utils.TestUtils;
import it.fabioformosa.quartzmanager.api.dto.SchedulerDTO;
import it.fabioformosa.quartzmanager.api.enums.SchedulerStatus;
import it.fabioformosa.quartzmanager.api.services.SchedulerService;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
@WebMvcTest(controllers = SimpleTriggerController.class, properties = {
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
})
class SchedulerControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private SchedulerService schedulerService;
@Test
void whenTheGetIsCalled_thenTheSchedulerServiceIsReturned() throws Exception {
SchedulerDTO schedulerDTO = SchedulerDTO.builder()
.name("TEST_SCHEDULER")
.instanceId("testSchedulerId")
.status(SchedulerStatus.STOPPED)
.build();
Mockito.when(schedulerService.getScheduler()).thenReturn(schedulerDTO);
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(schedulerDTO)));
Mockito.verify(schedulerService).getScheduler();
}
@Test
void givenAScheduler_whenTheGetPausedIsCalled_then2xxReturned() throws Exception {
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/pause")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNoContent())
.andExpect(MockMvcResultMatchers.content().string(""));
Mockito.verify(schedulerService).standby();
}
@Test
void givenAScheduler_whenTheGetResumedIsCalled_then2xxReturned() throws Exception {
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/resume")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNoContent())
.andExpect(MockMvcResultMatchers.content().string(""));
Mockito.verify(schedulerService).start();
}
@Test
void givenAScheduler_whenTheGetRunIsCalled_then2xxReturned() throws Exception {
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/run")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNoContent())
.andExpect(MockMvcResultMatchers.content().string(""));
Mockito.verify(schedulerService).start();
}
@Test
void givenAScheduler_whenTheGetStoppedIsCalled_then2xxReturned() throws Exception {
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/stop")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNoContent())
.andExpect(MockMvcResultMatchers.content().string(""));
Mockito.verify(schedulerService).shutdown();
}
}

View File

@@ -21,7 +21,8 @@ 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.*;
@@ -68,6 +69,10 @@ 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())
@@ -75,6 +80,7 @@ class SimpleTriggerControllerTest {
.misfireInstruction(MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW)
.repeatCount(5)
.repeatInterval(1000L * 60 * 60)
.jobDataMap(triggerJobDataMap)
.build();
}

View File

@@ -0,0 +1,19 @@
package it.fabioformosa.quartzmanager.api.controllers;
import it.fabioformosa.quartzmanager.api.exceptions.ResourceConflictException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping(TestController.TEST_CONTROLLER_BASE_URL)
@RestController
public class TestController {
public static final String TEST_CONTROLLER_BASE_URL = "/test-controller";
@PostMapping("/test-conflict")
public void raiseConflictException(){
throw new ResourceConflictException(1000L, "another entity has found with the same ID");
}
}

View File

@@ -2,12 +2,23 @@ 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))
@@ -28,7 +39,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())
@@ -48,10 +59,11 @@ 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)
@@ -71,6 +83,44 @@ 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

@@ -0,0 +1,71 @@
package it.fabioformosa.quartzmanager.api.converters;
import it.fabioformosa.quartzmanager.api.dto.SchedulerDTO;
import it.fabioformosa.quartzmanager.api.enums.SchedulerStatus;
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.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.convert.ConversionService;
import org.springframework.test.annotation.DirtiesContext;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest
class SchedulerToSchedulerDTOTest {
@Autowired
private Scheduler scheduler;
@Autowired
private ConversionService conversionService;
@Order(1)
@Test
void givenAnActiveScheduler_whenItIsConverted_thenADtoIsReturned () throws SchedulerException {
Assertions.assertThat(scheduler.isShutdown()).isFalse();
SchedulerDTO schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
Assertions.assertThat(schedulerDTO).isNotNull();
Assertions.assertThat(schedulerDTO.getName()).isEqualTo(scheduler.getSchedulerName());
Assertions.assertThat(schedulerDTO.getInstanceId()).isEqualTo(scheduler.getSchedulerInstanceId());
}
@Order(2)
@Test
void givenAnActiveScheduler_whenItIsConverted_thenADtoHasAStatus () throws SchedulerException {
scheduler.start();
Assertions.assertThat(scheduler.isStarted()).isTrue();
SchedulerDTO schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
Assertions.assertThat(schedulerDTO.getStatus()).isEqualTo(SchedulerStatus.RUNNING);
scheduler.standby();
Assertions.assertThat(scheduler.isInStandbyMode()).isTrue();
schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
Assertions.assertThat(schedulerDTO.getStatus()).isEqualTo(SchedulerStatus.PAUSED);
}
@DirtiesContext
@Order(3)
@Test
void givenASchedulerInShutdown_whenItIsConverted_thenADtoIsReturnedWithNoTriggers () throws SchedulerException {
Assertions.assertThat(scheduler.isShutdown()).isFalse();
scheduler.shutdown(false);
Assertions.assertThat(scheduler.isShutdown()).isTrue();
SchedulerDTO schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
Assertions.assertThat(schedulerDTO).isNotNull();
Assertions.assertThat(schedulerDTO.getName()).isEqualTo(scheduler.getSchedulerName());
Assertions.assertThat(schedulerDTO.getInstanceId()).isEqualTo(scheduler.getSchedulerInstanceId());
Assertions.assertThat(schedulerDTO.getTriggerKeys()).isNull();
Assertions.assertThat(schedulerDTO.getStatus()).isEqualTo(SchedulerStatus.STOPPED);
}
}

Some files were not shown because too many files have changed in this diff Show More