mirror of
https://github.com/fabioformosa/quartz-manager.git
synced 2026-05-15 22:30:29 +09:00
Compare commits
439 Commits
quartz-man
...
v5.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71bc29df2a | ||
|
|
59a8b39305 | ||
|
|
ab1c3b5606 | ||
|
|
1ae381e842 | ||
|
|
7677947447 | ||
|
|
0a62366fa4 | ||
|
|
cc433bb531 | ||
|
|
501ef6c062 | ||
|
|
b6529b453a | ||
|
|
40d8c952a0 | ||
|
|
c1511b54f8 | ||
|
|
9a50949fcc | ||
|
|
699e661d81 | ||
|
|
93990a5994 | ||
|
|
82e684f0a7 | ||
|
|
7d481247bc | ||
|
|
e24c5bc62a | ||
|
|
29fff2a6cd | ||
|
|
2a20b930f0 | ||
|
|
b0ba230abe | ||
|
|
1e48e1803f | ||
|
|
e90648c027 | ||
|
|
23417fa6a2 | ||
|
|
57d5ebd641 | ||
|
|
da8c9d5707 | ||
|
|
6b245c7eec | ||
|
|
d7653dc73e | ||
|
|
e6a7b35f6a | ||
|
|
f6e02ae181 | ||
|
|
f6d6cd16e7 | ||
|
|
87901fe6a7 | ||
|
|
3088a2fec1 | ||
|
|
bdd0caa026 | ||
|
|
068b0eed34 | ||
|
|
b2f9692815 | ||
|
|
5fc6c56409 | ||
|
|
d7a78c57ae | ||
|
|
31658416f5 | ||
|
|
1d81e88684 | ||
|
|
f55a58b100 | ||
|
|
902542e480 | ||
|
|
c198d32bd5 | ||
|
|
56d9f5d94f | ||
|
|
45f66d51fe | ||
|
|
95769248a3 | ||
|
|
dcbf3eb277 | ||
|
|
fab977fd7d | ||
|
|
9d2a01ebbe | ||
|
|
9a0789cab0 | ||
|
|
e5a6b8b32b | ||
|
|
4537c8e304 | ||
|
|
82ac821b34 | ||
|
|
0a4a31ae65 | ||
|
|
3b325536e8 | ||
|
|
307c6eab98 | ||
|
|
b1ff70265f | ||
|
|
226296737d | ||
|
|
a5750d1d0d | ||
|
|
f71c9b20ab | ||
|
|
9cc55492dc | ||
|
|
6d36e4620c | ||
|
|
4d5e8f62c3 | ||
|
|
8ba33f25b4 | ||
|
|
7a742d5aea | ||
|
|
abd25d6158 | ||
|
|
9f46e52564 | ||
|
|
e6927209e5 | ||
|
|
63fbedbdc8 | ||
|
|
6ec886686f | ||
|
|
f96e356c8a | ||
|
|
c646624e45 | ||
|
|
0148056b1f | ||
|
|
e9542352b5 | ||
|
|
7cef35517b | ||
|
|
bc0619a92a | ||
|
|
2debf6c63f | ||
|
|
baa01d10cb | ||
|
|
adb4864c85 | ||
|
|
c6f10e04eb | ||
|
|
ad6a61f3df | ||
|
|
5cb73019de | ||
|
|
8a8e878e47 | ||
|
|
fae82e1d4e | ||
|
|
e0011913c2 | ||
|
|
a59b6a6c96 | ||
|
|
68aaab6ac4 | ||
|
|
c75190513a | ||
|
|
1421c52c34 | ||
|
|
b2942737af | ||
|
|
a1d8b12e98 | ||
|
|
45d6a4c59a | ||
|
|
e6f6fb5f06 | ||
|
|
13c438d097 | ||
|
|
412e455907 | ||
|
|
75d630aad0 | ||
|
|
2105e289ac | ||
|
|
9eddc0b1fd | ||
|
|
fa4ede5179 | ||
|
|
3aa672031a | ||
|
|
82ca186bff | ||
|
|
727a11fcea | ||
|
|
7106dc0fbb | ||
|
|
a2c8ecb227 | ||
|
|
e4c771e364 | ||
|
|
261dd8b624 | ||
|
|
ac63576704 | ||
|
|
a3b92443c4 | ||
|
|
527ee1200a | ||
|
|
82a60eb651 | ||
|
|
7fd174b313 | ||
|
|
7c910196e1 | ||
|
|
554e7e5e25 | ||
|
|
8b70319436 | ||
|
|
053f196b6b | ||
|
|
86f8cc8347 | ||
|
|
e91d02ba9f | ||
|
|
109d2868d9 | ||
|
|
4673e41fc5 | ||
|
|
a2122351d6 | ||
|
|
375aaf71d3 | ||
|
|
b752af948d | ||
|
|
52ed016073 | ||
|
|
632288726b | ||
|
|
1dc9bee987 | ||
|
|
387c440e06 | ||
|
|
136afa0fca | ||
|
|
8ba3daa2f0 | ||
|
|
b3fe337203 | ||
|
|
da7375ea0d | ||
|
|
d9487d1106 | ||
|
|
3110496630 | ||
|
|
22e90dc7d1 | ||
|
|
5943243e62 | ||
|
|
1244a64d34 | ||
|
|
40a6ecd159 | ||
|
|
9df68dccac | ||
|
|
72bba6f80c | ||
|
|
cf803bb1dd | ||
|
|
13c78ed5d3 | ||
|
|
3bd878a978 | ||
|
|
c9d3528f4b | ||
|
|
3b2f8fc780 | ||
|
|
bc3da09d60 | ||
|
|
284f56302c | ||
|
|
cdd5047bbc | ||
|
|
5d3ba95bd9 | ||
|
|
ec384113eb | ||
|
|
8b2651876c | ||
|
|
a3852c421e | ||
|
|
e1e1bdbd01 | ||
|
|
6757511de3 | ||
|
|
f5b717ec36 | ||
|
|
6b491d9949 | ||
|
|
cf382db49f | ||
|
|
ee2f80e582 | ||
|
|
4453d62515 | ||
|
|
72068818d7 | ||
|
|
92bb94b9fa | ||
|
|
7a2098e9ce | ||
|
|
56b81f4f48 | ||
|
|
82c060c2a7 | ||
|
|
fba35b796d | ||
|
|
5f6fc1fa6f | ||
|
|
a9c227cd05 | ||
|
|
a8330e062f | ||
|
|
30d0cbf6de | ||
|
|
3063e08eb3 | ||
|
|
f7054b160f | ||
|
|
98b5d0e37a | ||
|
|
6348bac11a | ||
|
|
c2a26c97a8 | ||
|
|
cd6e01109b | ||
|
|
7553efdc3b | ||
|
|
6578dc312a | ||
|
|
c490a7ab28 | ||
|
|
0969a406c6 | ||
|
|
eed3021373 | ||
|
|
5b33bd4dca | ||
|
|
6d22207e27 | ||
|
|
249cf49873 | ||
|
|
b17d487c8b | ||
|
|
fedb2b50b6 | ||
|
|
4b18313e2d | ||
|
|
8387f587b3 | ||
|
|
c7b64dbdf3 | ||
|
|
085d61cf29 | ||
|
|
e6cf2e9390 | ||
|
|
4bcea96789 | ||
|
|
866062bdcb | ||
|
|
53a54ddbda | ||
|
|
8bdb85b878 | ||
|
|
9a26be41a8 | ||
|
|
5cf39b3861 | ||
|
|
99d87636d2 | ||
|
|
bccd50ac4a | ||
|
|
3bb30accfd | ||
|
|
5d2b71652c | ||
|
|
b70af4dafe | ||
|
|
a95cf20c6b | ||
|
|
b58a5dbe9f | ||
|
|
f7222d65ae | ||
|
|
ff43103f37 | ||
|
|
93ab9c55bc | ||
|
|
28715cdf62 | ||
|
|
bd5276116b | ||
|
|
b20fb9e9c3 | ||
|
|
d3a406a382 | ||
|
|
c70fe687e1 | ||
|
|
fc81685044 | ||
|
|
6e24d7caf5 | ||
|
|
6786cffb4d | ||
|
|
1805705ff2 | ||
|
|
64fbabba4d | ||
|
|
0db13872a1 | ||
|
|
694e199709 | ||
|
|
77cf0a06d6 | ||
|
|
d9c7bcaad9 | ||
|
|
d9469b7dcc | ||
|
|
1a57a4b04e | ||
|
|
10d871be2e | ||
|
|
8b75cc1891 | ||
|
|
f6522a9a79 | ||
|
|
96449c8aeb | ||
|
|
1d03027efe | ||
|
|
69750267f6 | ||
|
|
2640e91055 | ||
|
|
9ccefe90ee | ||
|
|
c538ea95ee | ||
|
|
933975ce70 | ||
|
|
d189feea87 | ||
|
|
9637658b89 | ||
|
|
c3c6265dc6 | ||
|
|
63f871f649 | ||
|
|
a9259fd30d | ||
|
|
43e1fd3f04 | ||
|
|
8e1e4344e4 | ||
|
|
2186b0b007 | ||
|
|
9f7238021b | ||
|
|
db2a5949dc | ||
|
|
73293095f6 | ||
|
|
879fae55ce | ||
|
|
b2da564469 | ||
|
|
b52834a2d8 | ||
|
|
c8c4ad37c8 | ||
|
|
6fa7375f13 | ||
|
|
9127a50433 | ||
|
|
cabbec3d3b | ||
|
|
e42b26fa73 | ||
|
|
018c0f18dc | ||
|
|
a4b0a1bafb | ||
|
|
6eededed2c | ||
|
|
b62455836a | ||
|
|
9dfe06e346 | ||
|
|
461c31e7ea | ||
|
|
29a1903b21 | ||
|
|
83401a2ecb | ||
|
|
3242457cce | ||
|
|
12f91fa85c | ||
|
|
85ba371b72 | ||
|
|
014c348a89 | ||
|
|
a44d041e93 | ||
|
|
9ea4afcaef | ||
|
|
9b32f5e598 | ||
|
|
f4dd8519a9 | ||
|
|
1b1be180c6 | ||
|
|
026bdc6f18 | ||
|
|
1207a646c7 | ||
|
|
3e90a9b22c | ||
|
|
459aa136c1 | ||
|
|
d164cb9363 | ||
|
|
a8fb990966 | ||
|
|
121f2e364f | ||
|
|
b968329fb3 | ||
|
|
d9ff379d59 | ||
|
|
5c9a8d21a3 | ||
|
|
9d24c1587f | ||
|
|
436821a831 | ||
|
|
ecb07dc682 | ||
|
|
8f08927aad | ||
|
|
e29fd8dc92 | ||
|
|
3af5eb076e | ||
|
|
c190c15889 | ||
|
|
540bdf35a5 | ||
|
|
33b4d88d52 | ||
|
|
ad3eec4abe | ||
|
|
7b87a9485e | ||
|
|
a8a027ed88 | ||
|
|
69b62032b2 | ||
|
|
f411659ad6 | ||
|
|
0c33eda68c | ||
|
|
4013c4c08f | ||
|
|
ae3c2d72a1 | ||
|
|
5a96f81338 | ||
|
|
31208f9883 | ||
|
|
a64a06d663 | ||
|
|
02a73762b0 | ||
|
|
2ca24a9aae | ||
|
|
fc1ea9166c | ||
|
|
71ee075cfa | ||
|
|
33f4a7544c | ||
|
|
d16b681362 | ||
|
|
5547b7e868 | ||
|
|
3722d0ca24 | ||
|
|
41d11372ce | ||
|
|
3582ef4125 | ||
|
|
8e32fe531a | ||
|
|
17201aad08 | ||
|
|
4fb70ac8e8 | ||
|
|
425de89469 | ||
|
|
5746cb43c3 | ||
|
|
704f31581f | ||
|
|
7cb7dde65d | ||
|
|
bdc86b5510 | ||
|
|
59816c9693 | ||
|
|
4f8b75a8f9 | ||
|
|
5ea8f2adb1 | ||
|
|
80cd607d17 | ||
|
|
488916bcd7 | ||
|
|
b5abeac093 | ||
|
|
ed266dea15 | ||
|
|
65558c7ee8 | ||
|
|
e5d2c33d9a | ||
|
|
7f00f5de99 | ||
|
|
b2906d09f4 | ||
|
|
3f0d036dad | ||
|
|
9d66cd85f0 | ||
|
|
0a21920ad9 | ||
|
|
bda37213f8 | ||
|
|
c725871a4e | ||
|
|
39a10681bf | ||
|
|
381cfa1486 | ||
|
|
4d4385b7ba | ||
|
|
ec7debe8c5 | ||
|
|
05591546b3 | ||
|
|
2b0644b495 | ||
|
|
a30bd9e2c7 | ||
|
|
6eed819364 | ||
|
|
a1780b1087 | ||
|
|
c70dc3181e | ||
|
|
8ee0435738 | ||
|
|
72e1415038 | ||
|
|
65c3653494 | ||
|
|
304a1e7f71 | ||
|
|
6972915a5c | ||
|
|
77ea248457 | ||
|
|
14c1f7ea85 | ||
|
|
0652a6ec5e | ||
|
|
cecd30309f | ||
|
|
22762d7d84 | ||
|
|
6715665072 | ||
|
|
b92d8275db | ||
|
|
b4acc378e3 | ||
|
|
a313d8b19d | ||
|
|
93152f8157 | ||
|
|
1e99602c68 | ||
|
|
1571ab6d12 | ||
|
|
44d6854bc5 | ||
|
|
727403d420 | ||
|
|
b06b130d20 | ||
|
|
94107f2210 | ||
|
|
adb8e06f0a | ||
|
|
8cb0ac09e8 | ||
|
|
8935d77d0f | ||
|
|
0adb8bf94b | ||
|
|
7e21437dfc | ||
|
|
bfba79448b | ||
|
|
5be49a4090 | ||
|
|
e0b0378329 | ||
|
|
5f3ae667b0 | ||
|
|
b14cf64124 | ||
|
|
d9f9ee96af | ||
|
|
a644dd6052 | ||
|
|
3e5b25b37a | ||
|
|
a693e2aa0c | ||
|
|
599b6fb0b4 | ||
|
|
9638667368 | ||
|
|
21f3f7dca2 | ||
|
|
b4bb16130c | ||
|
|
f1c9fba68e | ||
|
|
09df7795a9 | ||
|
|
6bb768de59 | ||
|
|
86badb8f41 | ||
|
|
54999ce735 | ||
|
|
851100b774 | ||
|
|
3ba2bafc55 | ||
|
|
cbd3066f57 | ||
|
|
f3506304d9 | ||
|
|
c9b90478dd | ||
|
|
2ca2ba7ffc | ||
|
|
3df1abd46e | ||
|
|
34f21a58c9 | ||
|
|
233b56f282 | ||
|
|
1cd7f605e3 | ||
|
|
efee7b575f | ||
|
|
870a813c61 | ||
|
|
24285c7885 | ||
|
|
69e2ab3977 | ||
|
|
8218c63bba | ||
|
|
d89a2af1aa | ||
|
|
09e9b18f96 | ||
|
|
e70bbcff62 | ||
|
|
085d2f1706 | ||
|
|
4746ba9489 | ||
|
|
27fab8acca | ||
|
|
734bb1b087 | ||
|
|
81d9e92450 | ||
|
|
5d794536e3 | ||
|
|
4f3efc50fc | ||
|
|
37ad22090c | ||
|
|
95fa102720 | ||
|
|
f37ad1ae58 | ||
|
|
fb2d8da53d | ||
|
|
87ee4bebb3 | ||
|
|
9bf0871ff6 | ||
|
|
70827393b7 | ||
|
|
c4e8eb94d6 | ||
|
|
e50a48bd4c | ||
|
|
10df1116bd | ||
|
|
03f45346a7 | ||
|
|
6170e8f1ae | ||
|
|
0a718e897b | ||
|
|
d243c3a8e8 | ||
|
|
49020d9bd0 | ||
|
|
1457f3fdea | ||
|
|
42c63b963f | ||
|
|
aecc1cab68 | ||
|
|
4e63e0a833 | ||
|
|
4540165157 | ||
|
|
a56f5284d1 | ||
|
|
eb675f59a3 | ||
|
|
35f8b6b52a | ||
|
|
6c76f0a067 | ||
|
|
48b626c08e | ||
|
|
b2f796cf73 | ||
|
|
8ecdb4f5a6 | ||
|
|
2b6dadd5f1 | ||
|
|
bce1be698c | ||
|
|
b7d152a42a | ||
|
|
25a5e808f2 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
# .dockerignore
|
||||
quartz-manager-frontend/node_modules
|
||||
46
.github/workflows/maven-release.yml
vendored
Normal file
46
.github/workflows/maven-release.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Publish package to the Maven Central Repository
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java 21 for publishing to Maven Central Repository
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
server-id: maven-central-release
|
||||
server-username: MAVEN_USERNAME
|
||||
server-password: MAVEN_PASSWORD
|
||||
gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }}
|
||||
gpg-passphrase: MAVEN_GPG_PASSPHRASE
|
||||
|
||||
- name: Build with Maven
|
||||
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"
|
||||
env:
|
||||
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 21 for publishing to GitHub Packages
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: Publish to GitHub Packages Apache Maven
|
||||
run: mvn deploy --file quartz-manager-parent/pom.xml -P "deploy-github,build-webjar"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
36
.github/workflows/maven.yml
vendored
Normal file
36
.github/workflows/maven.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Java CI with Maven
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
paths: [ 'quartz-manager-parent/**' ]
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
cache: maven
|
||||
- name: Build and test with Maven
|
||||
run: mvn -B package --file quartz-manager-parent/pom.xml
|
||||
44
.github/workflows/npm.yml
vendored
Normal file
44
.github/workflows/npm.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||
|
||||
name: npm CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
paths: [ 'quartz-manager-frontend/**' ]
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./quartz-manager-frontend
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: ./quartz-manager-frontend/package-lock.json
|
||||
- name: 'install'
|
||||
run: npm ci
|
||||
- name: 'test'
|
||||
run: npm test
|
||||
- name: 'build'
|
||||
run: npm run build --if-present
|
||||
40
.github/workflows/sonar-java.yml
vendored
Normal file
40
.github/workflows/sonar-java.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: SonarCloud Analysis for Java
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
# 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@v4
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 21
|
||||
distribution: 'zulu' # Alternative distribution options are available.
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.sonar/cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
- name: Cache Maven packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.m2
|
||||
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: ${{ runner.os }}-m2
|
||||
- name: Build and analyze
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=fabioformosa_quartz-manager --file quartz-manager-parent/pom.xml
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
/.project
|
||||
.idea
|
||||
*.iml
|
||||
.DS_Store
|
||||
|
||||
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
language: java
|
||||
jdk: openjdk8
|
||||
before_script: cd quartz-manager-parent
|
||||
58
CHANGELOG.md
Normal file
58
CHANGELOG.md
Normal file
@@ -0,0 +1,58 @@
|
||||
## **v5.0.1**
|
||||
|
||||
### New Features
|
||||
* Added full job management: list eligible job classes, create stored jobs, update jobs, delete jobs, and trigger jobs on demand.
|
||||
* Added trigger management APIs and UI flows to inspect, create, reschedule, pause, resume, and unschedule triggers.
|
||||
* Added support for Quartz trigger types beyond simple triggers: cron, daily time interval, and calendar interval triggers.
|
||||
* Added Quartz calendar management for annual, cron, daily, holiday, monthly, and weekly calendars.
|
||||
* Added calendar-aware scheduling support, including calendar assignment to triggers and included-time checks.
|
||||
* Redesigned the Quartz Manager dashboard with a broader operations view for scheduler, jobs, triggers, calendars, progress, and logs.
|
||||
* Updated the embedded UI to Angular 21.
|
||||
* Added support for Spring Boot 4 applications.
|
||||
|
||||
### Breaking Changes
|
||||
* Quartz Manager now requires Java 21+ and Spring Boot 4.x.
|
||||
* Applications using Quartz Manager APIs must migrate from `javax.*` validation/annotation dependencies to `jakarta.*` equivalents through the Spring Boot 4 stack.
|
||||
* Scheduler command endpoints now use `POST` operations and clearer action names: `/scheduler/start`, `/scheduler/standby`, `/scheduler/resume`, and `/scheduler/shutdown` replace the previous `GET` command endpoints.
|
||||
* Simple trigger endpoints now include the trigger group in the path: `/simple-triggers/{group}/{name}`.
|
||||
* New trigger creation should use the generalized `/triggers/{group}/{name}` API when working with cron, daily time interval, or calendar interval triggers.
|
||||
|
||||
### Fixes
|
||||
* Fixed WebSocket log retrieval for job execution logs.
|
||||
* Fixed UI style regressions and improved readability in the dashboard, login page, job class display, and misfire instruction display.
|
||||
* Improved API error handling for missing jobs, missing triggers, missing calendars, unsupported trigger types, and scheduling conflicts.
|
||||
|
||||
## **v4.1.1**
|
||||
**NEW FEATURE** support for multiple triggers
|
||||
|
||||
## **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.
|
||||
You can import:
|
||||
|
||||
* `quartz-manager-starter-api` to have a REST API layer to control your scheduler
|
||||
* `quartz-manager-starter-ui` to import the UI also, in your spring webapp.
|
||||
* `quartz-manager-starter-security` if you want to give access to the quartz-manager UI and API only to authenticated users
|
||||
34
Dockerfile
Normal file
34
Dockerfile
Normal 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"]
|
||||
380
README.md
380
README.md
@@ -1,110 +1,340 @@
|
||||
[](https://gitter.im/quartz-manager/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
<div align="center">
|
||||
|
||||
# QUARTZ MANAGER
|
||||
UI Manager for Quartz Scheduler.
|
||||
# Quartz Manager
|
||||
|
||||
Through this webapp you can launch and control your scheduled job. The UI Console is composed by a management panel to set trigger, start/stop scheduler and a log panel with a progress bar to display the job output.
|
||||
**A Spring Boot library and standalone web app that adds REST API and dashboard management to Quartz Scheduler.**
|
||||
|
||||

|
||||
[](https://github.com/fabioformosa/quartz-manager/actions/workflows/maven.yml)
|
||||
[](https://github.com/fabioformosa/quartz-manager/actions/workflows/npm.yml)
|
||||
[](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager)
|
||||
[](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager)
|
||||
[](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager)
|
||||
[](https://sonarcloud.io/summary/new_code?id=fabioformosa_quartz-manager)
|
||||
|
||||
## HOW IT WORKS
|
||||
* Set up the trigger into the left sidebar in terms of: daily frequency and and max occurrences.
|
||||
* Press the start button
|
||||
* The GUI manager updates the progress bar and reports all logs of your quartz job.
|
||||
[Choose Your Path](#choose-your-path) • [Features](#features) • [Quick Start](#quick-start) • [REST API](#rest-api) • [Security](#security) • [Persistence](#persistence) • [Roadmap](#roadmap)
|
||||
|
||||
## ROADMAP
|
||||
Open the [Project Roadmap](https://github.com/fabioformosa/quartz-manager/projects) to take a look at the plan of Quartz Manager.
|
||||
Currently this project might be useful to look how to import Quartz Library in a spring boot application. For this purpose, browse the folder `quartz-manager-parent/quartz-manager-api`.
|
||||
We're just working to create a library, from project `quartz-manager-parent/quartz-manager-api`, to be imported in your spring boot where you have your job to be scheduled.
|
||||
</div>
|
||||
|
||||
Take a loot to the project [Quartz-Manager Demo](https://github.com/fabioformosa/quartz-manager-demo), it is an example of how-to:
|
||||
* import the quartz-manager-api library in your webapp
|
||||
* include the quartz-manager frontend (angular based) through a webjar
|
||||
* set properties into the application.yml
|
||||
* add a secure layer to allow the API only to logged users
|
||||
* schedule a custom job (a dummy `hello world`)
|
||||
|
||||
**NB: In few days, we'll release the library jar of quartz-manager into the maven central repo.**
|
||||
[Quartz Scheduler](https://www.quartz-scheduler.org/) is powerful, but it does not ship with a REST API or an operations dashboard. Quartz Manager fills that gap for Spring Boot applications and can also run as a standalone scheduler web app.
|
||||
|
||||
Next steps in the roadmap are:
|
||||
* to add a persistent layer to save all job setup.
|
||||
* to add a complete setup UI panel for quartz, in term of cronjobs and multiple jobs.
|
||||
* to add CI/CD pipeline to ease the deploy pulling a docker container.
|
||||
* Enabling adapters for integrations: kafka, etc.
|
||||
Use it to start and stop a scheduler, create jobs, schedule triggers, manage calendars, inspect executions, and monitor job progress from HTTP endpoints or from a browser UI.
|
||||
|
||||
## PROJECT STRUCTURE
|
||||
* **quartz-parent/quartz-manager-api** is the library that can be imported in webapp to have the quartz-manager API.
|
||||
* **quartz-parent/quartz-manager-webjar** is a maven module to build and package the angular frontend in a webjar.
|
||||
* **quartz-parent/quartz-manager-security** is ther library that can be imported in a webapp to have a security layer (login) over the quartz-manager API.
|
||||
* **quartz-parent/quartz-manager-web-showcase** is an example of webapp that imports quartz-manager-api. Useful to develop the frontend started locally with the webpack dev server.
|
||||
* **quartz-frontend** is the angular app that interacts with the Quartz Manager API.
|
||||

|
||||
|
||||
## HOW-TO CONTRIBUTE
|
||||
**[requirements]** Make sure you have installed
|
||||
* [Java 8](https://java.com/download/) or greater
|
||||
* [Maven](https://maven.apache.org/)
|
||||
* [npm](https://www.npmjs.com/get-npm), [node](https://nodejs.org) and [angular-cli](https://cli.angular.io/)
|
||||
## Choose Your Path
|
||||
|
||||
To build&run quartz-manager in your machine:
|
||||
### 1. Add API And UI To Your Existing App
|
||||
|
||||
Use this path when you already have a Spring Boot application and want to add a Quartz management API, an embedded management panel, or both.
|
||||
|
||||
Current behavior: Quartz Manager creates and manages its own scheduler bean named `quartzManagerScheduler` by default. It can coexist with other Quartz schedulers in the same application, but it does not automatically take control of an arbitrary existing scheduler instance.
|
||||
|
||||
If you want Quartz Manager to manage an existing scheduler today, disable the default scheduler configuration with `quartz-manager.quartz.enabled=false` and provide a compatible bean named `quartzManagerScheduler`. First-class existing-scheduler integration is planned on the roadmap.
|
||||
|
||||
Your managed jobs must extend `AbstractQuartzManagerJob` so Quartz Manager can expose them as eligible jobs and stream their execution logs/progress to the UI.
|
||||
|
||||
If you also want the browser dashboard, see [Add The UI](#add-the-ui).
|
||||
|
||||
### 2. Add A New Scheduler To Your App
|
||||
|
||||
Use this path when your Spring Boot application does not have Quartz yet and you want to add a scheduler managed by Quartz Manager.
|
||||
|
||||
The easiest setup is to let Quartz Manager import, initialize, and manage a Quartz Scheduler for you. Import the API starter, create jobs extending `AbstractQuartzManagerJob`, configure the package that contains your jobs, and use the REST API or UI to create jobs and triggers.
|
||||
|
||||
You can later add optional modules for the embedded UI, JWT security, and PostgreSQL persistence.
|
||||
|
||||
If you also want the browser dashboard, see [Add The UI](#add-the-ui).
|
||||
|
||||
### 3. Run Quartz Manager As A Standalone App
|
||||
|
||||
Use this path when you want a standalone scheduler web application instead of embedding Quartz Manager into an existing product.
|
||||
|
||||
The `quartz-manager-web-showcase` application imports Quartz Manager API, UI, and security modules and runs with an embedded Quartz scheduler. It is useful as a ready-to-run management console, as a demo, and as a reference application.
|
||||
|
||||
Even in standalone mode, the jobs managed by Quartz Manager must extend `AbstractQuartzManagerJob`.
|
||||
|
||||
## Features
|
||||
|
||||
- REST API for scheduler, job, trigger, and calendar management.
|
||||
- Embeddable management UI provided as a webjar: import it as a Maven dependency and open `/quartz-manager-ui` in the browser.
|
||||
- Scheduler commands: start, standby, resume, and shutdown.
|
||||
- Job management: list eligible job classes, create stored jobs, update jobs, delete jobs, and trigger jobs manually.
|
||||
- Trigger management: create, inspect, reschedule, pause, resume, and unschedule triggers.
|
||||
- Trigger types: simple, cron, daily time interval, and calendar interval.
|
||||
- Calendar management: annual, cron, daily, holiday, monthly, and weekly calendars.
|
||||
- Misfire handling for supported trigger types.
|
||||
- WebSocket updates for job execution progress and logs.
|
||||
- Optional OpenAPI/Swagger UI documentation.
|
||||
- Optional JWT-based security with in-memory users.
|
||||
- Optional PostgreSQL persistence using Quartz JDBC job store and Liquibase-managed schema creation.
|
||||
|
||||
In dependency snippets, replace `VERSION` with the version you want to use.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Java 21+
|
||||
- Spring Boot 4.x
|
||||
- Maven 3.9+
|
||||
- Node.js and npm only if you build the frontend locally
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Required | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `quartz-manager-starter-api` | Required | REST API, managed scheduler integration, jobs, triggers, calendars, OpenAPI support, and WebSocket updates |
|
||||
| `quartz-manager-starter-ui` | Optional | Embeddable management UI provided as a webjar |
|
||||
| `quartz-manager-starter-security` | Optional | JWT authentication for Quartz Manager API and UI |
|
||||
| `quartz-manager-starter-persistence` | Optional | PostgreSQL-backed Quartz persistence and Liquibase schema setup |
|
||||
| `quartz-manager-web-showcase` | Optional | Standalone/demo Spring Boot application using the Quartz Manager modules |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Path 1: Existing Spring Boot App
|
||||
|
||||
Add the API starter:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-api</artifactId>
|
||||
<version>VERSION</version>
|
||||
</dependency>
|
||||
```
|
||||
#CLONE REPOSITORY
|
||||
|
||||
Create jobs by extending `AbstractQuartzManagerJob`:
|
||||
|
||||
```java
|
||||
import it.fabioformosa.quartzmanager.api.jobs.AbstractQuartzManagerJob;
|
||||
import it.fabioformosa.quartzmanager.api.jobs.entities.LogRecord;
|
||||
import org.quartz.JobExecutionContext;
|
||||
|
||||
public class SampleJob extends AbstractQuartzManagerJob {
|
||||
|
||||
@Override
|
||||
public LogRecord doIt(JobExecutionContext context) {
|
||||
return new LogRecord(LogRecord.LogType.INFO, "Hello from Quartz Manager");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Configure job discovery:
|
||||
|
||||
```properties
|
||||
quartz-manager.jobClassPackages=com.example.jobs
|
||||
quartz-manager.oas.enabled=true
|
||||
```
|
||||
|
||||
By default, Quartz Manager creates a dedicated scheduler named `quartz-manager-scheduler`. If your app already has another Quartz scheduler, both can coexist.
|
||||
|
||||
Advanced existing-scheduler setup:
|
||||
|
||||
```properties
|
||||
quartz-manager.quartz.enabled=false
|
||||
```
|
||||
|
||||
Then provide a `Scheduler` bean named `quartzManagerScheduler`. This is the current integration point; a more explicit existing-scheduler mode is planned.
|
||||
|
||||
To add the browser dashboard to your application, see [Add The UI](#add-the-ui).
|
||||
|
||||
### Path 2: New Scheduler In Your App
|
||||
|
||||
Use the same API starter setup as Path 1, then let Quartz Manager create its managed scheduler.
|
||||
|
||||
Create one or more jobs extending `AbstractQuartzManagerJob`, configure `quartz-manager.jobClassPackages`, then create stored jobs and triggers through the REST API, Swagger UI, or the dashboard.
|
||||
|
||||
Default managed Quartz properties:
|
||||
|
||||
```properties
|
||||
org.quartz.scheduler.instanceName=quartz-manager-scheduler
|
||||
org.quartz.threadPool.threadCount=1
|
||||
```
|
||||
|
||||
To customize the managed scheduler, add `managed-quartz.properties` to your classpath.
|
||||
|
||||
To add the browser dashboard to your application, see [Add The UI](#add-the-ui).
|
||||
|
||||
### Path 3: Standalone Quartz Manager App
|
||||
|
||||
Run the standalone showcase application when you want Quartz Manager as a ready-to-use scheduler web app.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/fabioformosa/quartz-manager.git
|
||||
|
||||
# START QUARTZ-MANAGER-WEB
|
||||
cd quartz-manager/quartz-parent/quartz-manager-web
|
||||
cd quartz-manager/quartz-manager-parent
|
||||
mvn install -Pbuild-webjar
|
||||
cd quartz-manager-web-showcase
|
||||
mvn spring-boot:run
|
||||
|
||||
# START QUARTZ-MANAGER-FRONTEND
|
||||
cd quartz-manager/quartz-manager-frontend
|
||||
npm install
|
||||
npm start
|
||||
|
||||
```
|
||||
|
||||
1. Open browser at [http://localhost:4200](http://localhost:4200)
|
||||
1. Log in with **default credentials**: `admin/admin`
|
||||
Open the dashboard:
|
||||
|
||||
If you are not confident with maven CLI, you can start it by your IDE. For more details [spring boot ref.](http://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-running-your-application.html)
|
||||
```text
|
||||
http://localhost:8080/quartz-manager-ui/index.html
|
||||
```
|
||||
|
||||
Open Swagger UI when OpenAPI is enabled:
|
||||
|
||||
## HOW TO RUN YOUR SCHEDULED JOB
|
||||
By default, quartz-manager-web executes the dummy job that logs "hello world!".
|
||||
Replace the dummy job (class: `it.fabioformosa.quartzmanager.jobs.SampleJob`) with yours. Follow these steps:
|
||||
```text
|
||||
http://localhost:8080/swagger-ui.html
|
||||
```
|
||||
|
||||
1. Extend the super class `it.fabioformosa.quartzmanager.jobs.AbstractLoggingJob`
|
||||
1. set property `quartz-manager.jobClass` with qualified name of your custom Job Class (default job is SampleJob.class)
|
||||
Default showcase credentials:
|
||||
|
||||
## HOW TO CHANGE SETTINGS
|
||||
* Num of Threads: `/quartz-manager-parent/quartz-manager-web/src/main/resources/quartz.properties`
|
||||
* Credentials: To change admin's password, set ENV var `quartz-manager.account.pwd`
|
||||
* quartz-manager backend context path (default `/quartz-manager`) and port (default `8080`): `/quartz-manager/src/main/resources/application.properties`
|
||||
```text
|
||||
admin / admin
|
||||
```
|
||||
|
||||
## HOW TO BROWSE REST API DOC
|
||||
Swagger has been added as library. So, you can get REST API doc opening: [http://localhost:8080/quartz-manager/swagger-ui.html](http://localhost:8080/quartz-manager/swagger-ui.html)
|
||||
To plug in your own jobs today, add your job classes inside the cloned repository, rebuild the standalone application, and configure `quartz-manager.jobClassPackages` to include their package.
|
||||
|
||||
## Tech Overview
|
||||
A Docker-based standalone distribution is planned. It will provide a supported mechanism to attach external job classes without modifying the cloned repository.
|
||||
|
||||
**Backend Stack** Java 8, Spring Boot 2.1.4 (Spring MVC 5.1.6, Spring Security 5.1.5, Spring AOP 5.1.6), Quartz Scheduler 2.3.1
|
||||
## Add The UI
|
||||
|
||||
**Application Server** Tomcat (embedded)
|
||||
Add the UI starter when you want the embedded management panel in your Spring Boot app:
|
||||
|
||||
**Frontend** Angular 9.1.4, Web-Socket (stompjs 2.3.3)
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-ui</artifactId>
|
||||
<version>VERSION</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**Style** angular material, FontAwesome 5
|
||||
The UI is served from:
|
||||
|
||||
From quartz manager ver 2.x.x, the new structure of project is:
|
||||
* REST API backend
|
||||
* Single Page Application frontend (angular 9)
|
||||
```text
|
||||
http://localhost:8080/quartz-manager-ui/index.html
|
||||
```
|
||||
|
||||
(The previous 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)
|
||||
## REST API
|
||||
|
||||
## Contributes
|
||||
Quartz Manager exposes its API under `/quartz-manager`.
|
||||
|
||||
Every contribution is welcome. Open a issue, so we can discuss about new features and implement them.
|
||||
| Area | Endpoints |
|
||||
| --- | --- |
|
||||
| Scheduler | `GET /quartz-manager/scheduler`, `POST /quartz-manager/scheduler/start`, `POST /quartz-manager/scheduler/standby`, `POST /quartz-manager/scheduler/resume`, `POST /quartz-manager/scheduler/shutdown` |
|
||||
| Job classes | `GET /quartz-manager/job-classes` |
|
||||
| Jobs | `GET /quartz-manager/jobs`, `POST /quartz-manager/jobs/{group}/{name}`, `PUT /quartz-manager/jobs/{group}/{name}`, `POST /quartz-manager/jobs/{group}/{name}/trigger`, `DELETE /quartz-manager/jobs/{group}/{name}` |
|
||||
| Triggers | `GET /quartz-manager/triggers`, `POST /quartz-manager/triggers/{group}/{name}`, `PUT /quartz-manager/triggers/{group}/{name}`, `POST /quartz-manager/triggers/{group}/{name}/pause`, `POST /quartz-manager/triggers/{group}/{name}/resume`, `DELETE /quartz-manager/triggers/{group}/{name}` |
|
||||
| Calendars | `GET /quartz-manager/calendars`, `POST /quartz-manager/calendars/{name}`, `PUT /quartz-manager/calendars/{name}`, `DELETE /quartz-manager/calendars/{name}`, `POST /quartz-manager/calendars/{name}/included-time-test` |
|
||||
|
||||
## Credits
|
||||
Enable OpenAPI and Swagger UI with:
|
||||
|
||||
* this project has been created from [angular-spring-starter](https://github.com/bfwg/angular-spring-starter)
|
||||
```properties
|
||||
quartz-manager.oas.enabled=true
|
||||
```
|
||||
|
||||
Then open:
|
||||
|
||||
```text
|
||||
http://localhost:8080/swagger-ui.html
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
Add the security starter when you want Quartz Manager API and UI protected by JWT authentication:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-security</artifactId>
|
||||
<version>VERSION</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Example configuration:
|
||||
|
||||
```yaml
|
||||
quartz-manager:
|
||||
security:
|
||||
jwt:
|
||||
secret: "change-me"
|
||||
expiration-in-sec: 28800
|
||||
header-strategy:
|
||||
enabled: true
|
||||
header: Authorization
|
||||
cookie-strategy:
|
||||
enabled: false
|
||||
cookie: AUTH-TOKEN
|
||||
accounts:
|
||||
in-memory:
|
||||
enabled: true
|
||||
users:
|
||||
- username: admin
|
||||
password: admin
|
||||
roles:
|
||||
- ADMIN
|
||||
```
|
||||
|
||||
Security is applied to `/quartz-manager/**`. The UI webjar path is ignored by the security filter chain, while API calls require authentication.
|
||||
|
||||
## Persistence
|
||||
|
||||
By default, Quartz Manager uses Quartz's in-memory job store. Scheduling data is lost when the application stops.
|
||||
|
||||
Add the persistence starter when you want Quartz Manager's managed scheduler to use PostgreSQL-backed Quartz persistence:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-persistence</artifactId>
|
||||
<version>VERSION</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Configure the Quartz Manager datasource:
|
||||
|
||||
```yaml
|
||||
quartz-manager:
|
||||
persistence:
|
||||
quartz:
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5432/quartzmanager
|
||||
user: quartzmanager
|
||||
password: quartzmanager
|
||||
```
|
||||
|
||||
The persistence module configures Quartz `JobStoreTX`, uses the PostgreSQL delegate, and creates the required Quartz tables through Liquibase.
|
||||
|
||||
## Examples
|
||||
|
||||
Example integrations are available in [quartz-manager-use-cases](https://github.com/fabioformosa/quartz-manager-use-cases).
|
||||
|
||||
The use cases cover simple Spring applications, secured and unsecured setups, existing application security, existing Quartz scenarios, and persistence.
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- Quartz Manager creates and manages its own scheduler by default. Automatic discovery and first-class management of an arbitrary existing scheduler is not yet supported.
|
||||
- Existing applications that want Quartz Manager to manage a pre-existing scheduler must currently provide it as a bean named `quartzManagerScheduler` and disable Quartz Manager's default scheduler creation.
|
||||
- Persistence currently targets PostgreSQL.
|
||||
- Cluster mode is not currently documented as a supported production mode.
|
||||
- Managed jobs must extend `AbstractQuartzManagerJob` to be eligible for job discovery and UI log/progress streaming.
|
||||
|
||||
## Roadmap
|
||||
|
||||
The next priorities are tracked in the [project roadmap](https://github.com/users/fabioformosa/projects/1).
|
||||
|
||||
Planned improvements include:
|
||||
|
||||
- First-class support for managing an existing Quartz Scheduler instance.
|
||||
- Cluster mode support.
|
||||
- Additional persistence targets beyond PostgreSQL.
|
||||
- OAuth2 client support.
|
||||
- Continued UI improvements.
|
||||
|
||||
## Development
|
||||
|
||||
This repository contains the backend modules and the frontend application.
|
||||
|
||||
For local development, repository structure, build commands, and contribution details, see [quartz-manager-parent/README.md](https://github.com/fabioformosa/quartz-manager/blob/develop/quartz-manager-parent/README.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. Open an issue to discuss bugs, questions, or feature proposals before starting larger changes.
|
||||
|
||||
## License
|
||||
|
||||
Quartz Manager is released under the [Apache License 2.0](LICENSE).
|
||||
|
||||
## Support
|
||||
|
||||
If Quartz Manager is useful to you, consider starring the repository to support the project.
|
||||
|
||||
45
cloudbuild.yaml
Normal file
45
cloudbuild.yaml
Normal 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
|
||||
BIN
docs/assets/quartz-manager-dashboard.png
Normal file
BIN
docs/assets/quartz-manager-dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
BIN
docs/assets/quartz-manager-openapidoc.png
Normal file
BIN
docs/assets/quartz-manager-openapidoc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -5,8 +5,8 @@
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
last 2 Chrome versions
|
||||
last 2 Firefox versions
|
||||
last 2 Edge versions
|
||||
last 2 Safari versions
|
||||
last 2 iOS versions
|
||||
178
quartz-manager-frontend/.eslintrc.json
Normal file
178
quartz-manager-frontend/.eslintrc.json
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
👋 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"
|
||||
],
|
||||
"root": true,
|
||||
"rules": {
|
||||
"@angular-eslint/component-class-suffix": "off",
|
||||
"@angular-eslint/component-selector": "off",
|
||||
"@angular-eslint/directive-class-suffix": "error",
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@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": "off",
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
"@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/semi": [
|
||||
"off",
|
||||
null
|
||||
],
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
19
quartz-manager-frontend/.eslintrc.sonar.json
Normal file
19
quartz-manager-frontend/.eslintrc.sonar.json
Normal 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
|
||||
}
|
||||
4
quartz-manager-frontend/.gitignore
vendored
4
quartz-manager-frontend/.gitignore
vendored
@@ -40,6 +40,4 @@ testem.log
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
#package-lock.json
|
||||
package-lock.json
|
||||
/.angular/
|
||||
|
||||
47
quartz-manager-frontend/.prettierignore
Normal file
47
quartz-manager-frontend/.prettierignore
Normal 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/
|
||||
11
quartz-manager-frontend/.prettierrc.json
Normal file
11
quartz-manager-frontend/.prettierrc.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"trailingComma": "es5",
|
||||
"bracketSameLine": true,
|
||||
"printWidth": 80
|
||||
}
|
||||
@@ -3,8 +3,9 @@
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-spring-starter": {
|
||||
"quartz-manager-ui": {
|
||||
"root": "",
|
||||
"prefix": "qrzmng",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
@@ -17,16 +18,28 @@
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"allowedCommonJsDependencies": [
|
||||
"@stomp/stompjs", "stompjs", "sockjs-client", "angular2-uuid"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
"src/styles.css",
|
||||
"node_modules/roboto-fontface/css/roboto/roboto-fontface.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
@@ -37,7 +50,6 @@
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
@@ -55,64 +67,60 @@
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "angular-spring-starter:build"
|
||||
"buildTarget": "quartz-manager-ui:build:development"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "angular-spring-starter:build:production"
|
||||
"buildTarget": "quartz-manager-ui:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "angular-spring-starter:build"
|
||||
"buildTarget": "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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"angular-spring-starter-e2e": {
|
||||
"root": "e2e",
|
||||
"sourceRoot": "e2e",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "./protractor.conf.js",
|
||||
"devServerTarget": "angular-spring-starter:serve"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"e2e/tsconfig.e2e.json"
|
||||
],
|
||||
"exclude": []
|
||||
"eslintConfig": ".eslintrc.json",
|
||||
"lintFilePatterns": ["**/*.spec.ts", "**/*.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "angular-spring-starter",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"style": "css"
|
||||
"prefix": "qrzmng",
|
||||
"style": "css",
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
"prefix": "qrzmng",
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
quartz-manager-frontend/eslint.sonar.config.mjs
Normal file
30
quartz-manager-frontend/eslint.sonar.config.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import sonarjs from 'eslint-plugin-sonarjs';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ['src/**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
sonarjs
|
||||
},
|
||||
rules: {
|
||||
...sonarjs.configs.recommended.rules,
|
||||
'sonarjs/deprecation': 'off',
|
||||
'sonarjs/no-commented-code': 'off',
|
||||
'sonarjs/no-dead-store': 'off',
|
||||
'sonarjs/no-incomplete-assertions': 'off',
|
||||
'sonarjs/no-primitive-wrappers': 'off',
|
||||
'sonarjs/no-unused-vars': 'off',
|
||||
'sonarjs/prefer-promise-shorthand': 'off',
|
||||
'sonarjs/todo-tag': 'off',
|
||||
'sonarjs/unused-import': 'off'
|
||||
}
|
||||
}
|
||||
];
|
||||
20
quartz-manager-frontend/jest.config.js
Normal file
20
quartz-manager-frontend/jest.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const {createEsmPreset} = require('jest-preset-angular/presets');
|
||||
|
||||
module.exports = {
|
||||
...createEsmPreset({
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$'
|
||||
}),
|
||||
moduleNameMapper: {
|
||||
'^tslib$': '<rootDir>/node_modules/tslib/tslib.es6.mjs',
|
||||
'^rxjs$': '<rootDir>/node_modules/rxjs/dist/cjs/index.js',
|
||||
'^rxjs/operators$': '<rootDir>/node_modules/rxjs/dist/cjs/operators/index.js',
|
||||
'^rxjs/(.*)$': '<rootDir>/node_modules/rxjs/dist/cjs/$1',
|
||||
'^@fortawesome/fontawesome$': '<rootDir>/node_modules/@fortawesome/fontawesome/index.js',
|
||||
'^@fortawesome/fontawesome-free-solid$': '<rootDir>/node_modules/@fortawesome/fontawesome-free-solid/index.js'
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(@angular|@fortawesome|@stomp/rx-stomp|@stomp/stompjs|.*\\.mjs$)/)'
|
||||
]
|
||||
};
|
||||
@@ -1 +1,3 @@
|
||||
import 'jest-preset-angular';
|
||||
import {setupZoneTestEnv} from 'jest-preset-angular/setup-env/zone/index.mjs';
|
||||
|
||||
setupZoneTestEnv();
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/0.13/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client:{
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
files: [
|
||||
|
||||
],
|
||||
preprocessors: {
|
||||
|
||||
},
|
||||
mime: {
|
||||
'text/x-typescript': ['ts','tsx']
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
|
||||
reporters: config.angularCli && config.angularCli.codeCoverage
|
||||
? ['progress', 'coverage-istanbul']
|
||||
: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false
|
||||
});
|
||||
};
|
||||
23059
quartz-manager-frontend/package-lock.json
generated
Normal file
23059
quartz-manager-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,66 +5,69 @@
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
"build": "ng build --prod",
|
||||
"test": "jest",
|
||||
"build": "ng build --configuration production",
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
"lint:sonar": "eslint -c eslint.sonar.config.mjs \"src/**/*.ts\"",
|
||||
"lint:sonar:fix": "eslint -c eslint.sonar.config.mjs \"src/**/*.ts\" --fix"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@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/animations": "21.2.12",
|
||||
"@angular/cdk": "21.2.10",
|
||||
"@angular/common": "21.2.12",
|
||||
"@angular/compiler": "21.2.12",
|
||||
"@angular/core": "21.2.12",
|
||||
"@angular/forms": "21.2.12",
|
||||
"@angular/material": "21.2.10",
|
||||
"@angular/platform-browser": "21.2.12",
|
||||
"@angular/platform-browser-dynamic": "21.2.12",
|
||||
"@angular/platform-server": "21.2.12",
|
||||
"@angular/router": "21.2.12",
|
||||
"@auth0/angular-jwt": "5.2.0",
|
||||
"@danielmoncada/angular-datetime-picker": "21.0.0",
|
||||
"@fortawesome/fontawesome": "^1.1.4",
|
||||
"@fortawesome/fontawesome-free-regular": "^5.0.8",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.8",
|
||||
"@stomp/ng2-stompjs": "^0.6.3",
|
||||
"core-js": "2.5.1",
|
||||
"@stomp/rx-stomp": "2.4.0",
|
||||
"@stomp/stompjs": "^7.2.0",
|
||||
"hammerjs": "2.0.8",
|
||||
"net": "^1.0.2",
|
||||
"rxjs": "6.5.5",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"sockjs-client": "^1.1.1",
|
||||
"stompjs": "^2.3.3",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.10.2"
|
||||
"tslib": "^2.8.1",
|
||||
"uuid": "^13.0.0",
|
||||
"zone.js": "~0.16.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": "^21.2.10",
|
||||
"@angular-devkit/core": "^21.2.10",
|
||||
"@angular-eslint/builder": "21.3.1",
|
||||
"@angular-eslint/eslint-plugin": "21.3.1",
|
||||
"@angular-eslint/eslint-plugin-template": "21.3.1",
|
||||
"@angular-eslint/schematics": "21.3.1",
|
||||
"@angular-eslint/template-parser": "21.3.1",
|
||||
"@angular/cli": "^21.2.10",
|
||||
"@angular/compiler-cli": "21.2.12",
|
||||
"@angular/language-service": "21.2.12",
|
||||
"@types/hammerjs": "2.0.34",
|
||||
"@types/jasmine": "2.5.54",
|
||||
"@types/jasminewd2": "2.0.3",
|
||||
"@types/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"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-preset-angular",
|
||||
"setupFilesAfterEnv": ["<rootDir>/jest.setup.ts"]
|
||||
"@types/jasmine": "^5.1.13",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.13.14",
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-sonarjs": "^4.0.3",
|
||||
"jest": "30.4.1",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"jest-preset-angular": "^16.1.5",
|
||||
"jsdom": "^27.3.0",
|
||||
"prettier": "^2.8.1",
|
||||
"prettier-eslint": "^15.0.1",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/dist/long-stack-trace-zone';
|
||||
import 'zone.js/dist/proxy.js';
|
||||
import 'zone.js/dist/sync-test';
|
||||
import 'zone.js/dist/jasmine-patch';
|
||||
import 'zone.js/dist/async-test';
|
||||
import 'zone.js/dist/fake-async-test';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
|
||||
declare var __karma__: any;
|
||||
declare var require: any;
|
||||
|
||||
// Prevent Karma from running prematurely.
|
||||
__karma__.loaded = function () {};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
||||
// Finally, start Karma to run the tests.
|
||||
__karma__.start();
|
||||
7
quartz-manager-frontend/src/animate.css
vendored
Normal file
7
quartz-manager-frontend/src/animate.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,15 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AppComponent } from './app.component';
|
||||
import { LoginComponent } from './views/login';
|
||||
import { LoginGuard } from './guards';
|
||||
import { GuestGuard, AdminGuard } from './guards';
|
||||
import { NotFoundComponent } from './views/not-found';
|
||||
import { ChangePasswordComponent } from './views/change-password';
|
||||
import { ForbiddenComponent } from './views/forbidden';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {LoginComponent} from './views/login';
|
||||
import {AdminGuard, GuestGuard} from './guards';
|
||||
import {NotFoundComponent} from './views/not-found';
|
||||
import {ForbiddenComponent} from './views/forbidden';
|
||||
|
||||
import {ManagerComponent} from './views/manager';
|
||||
import {GenericErrorComponent} from './views/error/genericError.component';
|
||||
|
||||
import { ManagerComponent } from './views/manager';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
@@ -28,11 +26,6 @@ export const routes: Routes = [
|
||||
component: LoginComponent,
|
||||
canActivate: [GuestGuard]
|
||||
},
|
||||
// {
|
||||
// path: 'change-password',
|
||||
// component: ChangePasswordComponent,
|
||||
// canActivate: [LoginGuard]
|
||||
// },
|
||||
{
|
||||
path: '404',
|
||||
component: NotFoundComponent
|
||||
@@ -41,6 +34,10 @@ export const routes: Routes = [
|
||||
path: '403',
|
||||
component: ForbiddenComponent
|
||||
},
|
||||
{
|
||||
path: 'error',
|
||||
component: GenericErrorComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '/404'
|
||||
@@ -48,7 +45,9 @@ export const routes: Routes = [
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: 'disabled'
|
||||
})],
|
||||
exports: [RouterModule],
|
||||
providers: []
|
||||
})
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<app-header></app-header>
|
||||
<div class="content">
|
||||
@if (isOperationsConsoleRoute()) {
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<app-footer></app-footer>
|
||||
} @else {
|
||||
<div class="app-shell flex flex-column justify-space-between h-100">
|
||||
<app-header class="flex-none"></app-header>
|
||||
<div class="content flex h-100">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<app-footer class="flex-none"></app-footer>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,20 +2,10 @@
|
||||
display: block;
|
||||
color: rgba(0,0,0,.54);
|
||||
font-family: Roboto,"Helvetica Neue";
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 50px 70px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) and (max-width: 1279px) {
|
||||
.content {
|
||||
margin: 20px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
.content {
|
||||
margin: 8px 12px;
|
||||
}
|
||||
padding: 20px;
|
||||
max-height: calc(100vh - 169px);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TestBed, async } from '@angular/core/testing';
|
||||
import {TestBed, waitForAsync} from '@angular/core/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from './services';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
@@ -42,7 +42,7 @@ describe('AppComponent', () => {
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', async(() => {
|
||||
it('should create the app', waitForAsync(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {Component} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
// I remove temporary fontawesome5 and downgrade to fontawesome4
|
||||
import fontawesome from '@fortawesome/fontawesome';
|
||||
import solid from '@fortawesome/fontawesome-free-solid/';
|
||||
fontawesome.library.add(solid);
|
||||
import fontawesome from '@fortawesome/fontawesome';
|
||||
import {
|
||||
faCheckCircle,
|
||||
faExclamationCircle,
|
||||
faExclamationTriangle,
|
||||
faPause,
|
||||
faPlay,
|
||||
faTimesCircle
|
||||
} from '@fortawesome/fontawesome-free-solid';
|
||||
|
||||
fontawesome.library.add(faCheckCircle, faExclamationCircle, faExclamationTriangle, faPause, faPlay, faTimesCircle);
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
|
||||
export class AppComponent {
|
||||
}
|
||||
export class AppComponent {
|
||||
constructor(private router: Router) {
|
||||
}
|
||||
|
||||
isOperationsConsoleRoute(): boolean {
|
||||
const url = this.router.url || '/';
|
||||
return url === '/' || url.startsWith('/manager');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule, APP_INITIALIZER} from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
|
||||
import {JWT_OPTIONS, JwtModule} from "@auth0/angular-jwt";
|
||||
import {JWT_OPTIONS, JwtModule} from '@auth0/angular-jwt';
|
||||
|
||||
// material
|
||||
import {MatIconRegistry} from '@angular/material/icon';
|
||||
@@ -17,9 +17,14 @@ import {MatToolbarModule} from '@angular/material/toolbar';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
import {MatListModule} from '@angular/material/list';
|
||||
import {MatSidenavModule} from '@angular/material/sidenav';
|
||||
import {MatDialogModule} from '@angular/material/dialog';
|
||||
|
||||
import {OwlDateTimeModule, OwlNativeDateTimeModule} from '@danielmoncada/angular-datetime-picker';
|
||||
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { ManagerComponent } from './views/manager';
|
||||
@@ -32,10 +37,11 @@ import {
|
||||
HeaderComponent,
|
||||
FooterComponent,
|
||||
GithubComponent,
|
||||
SchedulerConfigComponent,
|
||||
SchedulerControlComponent,
|
||||
LogsPanelComponent,
|
||||
ProgressPanelComponent
|
||||
ProgressPanelComponent,
|
||||
TriggerListComponent,
|
||||
SimpleTriggerConfigComponent
|
||||
} from './components';
|
||||
|
||||
import {
|
||||
@@ -44,125 +50,100 @@ import {
|
||||
UserService,
|
||||
SchedulerService,
|
||||
ConfigService,
|
||||
ProgressWebsocketService,
|
||||
LogsWebsocketService,
|
||||
getHtmlBaseUrl
|
||||
} from './services';
|
||||
import { ChangePasswordComponent } from './views/change-password/change-password.component';
|
||||
getHtmlBaseUrl,
|
||||
LogsRxWebsocketService,
|
||||
ProgressRxWebsocketService,
|
||||
TriggerService,
|
||||
CalendarService
|
||||
} from './services';
|
||||
import { ForbiddenComponent } from './views/forbidden/forbidden.component';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { environment } from '../environments/environment';
|
||||
import JobService from './services/job.service';
|
||||
import {GenericErrorComponent} from './views/error/genericError.component';
|
||||
|
||||
export function initUserFactory(userService: UserService) {
|
||||
return () => userService.jsessionInitUser();
|
||||
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: () => {
|
||||
return apiService.getToken();
|
||||
},
|
||||
whitelistedDomains: ['localhost:8080', 'localhost:4200']
|
||||
allowedDomains: ['localhost:8080', 'localhost:4200']
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
HeaderComponent,
|
||||
FooterComponent,
|
||||
ManagerComponent,
|
||||
GithubComponent,
|
||||
LoginComponent,
|
||||
NotFoundComponent,
|
||||
AccountMenuComponent,
|
||||
SchedulerConfigComponent,
|
||||
SchedulerControlComponent,
|
||||
LogsPanelComponent,
|
||||
ProgressPanelComponent,
|
||||
ChangePasswordComponent,
|
||||
ForbiddenComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientModule,
|
||||
AppRoutingModule,
|
||||
JwtModule.forRoot({
|
||||
jwtOptionsProvider: {
|
||||
provide: JWT_OPTIONS,
|
||||
useFactory: jwtOptionsFactory,
|
||||
deps: [ApiService]
|
||||
}
|
||||
}),
|
||||
MatMenuModule,
|
||||
MatTooltipModule,
|
||||
MatButtonModule,
|
||||
MatChipsModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatToolbarModule,
|
||||
MatCardModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatProgressBarModule,
|
||||
FlexLayoutModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_BASE_HREF,
|
||||
useValue: getHtmlBaseUrl()
|
||||
},
|
||||
{
|
||||
'provide': APP_INITIALIZER,
|
||||
'useFactory': initUserFactory,
|
||||
'deps': [UserService],
|
||||
'multi': true
|
||||
},
|
||||
LoginGuard,
|
||||
GuestGuard,
|
||||
AdminGuard,
|
||||
SchedulerService,
|
||||
ProgressWebsocketService,
|
||||
LogsWebsocketService,
|
||||
AuthService,
|
||||
ApiService,
|
||||
UserService,
|
||||
ConfigService,
|
||||
MatIconRegistry
|
||||
// StompService,
|
||||
// ServerSocket
|
||||
// {
|
||||
// provide: StompConfig,
|
||||
// useValue: stompConfig
|
||||
// }
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
@NgModule({ declarations: [
|
||||
AppComponent,
|
||||
HeaderComponent,
|
||||
FooterComponent,
|
||||
ManagerComponent,
|
||||
GithubComponent,
|
||||
LoginComponent,
|
||||
NotFoundComponent,
|
||||
AccountMenuComponent,
|
||||
SimpleTriggerConfigComponent,
|
||||
SchedulerControlComponent,
|
||||
LogsPanelComponent,
|
||||
ProgressPanelComponent,
|
||||
ForbiddenComponent,
|
||||
GenericErrorComponent,
|
||||
TriggerListComponent
|
||||
],
|
||||
bootstrap: [AppComponent], imports: [BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
AppRoutingModule,
|
||||
JwtModule.forRoot({
|
||||
jwtOptionsProvider: {
|
||||
provide: JWT_OPTIONS,
|
||||
useFactory: jwtOptionsFactory,
|
||||
deps: [ApiService]
|
||||
}
|
||||
}),
|
||||
MatDialogModule,
|
||||
MatMenuModule,
|
||||
MatTooltipModule,
|
||||
MatButtonModule,
|
||||
MatChipsModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatToolbarModule,
|
||||
MatCardModule,
|
||||
MatListModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatProgressBarModule,
|
||||
OwlDateTimeModule,
|
||||
OwlNativeDateTimeModule,
|
||||
MatSidenavModule,
|
||||
], providers: [
|
||||
{
|
||||
provide: APP_BASE_HREF,
|
||||
useValue: getHtmlBaseUrl()
|
||||
},
|
||||
{
|
||||
'provide': APP_INITIALIZER,
|
||||
'useFactory': initUserFactory,
|
||||
'deps': [UserService],
|
||||
'multi': true
|
||||
},
|
||||
LoginGuard,
|
||||
GuestGuard,
|
||||
AdminGuard,
|
||||
SchedulerService,
|
||||
JobService,
|
||||
TriggerService,
|
||||
CalendarService,
|
||||
ProgressRxWebsocketService,
|
||||
LogsRxWebsocketService,
|
||||
AuthService,
|
||||
ApiService,
|
||||
UserService,
|
||||
ConfigService,
|
||||
MatIconRegistry,
|
||||
provideHttpClient(withInterceptorsFromDi())
|
||||
] })
|
||||
export class AppModule { }
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<p style="margin: 0px auto; padding: 0px; color: rgba(255, 255, 255, 0.541176); max-width: 356px;">
|
||||
Hand crafted with love by
|
||||
<a href="https://github.com/fabioformosa" style="color: rgba(255, 255, 255, 0.870588);">Fabio Formosa</a>
|
||||
</p>
|
||||
<a style="margin-top: 22px;" mat-icon-button href="https://github.com/fabioformosa/quartz-manager">
|
||||
<img src="assets/image/github.png"/>
|
||||
</a>
|
||||
<mat-toolbar id="footer" class="flex flex-row justify-center align-items-center" style="color: rgba(255, 255, 255, 0.541176);">
|
||||
<a href="https://github.com/fabioformosa/quartz-manager" class="flex flex-row align-items-center" style="gap: 6px">
|
||||
<div class="flex"><img src="assets/image/github.png"/></div>
|
||||
<div class="font-size-14 font-weight-500 display-block line-height-100">Quartz Manager</div>
|
||||
</a>
|
||||
<!-- Hand crafted with love by -->
|
||||
<!-- <a href="https://github.com/fabioformosa" style="color: rgba(255, 255, 255, 0.870588);">Fabio Formosa</a>-->
|
||||
</mat-toolbar>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
:host {
|
||||
display: block;
|
||||
font-weight: 300;
|
||||
font-size: 15px;
|
||||
display: block;
|
||||
:host{
|
||||
//position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
#footer{
|
||||
background-color: rgb(33, 33, 33);
|
||||
height: 236px;
|
||||
padding: 72px 24px;
|
||||
font-size: 15px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
a {
|
||||
text-decoration: none;
|
||||
cursor: auto;
|
||||
cursor: pointer;
|
||||
color: #FFFFFF;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -21,7 +23,5 @@
|
||||
font-weight: 300;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class FooterComponent implements OnInit {
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-github',
|
||||
templateUrl: './github.component.html',
|
||||
styleUrls: ['./github.component.scss']
|
||||
selector: 'app-github',
|
||||
templateUrl: './github.component.html',
|
||||
styleUrls: ['./github.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class GithubComponent implements OnInit {
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
<button mat-menu-item [routerLink]="['/change-password']">CHANGE PASSWORD</button>
|
||||
<button mat-menu-item (click)="logout()">SIGN OUT</button>
|
||||
<button mat-menu-item (click)="logout()">Logout</button>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,9 +7,10 @@ import {
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account-menu',
|
||||
templateUrl: './account-menu.component.html',
|
||||
styleUrls: ['./account-menu.component.scss']
|
||||
selector: 'app-account-menu',
|
||||
templateUrl: './account-menu.component.html',
|
||||
styleUrls: ['./account-menu.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class AccountMenuComponent implements OnInit {
|
||||
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
<mat-toolbar color="primary" class="app-navbar">
|
||||
<button mat-button mat-ripple routerLink="/">
|
||||
<img alt="Quartz Manager" class="app-angular-logo" src="assets/image/angular-white-transparent.svg">
|
||||
<span>Quartz Manager</span>
|
||||
</button>
|
||||
|
||||
<div class="right">
|
||||
<div fxFlex="1 1 auto" fxLayout="row" fxLayoutAlign="flex-end center">
|
||||
<button *ngIf="!hasSignedIn()" routerLink="/login" mat-button mat-ripple>
|
||||
<span>Login</span>
|
||||
</button>
|
||||
<button
|
||||
class="greeting-button"
|
||||
*ngIf="hasSignedIn() && !noAuthenticationRequired()"
|
||||
mat-button mat-ripple
|
||||
[matMenuTriggerFor]="accountMenu">
|
||||
<span>Hi, {{userName()}}</span>
|
||||
</button>
|
||||
<button
|
||||
class="greeting-hamburger"
|
||||
*ngIf="hasSignedIn()"
|
||||
mat-icon-button mat-ripple
|
||||
[matMenuTriggerFor]="accountMenu">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<mat-menu #accountMenu
|
||||
class="app-header-accountMenu"
|
||||
yposition="below"
|
||||
[overlapTrigger]="false">
|
||||
<app-account-menu ></app-account-menu>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
||||
<mat-toolbar color="primary" class="app-navbar">
|
||||
<button mat-button mat-ripple routerLink="/">
|
||||
<!-- <img alt="Quartz Manager" class="app-angular-logo" src="assets/image/angular-white-transparent.svg">-->
|
||||
<span>Quartz Manager</span>
|
||||
</button>
|
||||
|
||||
<div class="right">
|
||||
<div class="flex flex-row flex-1 justify-flex-end align-items-center">
|
||||
@if (!hasSignedIn() && !noAuthenticationRequired()) {
|
||||
<button routerLink="/login" mat-button mat-ripple>
|
||||
<span>Login</span>
|
||||
</button>
|
||||
} @if (hasSignedIn() && !noAuthenticationRequired()) {
|
||||
<button
|
||||
class="greeting-button"
|
||||
mat-button
|
||||
mat-ripple
|
||||
[matMenuTriggerFor]="accountMenu">
|
||||
<span>Hi, {{ userName() }}</span>
|
||||
</button>
|
||||
} @if (hasSignedIn()) {
|
||||
<button
|
||||
class="greeting-hamburger"
|
||||
mat-icon-button
|
||||
mat-ripple
|
||||
[matMenuTriggerFor]="accountMenu">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<mat-menu
|
||||
#accountMenu
|
||||
class="app-header-accountMenu"
|
||||
yposition="below"
|
||||
[overlapTrigger]="false">
|
||||
<app-account-menu></app-account-menu>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
||||
@@ -2,14 +2,15 @@ import { Component, OnInit } from '@angular/core';
|
||||
import {
|
||||
UserService,
|
||||
AuthService,
|
||||
NO_AUTH
|
||||
// NO_AUTH
|
||||
} from '../../services';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss']
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class HeaderComponent implements OnInit {
|
||||
|
||||
@@ -32,8 +33,8 @@ export class HeaderComponent implements OnInit {
|
||||
return !!this.userService.currentUser;
|
||||
}
|
||||
|
||||
noAuthenticationRequired = () => this.hasSignedIn() && this.userService.currentUser === NO_AUTH;
|
||||
|
||||
noAuthenticationRequired = () => !this.hasSignedIn() && this.userService.isAnAnonymousUser === true;
|
||||
|
||||
|
||||
userName() {
|
||||
const user = this.userService.currentUser;
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from './header';
|
||||
export * from './github';
|
||||
export * from './footer';
|
||||
export * from './logs-panel';
|
||||
export * from './scheduler-config';
|
||||
export * from './scheduler-control';
|
||||
export * from './progress-panel';
|
||||
export * from './trigger-list';
|
||||
export * from './simple-trigger-config';
|
||||
|
||||
@@ -1,29 +1,67 @@
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title><b>JOB LOGS</b></mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div id="logs">
|
||||
<div *ngFor = "let log of logs" fxLayout="row" fxLayout.xs="column">
|
||||
<div fxFlex="1 1 20%">
|
||||
<span [ngClass]="{'animated zoomIn firstLog': $first}"> [{{log.time|date:'medium'}}]</span>
|
||||
</div>
|
||||
<div fxFlex="1 1 10%">
|
||||
<span [ngClass]="{'animated zoomIn firstLog': $first}">
|
||||
<i class = "fas" [ngClass]="{'fa-check-circle green': log.type == 'INFO',
|
||||
'fa-exclamation-triangle yellow': log.type == 'WARN',
|
||||
'fa-times-circle red': log.type == 'ERROR'}"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div fxFlex="1 1 20%">
|
||||
<span [ngClass]="{'animated zoomIn firstLog': $first}">
|
||||
{{log.threadName}}
|
||||
</span>
|
||||
</div>
|
||||
<div fxFlex="1 1 50%">
|
||||
<span [ngClass]="{'animated zoomIn firstLog': $first}"> {{log.msg}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="flex flex-1 max-h-100">
|
||||
<mat-card-header class="pb-16">
|
||||
<mat-card-subtitle><b>JOB LOGS</b></mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="flex flex-1 overflow-y-auto">
|
||||
<div class="flex-1">
|
||||
@if (!selectedTriggerName && (!logs || logs.length == 0)) {
|
||||
<div class="h-100 w-100" style="text-align: center">
|
||||
<img
|
||||
src="assets/image/logs.svg"
|
||||
alt="no logs"
|
||||
width="320"
|
||||
style="margin-top: 6em" />
|
||||
</div>
|
||||
} @if (isWaitingForLogs()) {
|
||||
<div
|
||||
class="waitingLogs flex flex-column align-items-center justify-center gap-12">
|
||||
<mat-spinner diameter="36"></mat-spinner>
|
||||
<div>Waiting for logs from {{ selectedTriggerName }}...</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div id="logs" class="w-100" style="height: 100%">
|
||||
@for (log of logs; track log; let first = $first) {
|
||||
<div
|
||||
class="log-row flex flex-row gap-10">
|
||||
<div style="flex: 1; max-width: 300px">
|
||||
<span
|
||||
[ngClass]="{
|
||||
'animate__animated animate__zoomIn zoomIn firstLog': first
|
||||
}">
|
||||
[{{ log.time | date : 'medium' }}]</span
|
||||
>
|
||||
</div>
|
||||
<div style="flex: 1; max-width: 16px">
|
||||
<span [ngClass]="{ 'animated zoomIn firstLog': first }">
|
||||
<i
|
||||
class="fas"
|
||||
[ngClass]="{
|
||||
'fa-check-circle green': log.type == 'INFO',
|
||||
'fa-exclamation-triangle yellow': log.type == 'WARN',
|
||||
'fa-times-circle red': log.type == 'ERROR'
|
||||
}"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div style="flex: 1; max-width: 250px">
|
||||
<span
|
||||
[ngClass]="{
|
||||
'animate__animated animate__zoomIn zoomIn firstLog': first
|
||||
}">
|
||||
{{ log.threadName }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="flex: 1">
|
||||
<span
|
||||
[ngClass]="{
|
||||
'animate__animated animate__zoomIn zoomIn firstLog': first
|
||||
}">
|
||||
{{ log.msg }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
:host {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.red{
|
||||
color: red;
|
||||
}
|
||||
@@ -7,4 +13,37 @@
|
||||
|
||||
.yellow{
|
||||
color: gold;
|
||||
}
|
||||
}
|
||||
|
||||
#logs{
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.waitingLogs {
|
||||
color: #6b7280;
|
||||
height: 100%;
|
||||
min-height: 180px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ===== Scrollbar CSS ===== */
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: #b8b8b8 #ffffff;
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #b8b8b8;
|
||||
border-radius: 10px;
|
||||
border: 3px solid #ffffff;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import {Subject} from 'rxjs';
|
||||
import {LogsPanelComponent} from './logs-panel.component';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {jest} from '@jest/globals';
|
||||
|
||||
describe('LogsPanelComponent', () => {
|
||||
|
||||
const ngZone = {run: jest.fn((fn: () => void) => fn())};
|
||||
|
||||
beforeEach(() => ngZone.run.mockClear());
|
||||
|
||||
it('should subscribe to the selected trigger logs topic', () => {
|
||||
const messages = new Subject<any>();
|
||||
const logsRxWebsocketService = {
|
||||
watch: jest.fn(() => messages.asObservable())
|
||||
};
|
||||
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
|
||||
expect(logsRxWebsocketService.watch.mock.calls[0]).toEqual(['/topic/logs/trigger-1']);
|
||||
expect(component.selectedTriggerName).toEqual('trigger-1');
|
||||
expect(component.isWaitingForLogs()).toBeTruthy();
|
||||
|
||||
const logRecord = {
|
||||
date: new Date(),
|
||||
type: 'INFO',
|
||||
message: 'job completed',
|
||||
threadName: 'worker-1'
|
||||
};
|
||||
messages.next({body: JSON.stringify(logRecord)});
|
||||
|
||||
expect(ngZone.run).toHaveBeenCalled();
|
||||
expect(component.logs[0]).toEqual({
|
||||
time: logRecord.date.toISOString(),
|
||||
type: 'INFO',
|
||||
msg: 'job completed',
|
||||
threadName: 'worker-1'
|
||||
});
|
||||
expect(component.isWaitingForLogs()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should unsubscribe from the previous topic when the trigger changes', () => {
|
||||
const firstMessages = new Subject<any>();
|
||||
const secondMessages = new Subject<any>();
|
||||
const logsRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
.mockReturnValueOnce(secondMessages.asObservable())
|
||||
};
|
||||
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
const firstSubscription = component.topicSubscription;
|
||||
jest.spyOn(firstSubscription, 'unsubscribe');
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-2', null);
|
||||
|
||||
expect(firstSubscription.unsubscribe).toHaveBeenCalled();
|
||||
expect(logsRxWebsocketService.watch.mock.calls[1]).toEqual(['/topic/logs/trigger-2']);
|
||||
});
|
||||
|
||||
it('should clear logs when the trigger changes', () => {
|
||||
const firstMessages = new Subject<any>();
|
||||
const secondMessages = new Subject<any>();
|
||||
const logsRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
.mockReturnValueOnce(secondMessages.asObservable())
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
};
|
||||
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
firstMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})});
|
||||
expect(component.logs.length).toEqual(1);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-2', null);
|
||||
expect(component.logs).toEqual([]);
|
||||
expect(component.selectedTriggerName).toEqual('trigger-2');
|
||||
expect(component.isWaitingForLogs()).toBeTruthy();
|
||||
|
||||
secondMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'second log', threadName: 'worker-2'})});
|
||||
expect(component.logs.length).toEqual(1);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
expect(component.logs).toEqual([]);
|
||||
expect(component.selectedTriggerName).toEqual('trigger-1');
|
||||
expect(component.isWaitingForLogs()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should clear logs when no trigger is selected', () => {
|
||||
const messages = new Subject<any>();
|
||||
const logsRxWebsocketService = {
|
||||
watch: jest.fn(() => messages.asObservable())
|
||||
};
|
||||
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
messages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})});
|
||||
|
||||
component.triggerKey = null;
|
||||
|
||||
expect(component.logs).toEqual([]);
|
||||
expect(component.selectedTriggerName).toBeNull();
|
||||
expect(component.isWaitingForLogs()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should ignore destroy when no topic was selected', () => {
|
||||
const logsRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
};
|
||||
const component = new LogsPanelComponent(logsRxWebsocketService as any, null, ngZone as any);
|
||||
|
||||
expect(() => component.ngOnDestroy()).not.toThrow();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,48 +1,97 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import {Component, Input, NgZone, OnDestroy, OnInit} from '@angular/core';
|
||||
|
||||
import { LogsWebsocketService, ApiService } from '../../services';
|
||||
import { Observable } from 'rxjs';
|
||||
import {ApiService} from '../../services';
|
||||
import {LogsRxWebsocketService} from '../../services/logs.rx-websocket.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
|
||||
@Component({
|
||||
selector: 'logs-panel',
|
||||
templateUrl: './logs-panel.component.html',
|
||||
styleUrls: ['./logs-panel.component.scss']
|
||||
|
||||
@Component({
|
||||
selector: 'logs-panel',
|
||||
templateUrl: './logs-panel.component.html',
|
||||
styleUrls: ['./logs-panel.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class LogsPanelComponent implements OnInit {
|
||||
export class LogsPanelComponent implements OnInit, OnDestroy {
|
||||
|
||||
MAX_LOGS : number = 20;
|
||||
MAX_LOGS = 30;
|
||||
|
||||
logs : Array<any> = new Array();
|
||||
logs = new Array();
|
||||
|
||||
selectedTriggerName: string;
|
||||
|
||||
constructor(
|
||||
private logsWebsocketService: LogsWebsocketService,
|
||||
private apiService : ApiService
|
||||
) { }
|
||||
topicSubscription;
|
||||
|
||||
private selectedTriggerKey: TriggerKey;
|
||||
|
||||
constructor(
|
||||
private logsRxWebsocketService: LogsRxWebsocketService,
|
||||
private apiService: ApiService,
|
||||
private ngZone: NgZone
|
||||
) {
|
||||
}
|
||||
|
||||
@Input()
|
||||
set triggerKey(triggerKey: TriggerKey) {
|
||||
if (!triggerKey || !triggerKey.name) {
|
||||
this._unsubscribeFromTopic();
|
||||
this.selectedTriggerKey = null;
|
||||
this.selectedTriggerName = null;
|
||||
this._resetLogs();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedTriggerKey?.name === triggerKey.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._resetLogs();
|
||||
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
||||
this.selectedTriggerName = triggerKey.name;
|
||||
this._subscribeToTheTopic(this.selectedTriggerKey);
|
||||
}
|
||||
|
||||
isWaitingForLogs = (): boolean => !!this.selectedTriggerName && (!this.logs || this.logs.length === 0);
|
||||
|
||||
ngOnInit() {
|
||||
let 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(logRecord => this.ngZone.run(() => this._showNewLog(logRecord)), (err) => {
|
||||
console.log(err);
|
||||
// TODO in case of 401
|
||||
// this.apiService.get('/quartz-manager/session/refresh');
|
||||
});
|
||||
};
|
||||
|
||||
ngOnDestroy() {
|
||||
this._unsubscribeFromTopic();
|
||||
}
|
||||
|
||||
private _unsubscribeFromTopic() {
|
||||
if (this.topicSubscription) {
|
||||
this.topicSubscription.unsubscribe();
|
||||
this.topicSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _resetLogs() {
|
||||
this.logs = [];
|
||||
}
|
||||
|
||||
_showNewLog = (logRecord) => {
|
||||
if(this.logs.length > this.MAX_LOGS)
|
||||
if (this.logs.length > this.MAX_LOGS) {
|
||||
this.logs.pop();
|
||||
|
||||
}
|
||||
|
||||
this.logs.unshift({
|
||||
time : logRecord.date,
|
||||
type : logRecord.type,
|
||||
msg : logRecord.message,
|
||||
threadName : logRecord.threadName
|
||||
time: logRecord.date,
|
||||
type: logRecord.type,
|
||||
msg: logRecord.message,
|
||||
threadName: logRecord.threadName
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,72 @@
|
||||
<!-- <div class="progress" [hidden]="progress.percentage < 0">
|
||||
<div class="progress-bar"
|
||||
role="progressbar"
|
||||
[ngStyle]="{width: percentageStr}">
|
||||
{{percentageStr}}
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title><b>JOB PROGRESS</b></mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div>
|
||||
<mat-progress-bar mode="determinate" value="{{progress.percentage}}"></mat-progress-bar>
|
||||
{{percentageStr}}
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<mat-chip>counter</mat-chip>
|
||||
<span class="animated pulse">{{progress.timesTriggered}}</span> <span ng-show="progress.repeatCount > 0">/ {{progress.repeatCount}} </span>
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<mat-chip>job key</mat-chip> <span class="animated pulse">{{progress.jobKey}}</span><br>
|
||||
<mat-chip>job class</mat-chip> <span class="animated pulse">{{progress.jobClass}}</span><br/>
|
||||
|
||||
<br/>
|
||||
<mat-chip>prev fire time</mat-chip> <span class="animated pulse">{{progress.previousFireTime|date:'dd-MM-yyyy HH:mm:ss'}}</span><br/>
|
||||
<mat-chip>next fire time</mat-chip> <span class="animated pulse">{{progress.nextFireTime|date:'dd-MM-yyyy HH:mm:ss'}}</span><br/>
|
||||
<mat-chip>final fire time</mat-chip> <span class="animated pulse">{{progress.finalFireTime|date:'dd-MM-yyyy HH:mm:ss'}}</span><br/>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<!-- <div class="progress" [hidden]="progress.percentage < 0">
|
||||
<div class="progress-bar"
|
||||
role="progressbar"
|
||||
[ngStyle]="{width: percentageStr}">
|
||||
{{percentageStr}}
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<mat-card
|
||||
style="padding-bottom: 0"
|
||||
[ngClass]="{ 'progress-updated': progressUpdated }">
|
||||
<mat-card-header style="padding-bottom: 16px">
|
||||
<mat-card-subtitle><b>JOB PROGRESS</b></mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
@if (progress.percentage !== -1) {
|
||||
<div id="progressBarBox">
|
||||
<mat-progress-bar
|
||||
mode="determinate"
|
||||
value="{{ progress.percentage }}"></mat-progress-bar>
|
||||
{{ percentageStr }}
|
||||
</div>
|
||||
} @if (progress.timesTriggered) {
|
||||
<div id="counterBox" class="flex flex-row justify-center">
|
||||
<span id="timesTriggeredCounter" class="animated pulse">{{
|
||||
progress.timesTriggered
|
||||
}}</span>
|
||||
@if (progress.repeatCount > 0) {
|
||||
<span id="totCounter"> / {{ progress.repeatCount }} </span>
|
||||
}
|
||||
</div>
|
||||
} @if (progress.timesTriggered) {
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
|
||||
<div class="flex flex-row align-items-center justify-space-around">
|
||||
<div class="fireBox">
|
||||
<div class="fireBoxHeader">prev fire time</div>
|
||||
<div class="fireBoxContent">
|
||||
<span class="animated pulse">{{
|
||||
progress.previousFireTime | date : 'dd-MM-yyyy HH:mm:ss'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="fireBoxContent" [hidden]="progress.previousFireTime">
|
||||
<span>-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fireBox">
|
||||
<div class="fireBoxHeader">next fire time</div>
|
||||
<div class="fireBoxContent">
|
||||
<span class="animated pulse">{{
|
||||
progress.nextFireTime | date : 'dd-MM-yyyy HH:mm:ss'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="fireBoxContent" [hidden]="progress.nextFireTime">
|
||||
<span>-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fireBox">
|
||||
<div class="fireBoxHeader">final fire time</div>
|
||||
<div class="fireBoxContent">
|
||||
<span class="animated pulse">{{
|
||||
progress.finalFireTime | date : 'dd-MM-yyyy HH:mm:ss'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="fireBoxContent" [hidden]="progress.finalFireTime">
|
||||
<span>-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#timesTriggeredCounter{
|
||||
font-size: 2em;
|
||||
}
|
||||
#totCounter{
|
||||
margin-top: auto;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
#counterBox{
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.fireBox{
|
||||
width: 100%;
|
||||
border-right: 1px solid rgba(0,0,0,.12);
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.fireBox:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.fireBoxHeader{
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
color: grey;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import {Subject} from 'rxjs';
|
||||
import {ProgressPanelComponent} from './progress-panel.component';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {jest} from '@jest/globals';
|
||||
|
||||
describe('ProgressPanelComponent', () => {
|
||||
|
||||
const ngZone = {run: jest.fn((fn: () => void) => fn())};
|
||||
|
||||
beforeEach(() => ngZone.run.mockClear());
|
||||
|
||||
it('should subscribe to the selected trigger progress topic', () => {
|
||||
jest.useFakeTimers();
|
||||
const messages = new Subject<any>();
|
||||
const progressRxWebsocketService = {
|
||||
watch: jest.fn(() => messages.asObservable())
|
||||
};
|
||||
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
|
||||
expect(progressRxWebsocketService.watch.mock.calls[0]).toEqual(['/topic/progress/trigger-1']);
|
||||
|
||||
messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(ngZone.run).toHaveBeenCalled();
|
||||
expect(component.progress.percentage).toEqual(75);
|
||||
expect(component.percentageStr).toEqual('75%');
|
||||
expect(component.progressUpdated).toBeTruthy();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should unsubscribe from the previous topic when the trigger changes', () => {
|
||||
const firstMessages = new Subject<any>();
|
||||
const secondMessages = new Subject<any>();
|
||||
const progressRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
.mockReturnValueOnce(secondMessages.asObservable())
|
||||
};
|
||||
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
const firstSubscription = component.topicSubscription;
|
||||
jest.spyOn(firstSubscription, 'unsubscribe');
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-2', null);
|
||||
|
||||
expect(firstSubscription.unsubscribe).toHaveBeenCalled();
|
||||
expect(progressRxWebsocketService.watch.mock.calls[1]).toEqual(['/topic/progress/trigger-2']);
|
||||
});
|
||||
|
||||
it('should reset progress when the trigger changes', () => {
|
||||
const firstMessages = new Subject<any>();
|
||||
const secondMessages = new Subject<any>();
|
||||
const progressRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
.mockReturnValueOnce(secondMessages.asObservable())
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
};
|
||||
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
firstMessages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
|
||||
expect(component.progress.percentage).toEqual(75);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-2', null);
|
||||
expect(component.progress.percentage).toEqual(-1);
|
||||
expect(component.percentageStr).toBeNull();
|
||||
expect(component.progressUpdated).toBeFalsy();
|
||||
|
||||
secondMessages.next({body: JSON.stringify({percentage: 20, timesTriggered: 1})});
|
||||
expect(component.progress.percentage).toEqual(20);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
expect(component.progress.percentage).toEqual(-1);
|
||||
});
|
||||
|
||||
it('should reset progress when no trigger is selected', () => {
|
||||
const messages = new Subject<any>();
|
||||
const progressRxWebsocketService = {
|
||||
watch: jest.fn(() => messages.asObservable())
|
||||
};
|
||||
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
|
||||
|
||||
component.triggerKey = null;
|
||||
|
||||
expect(component.progress.percentage).toEqual(-1);
|
||||
expect(component.percentageStr).toBeNull();
|
||||
expect(component.progressUpdated).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should ignore destroy when no topic was selected', () => {
|
||||
const progressRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
};
|
||||
const component = new ProgressPanelComponent(progressRxWebsocketService as any, ngZone as any);
|
||||
|
||||
expect(() => component.ngOnDestroy()).not.toThrow();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,85 +1,93 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'
|
||||
import { ProgressWebsocketService } from '../../services';
|
||||
import {Component, Input, NgZone, OnDestroy, OnInit} from '@angular/core'
|
||||
import TriggerFiredBundle from '../../model/trigger-fired-bundle.model';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {ProgressRxWebsocketService} from '../../services/progress.rx-websocket.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
// import {Message} from '@stomp/stompjs';
|
||||
|
||||
// import { Subscription } from 'rxjs/Subscription';
|
||||
// import {StompService} from '@stomp/ng2-stompjs';
|
||||
|
||||
// import { QueueingSubject } from 'queueing-subject'
|
||||
// import websocketConnect from 'rxjs-websockets'
|
||||
// import 'rxjs/add/operator/share'
|
||||
// import {ServerSocket} from '../../services/qz.socket.service'
|
||||
|
||||
@Component({
|
||||
selector: 'progress-panel',
|
||||
templateUrl: './progress-panel.component.html',
|
||||
styleUrls: ['./progress-panel.component.scss']
|
||||
@Component({
|
||||
selector: 'progress-panel',
|
||||
templateUrl: './progress-panel.component.html',
|
||||
styleUrls: ['./progress-panel.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class ProgressPanelComponent implements OnInit {
|
||||
export class ProgressPanelComponent implements OnInit, OnDestroy {
|
||||
|
||||
progress: TriggerFiredBundle = ProgressPanelComponent._buildEmptyProgress();
|
||||
percentageStr: string;
|
||||
progressUpdated = false;
|
||||
|
||||
progress : any = {}
|
||||
percentageStr : string
|
||||
topicSubscription;
|
||||
private selectedTriggerKey: TriggerKey;
|
||||
|
||||
// // Stream of messages
|
||||
// private subscription: Subscription;
|
||||
// public messages: Observable<Message>;
|
||||
// // Subscription status
|
||||
// public subscribed: boolean;
|
||||
// // Array of historic message (bodies)
|
||||
// public mq: Array<string> = [];
|
||||
constructor(
|
||||
private progressRxWebsocketService: ProgressRxWebsocketService,
|
||||
private ngZone: NgZone
|
||||
) { }
|
||||
|
||||
@Input()
|
||||
set triggerKey(triggerKey: TriggerKey) {
|
||||
if (!triggerKey || !triggerKey.name) {
|
||||
this._unsubscribeFromTopic();
|
||||
this.selectedTriggerKey = null;
|
||||
this._resetProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedTriggerKey?.name === triggerKey.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._resetProgress();
|
||||
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
||||
this._subscribeToTheTopic(this.selectedTriggerKey);
|
||||
}
|
||||
|
||||
private _subscribeToTheTopic = (triggerKey: TriggerKey) => {
|
||||
this._unsubscribeFromTopic();
|
||||
this.topicSubscription = this.progressRxWebsocketService.watch(`/topic/progress/${triggerKey.name}`)
|
||||
.pipe(map((msg: any) => JSON.parse(msg.body)))
|
||||
.subscribe(progress => this.ngZone.run(() => this.onNewProgressMsg(progress)), (err) => {
|
||||
console.log(err);
|
||||
// TODO in case of 401
|
||||
// this.apiService.get('/quartz-manager/session/refresh');
|
||||
});
|
||||
};
|
||||
|
||||
// private socketSubscription
|
||||
|
||||
constructor(
|
||||
private progressWebsocketService: ProgressWebsocketService,
|
||||
// private _stompService: StompService,
|
||||
// private serverSocket : ServerSocket
|
||||
) { }
|
||||
|
||||
onNewProgressMsg = (receivedMsg) => {
|
||||
if (receivedMsg.type == 'SUCCESS') {
|
||||
var newStatus = receivedMsg.message;
|
||||
this.progress = newStatus;
|
||||
this.percentageStr = this.progress.percentage + '%';
|
||||
}
|
||||
}
|
||||
onNewProgressMsg = (receivedMsg) => {
|
||||
this.progress = receivedMsg;
|
||||
this.percentageStr = this.progress.percentage + '%';
|
||||
this._markProgressUpdated();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
let 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './scheduler-config.component';
|
||||
@@ -1,38 +0,0 @@
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title><b>SCHEDULER CONFIG</b></mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div fxLayout="column">
|
||||
<form name="configForm" fxFlex="1 1 100%" #configForm="ngForm">
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="Freq [Num per day]" [(ngModel)]="config.triggerPerDay" name="triggerPerDay" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="Max Occurrences" [(ngModel)]="config.maxCount" name="maxCount" type="number">
|
||||
</mat-form-field>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<h5>Misfire Policy</h5>
|
||||
<div>RESCHEDULE NEXT WITH EXISTING COUNT</div>
|
||||
<div class="small">
|
||||
In case of misfire event, the trigger is re-scheduled to the next scheduled time after 'now' with the repeat count left unchanged (missed events are definitively lost).
|
||||
<br/>
|
||||
<strong>Warning:</strong> This policy could cause the trigger to go directly to the complete state if the end-time of the trigger has arrived,
|
||||
so this misfire instruction doesn't guarantee that the repeat counter reaches your max value, but it guarantees that the end-time doesn't go over the expected final fire time.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<button mat-raised-button
|
||||
type="button"
|
||||
(click)="submitConfig()">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -1,3 +0,0 @@
|
||||
.small{
|
||||
font-size: 0.8em;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { SchedulerService } from '../../services';
|
||||
import { SchedulerConfig } from '../../model/schedulerConfig.model'
|
||||
|
||||
@Component({
|
||||
selector: 'scheduler-config',
|
||||
templateUrl: './scheduler-config.component.html',
|
||||
styleUrls: ['./scheduler-config.component.scss']
|
||||
})
|
||||
export class SchedulerConfigComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private schedulerService: SchedulerService
|
||||
) { }
|
||||
|
||||
config : SchedulerConfig = new SchedulerConfig()
|
||||
configBackup : SchedulerConfig = new SchedulerConfig()
|
||||
|
||||
ngOnInit() {
|
||||
this.retrieveConfig()
|
||||
}
|
||||
|
||||
retrieveConfig = () => {
|
||||
this.schedulerService.getConfig()
|
||||
.subscribe(res => {
|
||||
this.config = new SchedulerConfig(res.triggerPerDay, res.maxCount)
|
||||
this.configBackup = res
|
||||
})
|
||||
}
|
||||
|
||||
submitConfig = () => {
|
||||
this.schedulerService.updateConfig(this.config)
|
||||
.subscribe(res => {
|
||||
this.configBackup = this.config;
|
||||
}, error => {
|
||||
this.config = this.configBackup;
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,15 +1,40 @@
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title><b>SCHEDULER CONTROLLER</b></mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<button id="schedulerControllerBtn1" mat-raised-button class="btn btn-default large-btn" (click)="startOrPause()">
|
||||
<span *ngIf = "schedulerState === 'running'">
|
||||
<i class="fas fa-pause red"></i>
|
||||
</span>
|
||||
<span *ngIf = "schedulerState === 'stopped' || schedulerState === 'paused'">
|
||||
<i class="fas fa-play green"></i>
|
||||
</span>
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="flex flex-row align-items-stretch gap-30">
|
||||
<button
|
||||
id="schedulerControllerBtn"
|
||||
mat-raised-button
|
||||
class="btn btn-default large-btn"
|
||||
(click)="startOrPause()">
|
||||
@if (scheduler?.status === 'RUNNING') {
|
||||
<span>
|
||||
<i class="fas fa-pause red"></i>
|
||||
</span>
|
||||
} @if (scheduler?.status === 'STOPPED' || scheduler?.status ===
|
||||
'PAUSED') {
|
||||
<span>
|
||||
<i class="fas fa-play green"></i>
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
<div class="flex flex-column align-items-center">
|
||||
<mat-card-subtitle style="margin: auto"
|
||||
><b>SCHEDULER</b></mat-card-subtitle
|
||||
>
|
||||
</div>
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
<div class="flex flex-column justify-space-between">
|
||||
<div><label>Name</label></div>
|
||||
<div>
|
||||
<span id="scheduler-name">{{ scheduler?.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-column justify-space-between">
|
||||
<div><label>Instance ID</label></div>
|
||||
<div>
|
||||
<span id="scheduler-instance">{{ scheduler?.instanceId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@@ -3,4 +3,20 @@
|
||||
}
|
||||
.green{
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
|
||||
label{
|
||||
color: grey;
|
||||
font-variant: small-caps;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#scheduler-name{
|
||||
text-transform: capitalize;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#scheduler-instance {
|
||||
text-transform: capitalize;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
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';
|
||||
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {DebugElement} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {Scheduler} from '../../model/scheduler.model';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
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(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatCardModule, MatDividerModule, MatIconModule, HttpClientTestingModule, RouterTestingModule],
|
||||
declarations: [SchedulerControlComponent],
|
||||
providers: [UserService, SchedulerService, ApiService, ConfigService]
|
||||
}).compileComponents();
|
||||
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
httpTestingController = TestBed.inject(HttpTestingController);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SchedulerControlComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should display the play button at the beginning since the scheduler is stopped', () => {
|
||||
expect(component).toBeDefined();
|
||||
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
|
||||
const mockScheduler = new Scheduler(schedulerName, schedulerId, stoppedStatus, []);
|
||||
getSchedulerReq.flush(mockScheduler);
|
||||
|
||||
expect(component.scheduler).toEqual(mockScheduler);
|
||||
expect(component.scheduler.status).toEqual(stoppedStatus);
|
||||
fixture.detectChanges();
|
||||
|
||||
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
|
||||
const schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
|
||||
expect(schedulerBtnDe).toBeTruthy();
|
||||
|
||||
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
|
||||
expect(playIconDe).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should switch the button to pause when the scheduler is started', () => {
|
||||
expect(component).toBeDefined();
|
||||
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
|
||||
const mockScheduler = new Scheduler(schedulerName, schedulerId, stoppedStatus, []);
|
||||
getSchedulerReq.flush(mockScheduler);
|
||||
fixture.detectChanges();
|
||||
|
||||
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
|
||||
let schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
|
||||
expect(schedulerBtnDe).toBeTruthy();
|
||||
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
|
||||
expect(playIconDe).toBeTruthy();
|
||||
|
||||
schedulerBtnDe.nativeElement.click();
|
||||
const startSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler/start');
|
||||
startSchedulerReq.flush(null);
|
||||
fixture.detectChanges();
|
||||
|
||||
schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
|
||||
const pauseIconDe = schedulerBtnDe.query(By.css('.fa-pause'));
|
||||
expect(pauseIconDe).toBeTruthy();
|
||||
|
||||
})
|
||||
|
||||
it('should switch the button to play when the scheduler is stopped', () => {
|
||||
expect(component).toBeDefined();
|
||||
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
|
||||
const mockScheduler = new Scheduler(schedulerName, schedulerId, 'RUNNING', []);
|
||||
getSchedulerReq.flush(mockScheduler);
|
||||
fixture.detectChanges();
|
||||
|
||||
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
|
||||
let schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
|
||||
expect(schedulerBtnDe).toBeTruthy();
|
||||
const pauseIconDe = schedulerBtnDe.query(By.css('.fa-pause'));
|
||||
expect(pauseIconDe).toBeTruthy();
|
||||
|
||||
schedulerBtnDe.nativeElement.click();
|
||||
const startSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler/standby');
|
||||
startSchedulerReq.flush(null);
|
||||
fixture.detectChanges();
|
||||
|
||||
schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
|
||||
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
|
||||
expect(playIconDe).toBeTruthy();
|
||||
|
||||
})
|
||||
|
||||
});
|
||||
@@ -1,54 +1,81 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { UserService, SchedulerService } from '../../services';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {SchedulerService, UserService} from '../../services';
|
||||
import {Scheduler} from '../../model/scheduler.model';
|
||||
|
||||
@Component({
|
||||
selector: 'scheduler-control',
|
||||
templateUrl: './scheduler-control.component.html',
|
||||
styleUrls: ['./scheduler-control.component.scss']
|
||||
@Component({
|
||||
selector: 'qrzmng-scheduler-control',
|
||||
templateUrl: './scheduler-control.component.html',
|
||||
styleUrls: ['./scheduler-control.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class SchedulerControlComponent implements OnInit {
|
||||
|
||||
schedulerState;
|
||||
|
||||
scheduler: Scheduler;
|
||||
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private schedulerService: SchedulerService
|
||||
) { }
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.schedulerService.getStatus().subscribe(res => {this.schedulerState = res.data}, err => {console.log(err)});
|
||||
this._getScheduler();
|
||||
}
|
||||
|
||||
startScheduler = function(){
|
||||
this.schedulerService.startScheduler().subscribe((res) => {this.schedulerState = 'running'}, (res) => {console.log(JSON.stringify(res))});
|
||||
private _getScheduler() {
|
||||
this.schedulerService.getScheduler()
|
||||
.subscribe(resp => this.scheduler = resp);
|
||||
}
|
||||
|
||||
startScheduler = function () {
|
||||
this.schedulerService.startScheduler().subscribe((res) => {
|
||||
this.scheduler.status = 'RUNNING'
|
||||
}, (res) => {
|
||||
console.log(JSON.stringify(res))
|
||||
});
|
||||
};
|
||||
|
||||
stopScheduler = function(){
|
||||
this.schedulerService.stopScheduler().subscribe((res) => {this.schedulerState = 'stopped'}, (res) => {console.log(JSON.stringify(res))});
|
||||
stopScheduler = function () {
|
||||
this.schedulerService.shutdownScheduler().subscribe((res) => {
|
||||
this.scheduler.status = 'STOPPED'
|
||||
}, (res) => {
|
||||
console.log(JSON.stringify(res))
|
||||
});
|
||||
};
|
||||
|
||||
pauseScheduler = function(){
|
||||
this.schedulerService.pauseScheduler().subscribe((res) => {this.schedulerState = 'paused'}, (res) => {console.log(JSON.stringify(res))});
|
||||
pauseScheduler = function () {
|
||||
this.schedulerService.standbyScheduler().subscribe((res) => {
|
||||
this.scheduler.status = 'PAUSED'
|
||||
}, (res) => {
|
||||
console.log(JSON.stringify(res))
|
||||
});
|
||||
};
|
||||
|
||||
resumeScheduler = function(){
|
||||
this.schedulerService.resumeScheduler().subscribe((res) => {this.schedulerState = 'running'}, (res) => {console.log(JSON.stringify(res))});
|
||||
resumeScheduler = function () {
|
||||
this.schedulerService.resumeScheduler().subscribe((res) => {
|
||||
this.scheduler.status = 'RUNNING'
|
||||
}, (res) => {
|
||||
console.log(JSON.stringify(res))
|
||||
});
|
||||
};
|
||||
|
||||
stop = function(){
|
||||
if(this.schedulerState != 'stopped')
|
||||
stop = function () {
|
||||
if (this.scheduler.status !== 'STOPPED') {
|
||||
this.stopScheduler();
|
||||
}
|
||||
}
|
||||
|
||||
startOrPause = function(){
|
||||
switch (this.schedulerState) {
|
||||
case 'running': this.pauseScheduler();
|
||||
break;
|
||||
case 'paused': this.resumeScheduler();
|
||||
break;
|
||||
default:
|
||||
this.startScheduler();
|
||||
break;
|
||||
startOrPause = function () {
|
||||
switch (this.scheduler.status) {
|
||||
case 'RUNNING':
|
||||
this.pauseScheduler();
|
||||
break;
|
||||
case 'PAUSED':
|
||||
this.resumeScheduler();
|
||||
break;
|
||||
default:
|
||||
this.startScheduler();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './simple-trigger-config.component';
|
||||
export {SimpleTriggerCommand} from '../../model/simple-trigger.command';
|
||||
@@ -0,0 +1,257 @@
|
||||
<mat-card class="trigger-config-card">
|
||||
<mat-card-header style="padding-bottom: 16px">
|
||||
<mat-card-subtitle><b>TRIGGER DETAILS</b></mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
@if (shouldShowTheTriggerCardContent()) {
|
||||
<mat-card-content class="trigger-config-content">
|
||||
<div
|
||||
class="flex flex-column"
|
||||
style="
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
padding: 1em;
|
||||
">
|
||||
@if (jobs?.length === 0) {
|
||||
<mat-card id="noEligibleJobsAlert" style="background-color: #ff6385">
|
||||
<mat-card-content>
|
||||
<i class="fas fa-exclamation-circle" style="color: #fff"></i
|
||||
> <strong>WARNING</strong> Not found any eligible job classes for
|
||||
quartz-manager! <br />
|
||||
<p style="font-size: 0.8em">
|
||||
Please, make sure you have extended
|
||||
<i>AbstractQuartzManagerJob</i> and set the app prop
|
||||
<i>quartz-manager.jobClassPackages</i> with the correct java package
|
||||
</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
<form
|
||||
name="triggerConfigForm"
|
||||
class="trigger-config-form"
|
||||
class="flex-1"
|
||||
[formGroup]="simpleTriggerReactiveForm"
|
||||
(ngSubmit)="onSubmitTriggerConfig()">
|
||||
<div>
|
||||
<mat-form-field class="full-size-input">
|
||||
<mat-label>Trigger Name</mat-label>
|
||||
<input
|
||||
id="triggerName"
|
||||
matInput
|
||||
placeholder="name of the trigger (unique)"
|
||||
formControlName="triggerName"
|
||||
name="triggerName" />
|
||||
@if
|
||||
(simpleTriggerReactiveForm.controls.triggerName.errors?.required) {
|
||||
<mat-error> Name is <strong>required</strong> </mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field class="full-size-input">
|
||||
<mat-label>Job Class</mat-label>
|
||||
<mat-select
|
||||
id="jobClass"
|
||||
name="jobClass"
|
||||
formControlName="jobClass">
|
||||
@for (job of jobs; track job) {
|
||||
<mat-option [value]="job" class="font-13">
|
||||
{{ job }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (simpleTriggerReactiveForm.controls.jobClass.errors?.required) {
|
||||
<mat-error> Job is <strong>required</strong> </mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field class="full-size-input">
|
||||
<mat-label>Misfire Instruction</mat-label>
|
||||
<mat-select
|
||||
id="misfireInstruction"
|
||||
name="misfireInstruction"
|
||||
formControlName="misfireInstruction">
|
||||
<mat-option class="font-13" value="MISFIRE_INSTRUCTION_FIRE_NOW"
|
||||
>FIRE NOW</mat-option
|
||||
>
|
||||
<mat-option
|
||||
class="font-13"
|
||||
value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT"
|
||||
>RESCHEDULE NOW WITH EXISTING REPEAT COUNT
|
||||
</mat-option>
|
||||
<mat-option
|
||||
class="font-13"
|
||||
value="MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT"
|
||||
>RESCHEDULE NOW WITH REMAINING REPEAT COUNT
|
||||
</mat-option>
|
||||
<mat-option
|
||||
class="font-13"
|
||||
value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT"
|
||||
>RESCHEDULE NEXT WITH REMAINING COUNT
|
||||
</mat-option>
|
||||
<mat-option
|
||||
class="font-13"
|
||||
value="MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT"
|
||||
>RESCHEDULE NEXT WITH EXISTING COUNT
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
@if
|
||||
(simpleTriggerReactiveForm.controls.misfireInstruction.errors?.required)
|
||||
{
|
||||
<mat-error>
|
||||
The misfire instruction is <strong>required</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="small" [innerHTML]="getMisfireInstructionCaption()"></div>
|
||||
</div>
|
||||
<br />
|
||||
<div formGroupName="triggerPeriod">
|
||||
<div>
|
||||
<mat-form-field class="full-size-input">
|
||||
<mat-label>Start Date (optional)</mat-label>
|
||||
<input
|
||||
id="startDate"
|
||||
matInput
|
||||
[owlDateTime]="startDatePicker"
|
||||
[owlDateTimeTrigger]="startDatePicker"
|
||||
placeholder="Choose a start date"
|
||||
formControlName="startDate"
|
||||
name="startDate" />
|
||||
<button
|
||||
type="button"
|
||||
class="datetime-picker-trigger"
|
||||
mat-icon-button
|
||||
matSuffix
|
||||
[owlDateTimeTrigger]="startDatePicker">
|
||||
<mat-icon>event</mat-icon>
|
||||
</button>
|
||||
<owl-date-time
|
||||
#startDatePicker
|
||||
[showSecondsTimer]="true">
|
||||
</owl-date-time>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field class="full-size-input">
|
||||
<mat-label>End Date (optional)</mat-label>
|
||||
<input
|
||||
id="endDate"
|
||||
matInput
|
||||
[owlDateTime]="endDatePicker"
|
||||
[owlDateTimeTrigger]="endDatePicker"
|
||||
placeholder="Choose a end date"
|
||||
formControlName="endDate"
|
||||
name="endDate" />
|
||||
<button
|
||||
type="button"
|
||||
class="datetime-picker-trigger"
|
||||
mat-icon-button
|
||||
matSuffix
|
||||
[owlDateTimeTrigger]="endDatePicker">
|
||||
<mat-icon>event</mat-icon>
|
||||
</button>
|
||||
<owl-date-time
|
||||
#endDatePicker
|
||||
[showSecondsTimer]="true">
|
||||
</owl-date-time>
|
||||
</mat-form-field>
|
||||
@if
|
||||
(simpleTriggerReactiveForm.controls.triggerPeriod.errors?.invalidTriggerPeriod)
|
||||
{
|
||||
<mat-error style="font-size: small">
|
||||
the end date cannot be <strong>before</strong> the start date
|
||||
</mat-error>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div formGroupName="triggerRecurrence">
|
||||
<div>
|
||||
<mat-form-field class="full-size-input">
|
||||
<mat-label>Repeat Interval [in mills]</mat-label>
|
||||
<input
|
||||
id="repeatInterval"
|
||||
matInput
|
||||
placeholder="Repeat Interval [in mills]"
|
||||
type="number"
|
||||
formControlName="repeatInterval"
|
||||
name="repeatInterval" />
|
||||
@if
|
||||
(simpleTriggerReactiveForm.controls.triggerRecurrence.errors?.invalidTriggerRecurrence)
|
||||
{
|
||||
<mat-error>
|
||||
repeatCount and repeatInterval must be <strong>both</strong> set
|
||||
or unset
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field class="full-size-input">
|
||||
<mat-label>Repeat Count</mat-label>
|
||||
<input
|
||||
id="repeatCount"
|
||||
matInput
|
||||
placeholder="Repeat Count (-1 REPEAT INDEFINITELY)"
|
||||
type="number"
|
||||
formControlName="repeatCount"
|
||||
name="repeatCount" />
|
||||
@if
|
||||
(simpleTriggerReactiveForm.controls.triggerRecurrence.errors?.invalidTriggerRecurrence)
|
||||
{
|
||||
<mat-error>
|
||||
repeatCount and repeatInterval must be <strong>both</strong> set
|
||||
or unset
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div
|
||||
class="flex flex-row align-items-center justify-space-evenly"
|
||||
style="padding-bottom: 1em">
|
||||
@if (simpleTriggerReactiveForm.enabled) {
|
||||
<div class="flex-1" style="text-align: center">
|
||||
<button
|
||||
mat-raised-button
|
||||
type="button"
|
||||
(click)="onResetReactiveForm()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
} @if (simpleTriggerReactiveForm.enabled) {
|
||||
<div class="flex-1" style="text-align: center">
|
||||
<button
|
||||
mat-raised-button
|
||||
type="submit"
|
||||
color="primary"
|
||||
[disabled]="simpleTriggerReactiveForm.invalid">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
} @if (!simpleTriggerReactiveForm.enabled) {
|
||||
<div class="flex-1" style="text-align: center">
|
||||
<button
|
||||
mat-raised-button
|
||||
type="button"
|
||||
(click)="
|
||||
openTriggerForm();
|
||||
simpleTriggerReactiveForm.controls['triggerName'].disable()
|
||||
">
|
||||
Reschedule
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
}
|
||||
</mat-card>
|
||||
@@ -0,0 +1,65 @@
|
||||
:host {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.trigger-config-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.trigger-config-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.small{
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.full-size-input{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host ::ng-deep .trigger-config-form .mat-mdc-form-field {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:host ::ng-deep .trigger-config-form .mat-mdc-select-value,
|
||||
:host ::ng-deep .trigger-config-form .mat-mdc-select-value-text,
|
||||
:host ::ng-deep .trigger-config-form .mat-mdc-input-element,
|
||||
:host ::ng-deep .trigger-config-form .mdc-floating-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:host ::ng-deep .trigger-config-form .mat-mdc-select-trigger {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
/* ===== Scrollbar CSS ===== */
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
scroll-margin-right: 0;
|
||||
scrollbar-color: #b8b8b8 #ffffff;
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #b8b8b8;
|
||||
border-radius: 10px;
|
||||
border: 3px solid #ffffff;
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import {ComponentFixture, fakeAsync, flush, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {SimpleTriggerConfigComponent} from './simple-trigger-config.component';
|
||||
import {ApiService, ConfigService, CONTEXT_PATH, SchedulerService} from '../../services';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||
import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
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 {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {Trigger} from '../../model/trigger.model';
|
||||
import {JobDetail} from '../../model/jobDetail.model';
|
||||
import {SimpleTrigger} from '../../model/simple-trigger.model';
|
||||
import JobService from '../../services/job.service';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
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(waitForAsync( () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [FormsModule, MatFormFieldModule, MatFormFieldModule, MatSelectModule, MatInputModule, NoopAnimationsModule,
|
||||
MatNativeDateModule, ReactiveFormsModule,
|
||||
MatCardModule, MatIconModule, HttpClientTestingModule, RouterTestingModule],
|
||||
declarations: [SimpleTriggerConfigComponent],
|
||||
providers: [SchedulerService, ApiService, ConfigService, JobService, FormBuilder],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
httpTestingController = TestBed.inject(HttpTestingController);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SimpleTriggerConfigComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should fetch no triggers at the init', () => {
|
||||
expect(component).toBeTruthy();
|
||||
httpTestingController.expectNone(`${CONTEXT_PATH}/simple-triggers/DEFAULT/my-simple-trigger`);
|
||||
});
|
||||
|
||||
function setInputValue(componentDe: DebugElement, inputSelector: string, value: string) {
|
||||
const inputDe = componentDe.query(By.css(inputSelector));
|
||||
const inputEl = inputDe.nativeElement;
|
||||
inputEl.value = value;
|
||||
inputEl.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
function setDropdownValue(componentDe: DebugElement, dropdownSelector: string, value: string) {
|
||||
const dropdownDe = componentDe.query(By.css(dropdownSelector));
|
||||
const dropdownEl = dropdownDe.nativeElement;
|
||||
dropdownEl.value = value;
|
||||
dropdownEl.dispatchEvent(new Event('change'));
|
||||
fixture.detectChanges();
|
||||
}
|
||||
function setDropdownValueByIndex(componentDe: DebugElement, dropdownSelector: string, index: number) {
|
||||
const dropdownDe = componentDe.query(By.css(dropdownSelector));
|
||||
const dropdownEl = dropdownDe.nativeElement;
|
||||
dropdownEl.value = dropdownEl.options[index].value;
|
||||
dropdownEl.dispatchEvent(new Event('change'));
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
async function setMatSelectValueByIndex(componentDe: DebugElement, dropdownSelector: string, index: number) {
|
||||
const dropdownDe = componentDe.query(By.css(dropdownSelector));
|
||||
dropdownDe.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
const matOptionDe = componentDe.query(By.css('.mat-mdc-select-panel')).queryAll(By.css('.mat-mdc-option'));
|
||||
matOptionDe[index].nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
function openFormAndFillAllMandatoryFields() {
|
||||
component.openTriggerForm();
|
||||
fixture.detectChanges();
|
||||
|
||||
const getJobsReq = httpTestingController.expectOne(`${CONTEXT_PATH}/job-classes`);
|
||||
getJobsReq.flush([testJobName]);
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
|
||||
const submitButton = componentDe.query(By.css(submitButtonSelector));
|
||||
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
|
||||
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
|
||||
|
||||
setInputValue(componentDe, '#triggerName', testTriggerName);
|
||||
expect(component.simpleTriggerReactiveForm.controls.triggerName.value).toEqual(testTriggerName);
|
||||
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
|
||||
setMatSelectValueByIndex(componentDe, '#misfireInstruction', 0);
|
||||
expect(component.simpleTriggerReactiveForm.controls.misfireInstruction.value).toEqual('MISFIRE_INSTRUCTION_FIRE_NOW');
|
||||
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
|
||||
setMatSelectValueByIndex(componentDe, '#jobClass', 0);
|
||||
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual(null);
|
||||
|
||||
setInputValue(componentDe, '#repeatCount', '1000');
|
||||
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
|
||||
|
||||
setInputValue(componentDe, repeatIntervalSelector, '2000');
|
||||
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual(null);
|
||||
}
|
||||
|
||||
it('should enabled the submit only when the form is valid', () => {
|
||||
openFormAndFillAllMandatoryFields();
|
||||
});
|
||||
|
||||
it('should emit an event when a new trigger is submitted', fakeAsync(() => {
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
const mockTrigger = new Trigger();
|
||||
mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null);
|
||||
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: testJobName, description: null};
|
||||
mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
|
||||
|
||||
openFormAndFillAllMandatoryFields();
|
||||
|
||||
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(submitButtonSelector));
|
||||
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
|
||||
|
||||
let actualNewTrigger;
|
||||
component.onNewTrigger.subscribe(simpleTrigger => actualNewTrigger = simpleTrigger);
|
||||
let submittedTriggerKey: TriggerKey;
|
||||
component.onTriggerSubmitting.subscribe(triggerKey => submittedTriggerKey = triggerKey);
|
||||
|
||||
submitButton.nativeElement.click();
|
||||
expect(submittedTriggerKey).toEqual(new TriggerKey(testTriggerName, null));
|
||||
flush();
|
||||
|
||||
const postSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/DEFAULT/${testTriggerName}`);
|
||||
postSimpleTriggerReq.flush(mockTrigger);
|
||||
|
||||
expect(actualNewTrigger).toEqual(mockTrigger);
|
||||
}));
|
||||
|
||||
it('should not emit an event when an existing trigger is edited', () => {
|
||||
const mockTriggerKey = new TriggerKey(testTriggerName, null);
|
||||
component.triggerKey = mockTriggerKey;
|
||||
fixture.detectChanges();
|
||||
|
||||
const mockTrigger = new SimpleTrigger();
|
||||
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/DEFAULT/${testTriggerName}`);
|
||||
getSimpleTriggerReq.flush(mockTrigger);
|
||||
|
||||
component.simpleTriggerReactiveForm.setValue({
|
||||
triggerName: testTriggerName,
|
||||
jobClass: testJobName,
|
||||
triggerRecurrence: {
|
||||
repeatInterval: 2000,
|
||||
repeatCount: 100,
|
||||
},
|
||||
triggerPeriod: {
|
||||
startDate: null,
|
||||
endDate: null
|
||||
},
|
||||
misfireInstruction: MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW.toString()
|
||||
});
|
||||
|
||||
component.openTriggerForm();
|
||||
fixture.detectChanges();
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
setInputValue(componentDe, repeatIntervalSelector, '4000');
|
||||
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatInterval).toEqual(4000);
|
||||
|
||||
const submitButton = componentDe.query(By.css(submitButtonSelector));
|
||||
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
|
||||
|
||||
let actualNewTrigger;
|
||||
component.onNewTrigger.subscribe(simpleTrigger => actualNewTrigger = simpleTrigger);
|
||||
|
||||
submitButton.nativeElement.click();
|
||||
|
||||
const putSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/DEFAULT/${testTriggerName}`);
|
||||
putSimpleTriggerReq.flush(mockTrigger);
|
||||
|
||||
expect(actualNewTrigger).toBeUndefined();
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
const mockTrigger = new Trigger();
|
||||
mockTrigger.triggerKeyDTO = mockTriggerKey;
|
||||
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: 'TestJob', description: null};
|
||||
const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/DEFAULT/my-simple-trigger`);
|
||||
getSimpleTriggerReq.flush(mockTrigger);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
const submitButton = componentDe.query(By.css('form button:not(.datetime-picker-trigger)'));
|
||||
expect(submitButton.nativeElement.textContent.trim()).toEqual('Reschedule');
|
||||
});
|
||||
|
||||
it('should display the form if the openTriggerForm method is called', () => {
|
||||
component.openTriggerForm();
|
||||
fixture.detectChanges();
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
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/DEFAULT/${testTriggerName}`);
|
||||
getSimpleTriggerReq.flush(mockTrigger);
|
||||
|
||||
expect(component.simpleTriggerReactiveForm.value.triggerName).toEqual(testTriggerName);
|
||||
|
||||
component.openNewTriggerForm();
|
||||
|
||||
expect(component.simpleTriggerReactiveForm.value.triggerName).toBeNull();
|
||||
expect(component.simpleTriggerReactiveForm.value.jobClass).toBeNull();
|
||||
expect(component.shouldShowTheTriggerCardContent()).toBeTruthy();
|
||||
|
||||
});
|
||||
|
||||
it('should not emit form open changes while applying a null trigger input', () => {
|
||||
let formOpenChangeEmitted = false;
|
||||
component.triggerFormOpenChange.subscribe(() => formOpenChangeEmitted = true);
|
||||
|
||||
component.triggerKey = null;
|
||||
|
||||
expect(formOpenChangeEmitted).toBeFalsy();
|
||||
expect(component.shouldShowTheTriggerCardContent()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should display the warning if there are no eligible jobs', () => {
|
||||
fixture.detectChanges();
|
||||
const getJobsReq = httpTestingController.expectOne(`${CONTEXT_PATH}/job-classes`);
|
||||
getJobsReq.flush([]);
|
||||
fixture.detectChanges();
|
||||
|
||||
component.openTriggerForm();
|
||||
fixture.detectChanges();
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
const warningCard = componentDe.query(By.css('#noEligibleJobsAlert'));
|
||||
expect(warningCard).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not display the warning if there are eligible jobs', () => {
|
||||
fixture.detectChanges();
|
||||
const getJobsReq = httpTestingController.expectOne(`${CONTEXT_PATH}/job-classes`);
|
||||
getJobsReq.flush(['sampleJob']);
|
||||
fixture.detectChanges();
|
||||
|
||||
component.openTriggerForm();
|
||||
fixture.detectChanges();
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
const warningCard = componentDe.query(By.css('#noEligibleJobsAlert'));
|
||||
expect(warningCard).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
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 {TriggerKey} from '../../model/triggerKey.model';
|
||||
import JobService from '../../services/job.service';
|
||||
import {MisfireInstruction, MisfireInstructionCaption} from '../../model/misfire-instruction.model';
|
||||
import {AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, Validators} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'qrzmng-simple-trigger-config',
|
||||
templateUrl: './simple-trigger-config.component.html',
|
||||
styleUrls: ['./simple-trigger-config.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class SimpleTriggerConfigComponent implements OnInit {
|
||||
|
||||
trigger: SimpleTrigger = null;
|
||||
|
||||
simpleTriggerReactiveForm: UntypedFormGroup = this.formBuilder.group({
|
||||
triggerName: [this.trigger?.triggerKeyDTO.name, Validators.required],
|
||||
jobClass: [this.trigger?.jobDetailDTO.jobClassName, Validators.required],
|
||||
triggerPeriod: this.formBuilder.group({
|
||||
startDate: [this.trigger?.startTime && new Date(this.trigger.startTime)],
|
||||
endDate: [this.trigger?.endTime && new Date(this.trigger.endTime)]
|
||||
}, {validators: this._triggerPeriodValidator}),
|
||||
triggerRecurrence: this.formBuilder.group({
|
||||
repeatCount: [this.trigger?.repeatCount],
|
||||
repeatInterval: [this.trigger?.repeatInterval]
|
||||
}, {validators: this._triggerRepetitionValidator}),
|
||||
misfireInstruction: [MisfireInstruction[this.trigger?.misfireInstruction], Validators.required]
|
||||
});
|
||||
|
||||
scheduler: Scheduler;
|
||||
|
||||
triggerLoading = false;
|
||||
|
||||
private triggerInProgress = false;
|
||||
|
||||
private selectedTriggerKey: TriggerKey;
|
||||
|
||||
private jobs: Array<String>;
|
||||
|
||||
@Output()
|
||||
onNewTrigger = new EventEmitter<SimpleTrigger>();
|
||||
|
||||
@Output()
|
||||
triggerFormOpenChange = new EventEmitter<boolean>();
|
||||
|
||||
@Output()
|
||||
onTriggerSubmitting = new EventEmitter<TriggerKey>();
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private schedulerService: SchedulerService,
|
||||
private jobService: JobService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.simpleTriggerReactiveForm.disable();
|
||||
this.fetchJobs();
|
||||
}
|
||||
|
||||
private fetchJobs() {
|
||||
this.jobService.fetchJobs().subscribe(jobs => this.jobs = jobs);
|
||||
}
|
||||
|
||||
openTriggerForm() {
|
||||
this.simpleTriggerReactiveForm.enable();
|
||||
this.triggerFormOpenChange.emit(true);
|
||||
}
|
||||
|
||||
private closeTriggerForm() {
|
||||
this.simpleTriggerReactiveForm.disable();
|
||||
this.triggerFormOpenChange.emit(false);
|
||||
}
|
||||
|
||||
@Input()
|
||||
set triggerKey(triggerKey: TriggerKey) {
|
||||
if (!triggerKey) {
|
||||
return;
|
||||
} else if (!this.selectedTriggerKey || this.selectedTriggerKey.name !== triggerKey.name) {
|
||||
this._resetTheTrigger();
|
||||
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
||||
this.fetchSelectedTrigger();
|
||||
this.simpleTriggerReactiveForm.disable();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
this.schedulerService.getSimpleTriggerConfig(this.selectedTriggerKey.name)
|
||||
.subscribe((retTrigger: SimpleTrigger) => {
|
||||
this.trigger = retTrigger;
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(retTrigger))
|
||||
this.triggerLoading = false;
|
||||
this.triggerInProgress = this.trigger.mayFireAgain;
|
||||
this.simpleTriggerReactiveForm.disable();
|
||||
})
|
||||
}
|
||||
|
||||
shouldShowTheTriggerCardContent = (): boolean => this.trigger !== null || this.simpleTriggerReactiveForm.enabled;
|
||||
|
||||
existsATriggerInProgress = (): boolean => this.trigger && this.triggerInProgress;
|
||||
|
||||
onResetReactiveForm = () => {
|
||||
if (this.trigger) {
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
|
||||
} else {
|
||||
this.simpleTriggerReactiveForm.reset(new SimpleTriggerReactiveForm());
|
||||
}
|
||||
this.closeTriggerForm();
|
||||
};
|
||||
|
||||
onSubmitTriggerConfig = () => {
|
||||
const schedulerServiceCall = this.existsATriggerInProgress() ?
|
||||
this.schedulerService.updateSimpleTriggerConfig : this.schedulerService.saveSimpleTriggerConfig;
|
||||
|
||||
const simpleTriggerCommand = this._fromReactiveFormToCommand();
|
||||
if (!this.trigger) {
|
||||
this.onTriggerSubmitting.emit(new TriggerKey(simpleTriggerCommand.triggerName, null));
|
||||
setTimeout(() => this.submitTriggerConfig(schedulerServiceCall, simpleTriggerCommand));
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitTriggerConfig(schedulerServiceCall, simpleTriggerCommand);
|
||||
|
||||
}
|
||||
|
||||
private submitTriggerConfig(schedulerServiceCall, simpleTriggerCommand: SimpleTriggerCommand) {
|
||||
this.triggerLoading = true;
|
||||
schedulerServiceCall(simpleTriggerCommand)
|
||||
.subscribe((retTrigger: SimpleTrigger) => {
|
||||
this.trigger = retTrigger;
|
||||
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(retTrigger));
|
||||
|
||||
this.triggerInProgress = this.trigger.mayFireAgain;
|
||||
|
||||
if (schedulerServiceCall === this.schedulerService.saveSimpleTriggerConfig) {
|
||||
this.onNewTrigger.emit(retTrigger);
|
||||
}
|
||||
|
||||
this.closeTriggerForm();
|
||||
}, error => {
|
||||
if (this.trigger) {
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
|
||||
}
|
||||
this.triggerLoading = false;
|
||||
}, () => {
|
||||
this.triggerLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
private _triggerPeriodValidator(control: AbstractControl): ValidationErrors | null {
|
||||
const startDate = control.get('startDate');
|
||||
const endDate = control.get('endDate');
|
||||
if (startDate.value && endDate.value) {
|
||||
return endDate.value < startDate.value ?
|
||||
<ValidationErrors>{invalidTriggerPeriod: true} : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _triggerRepetitionValidator(control: AbstractControl): ValidationErrors | null {
|
||||
const repeatInterval = control.get('repeatInterval');
|
||||
const repeatCount = control.get('repeatCount');
|
||||
if ((repeatCount.value && repeatInterval.value) || (!repeatCount.value && !repeatInterval.value)) {
|
||||
repeatInterval.setErrors(null);
|
||||
repeatCount.setErrors(null);
|
||||
return null;
|
||||
}
|
||||
const errors = <ValidationErrors>{invalidTriggerRecurrence: true};
|
||||
repeatInterval.setErrors(errors);
|
||||
repeatCount.setErrors(errors);
|
||||
return errors;
|
||||
}
|
||||
|
||||
private _fromTriggerToReactiveForm = (simpleTrigger: SimpleTrigger): SimpleTriggerReactiveForm => {
|
||||
const simpleTriggerReactiveForm = new SimpleTriggerReactiveForm();
|
||||
simpleTriggerReactiveForm.triggerName = simpleTrigger.triggerKeyDTO.name;
|
||||
simpleTriggerReactiveForm.jobClass = simpleTrigger.jobDetailDTO.jobClassName;
|
||||
simpleTriggerReactiveForm.triggerRecurrence.repeatCount = simpleTrigger.repeatCount || null;
|
||||
simpleTriggerReactiveForm.triggerRecurrence.repeatInterval = simpleTrigger.repeatInterval || null;
|
||||
simpleTriggerReactiveForm.triggerPeriod.startDate = (simpleTrigger.startTime && new Date(simpleTrigger.startTime)) || null;
|
||||
simpleTriggerReactiveForm.triggerPeriod.endDate = (simpleTrigger.endTime && new Date(simpleTrigger.endTime)) || null;
|
||||
simpleTriggerReactiveForm.misfireInstruction = (simpleTrigger.misfireInstruction
|
||||
&& MisfireInstruction[simpleTrigger.misfireInstruction]) || null;
|
||||
return simpleTriggerReactiveForm;
|
||||
};
|
||||
|
||||
private _fromReactiveFormToCommand = (): SimpleTriggerCommand => {
|
||||
const reactiveFormValue = this.simpleTriggerReactiveForm.getRawValue();
|
||||
const simpleTriggerCommand = new SimpleTriggerCommand();
|
||||
simpleTriggerCommand.triggerName = reactiveFormValue.triggerName;
|
||||
simpleTriggerCommand.triggerGroup = this.selectedTriggerKey?.group || 'DEFAULT';
|
||||
simpleTriggerCommand.jobClass = reactiveFormValue.jobClass;
|
||||
simpleTriggerCommand.repeatCount = reactiveFormValue.triggerRecurrence.repeatCount;
|
||||
simpleTriggerCommand.repeatInterval = reactiveFormValue.triggerRecurrence.repeatInterval;
|
||||
simpleTriggerCommand.startDate = reactiveFormValue.triggerPeriod.startDate;
|
||||
simpleTriggerCommand.endDate = reactiveFormValue.triggerPeriod.endDate;
|
||||
simpleTriggerCommand.misfireInstruction = reactiveFormValue.misfireInstruction;
|
||||
return simpleTriggerCommand;
|
||||
}
|
||||
|
||||
getMisfireInstructionCaption(): string {
|
||||
const misfireInstructionKey = this.simpleTriggerReactiveForm.controls
|
||||
.misfireInstruction.value as unknown as keyof typeof MisfireInstruction;
|
||||
return MisfireInstructionCaption.get(MisfireInstruction[misfireInstructionKey]);
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTriggerReactiveForm {
|
||||
triggerName: string;
|
||||
jobClass: string;
|
||||
triggerPeriod: {
|
||||
startDate?;
|
||||
endDate?;
|
||||
} = {};
|
||||
triggerRecurrence: {
|
||||
repeatCount?: number;
|
||||
repeatInterval?: number;
|
||||
} = {};
|
||||
misfireInstruction: string;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './trigger-list.component'
|
||||
@@ -0,0 +1,42 @@
|
||||
<mat-card class="trigger-list-card" style="padding-left: 0; padding-right: 0">
|
||||
<mat-card-header
|
||||
class="flex flex-row justify-space-between"
|
||||
style="padding-right: 1em">
|
||||
<mat-card-subtitle><b>TRIGGERS</b></mat-card-subtitle>
|
||||
@if (!triggerFormIsOpen) {
|
||||
<button
|
||||
mat-raised-button
|
||||
style="top: -0.5em"
|
||||
color="primary"
|
||||
(click)="onNewTriggerBtnClicked()">
|
||||
new
|
||||
</button>
|
||||
}
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content class="trigger-list-content">
|
||||
<mat-nav-list
|
||||
style="
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
height: calc(100% - 3em);
|
||||
">
|
||||
@for (triggerKey of getTriggerKeyList(); track triggerKey) {
|
||||
<mat-list-item
|
||||
class="triggerItemList"
|
||||
[ngClass]="{
|
||||
selectedTrigger:
|
||||
selectedTrigger && selectedTrigger.name == triggerKey.name
|
||||
}"
|
||||
(click)="selectTrigger(triggerKey)">
|
||||
<a matLine>{{ triggerKey.name }}</a>
|
||||
</mat-list-item>
|
||||
}
|
||||
</mat-nav-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,45 @@
|
||||
:host {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.trigger-list-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.trigger-list-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ===== Scrollbar CSS ===== */
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: #b8b8b8 #ffffff;
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #b8b8b8;
|
||||
border-radius: 10px;
|
||||
border: 3px solid #ffffff;
|
||||
}
|
||||
|
||||
.selectedTrigger{
|
||||
background-color: #dddddd;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {DebugElement} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatDividerModule} from '@angular/material/divider';
|
||||
import {TriggerListComponent} from './trigger-list.component';
|
||||
import {MatListModule} from '@angular/material/list';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {MatDialogModule} from '@angular/material/dialog';
|
||||
|
||||
describe('TriggerListComponent', () => {
|
||||
|
||||
let component: TriggerListComponent;
|
||||
let fixture: ComponentFixture<TriggerListComponent>;
|
||||
|
||||
let httpClient: HttpClient;
|
||||
let httpTestingController: HttpTestingController;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatCardModule, MatDialogModule, MatDividerModule,
|
||||
MatIconModule, MatListModule, HttpClientTestingModule, RouterTestingModule],
|
||||
declarations: [TriggerListComponent],
|
||||
providers: [TriggerService, ApiService, ConfigService]
|
||||
}).compileComponents();
|
||||
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
httpTestingController = TestBed.inject(HttpTestingController);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TriggerListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should select the first trigger of the list', () => {
|
||||
expect(component).toBeDefined();
|
||||
|
||||
let actualSelectedTrigger: TriggerKey;
|
||||
component.onSelectedTrigger.subscribe(selectedTrigger => actualSelectedTrigger = selectedTrigger);
|
||||
|
||||
const getTriggerListReq = httpTestingController.expectOne(`${CONTEXT_PATH}/triggers`);
|
||||
const mockExistingTriggers = new Array<TriggerKey>();
|
||||
const firstTriggerKey = new TriggerKey('trigger1', 'group1');
|
||||
mockExistingTriggers.push(firstTriggerKey);
|
||||
const secondTriggerKey = new TriggerKey('trigger2', 'group2');
|
||||
mockExistingTriggers.push(secondTriggerKey);
|
||||
getTriggerListReq.flush(mockExistingTriggers);
|
||||
fixture.detectChanges();
|
||||
|
||||
const triggerListComponentDe: DebugElement = fixture.debugElement;
|
||||
const triggerItemList = triggerListComponentDe.queryAll(By.css('.triggerItemList'));
|
||||
expect(triggerItemList.length).toEqual(2);
|
||||
|
||||
expect(actualSelectedTrigger).toEqual(firstTriggerKey);
|
||||
|
||||
});
|
||||
|
||||
it('should open the trigger form if the trigger list is empty', () => {
|
||||
expect(component).toBeDefined();
|
||||
|
||||
let actualSelectedTrigger: TriggerKey;
|
||||
component.onSelectedTrigger.subscribe(selectedTrigger => actualSelectedTrigger = selectedTrigger);
|
||||
|
||||
let expectedOpenedNewTriggerFormEvent: boolean;
|
||||
component.onNewTriggerClicked.subscribe(() => expectedOpenedNewTriggerFormEvent = true);
|
||||
|
||||
const getTriggerListReq = httpTestingController.expectOne(`${CONTEXT_PATH}/triggers`);
|
||||
getTriggerListReq.flush(new Array<TriggerKey>());
|
||||
fixture.detectChanges();
|
||||
|
||||
const triggerListComponentDe: DebugElement = fixture.debugElement;
|
||||
const triggerItemList = triggerListComponentDe.queryAll(By.css('.triggerItemList'));
|
||||
expect(triggerItemList.length).toEqual(0);
|
||||
|
||||
expect(expectedOpenedNewTriggerFormEvent).toBeTruthy();
|
||||
expect(actualSelectedTrigger).toBeUndefined();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
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, MatDialogRef} from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div style="padding:16px">
|
||||
<h3 mat-dialog-title>Coming Soon</h3>
|
||||
<div mat-dialog-content>
|
||||
<p>This feature is in roadmap and it will come with the next releases</p>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="closeDialog()" style="padding: 0.5em;width: 5em;">Ok</button>
|
||||
</div>
|
||||
</div>`,
|
||||
standalone: false
|
||||
})
|
||||
// tslint:disable-next-line:component-class-suffix
|
||||
export class UnsupportedMultipleJobsDialog {
|
||||
constructor(public dialogRef: MatDialogRef<UnsupportedMultipleJobsDialog>) {
|
||||
}
|
||||
closeDialog(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'qrzmng-trigger-list',
|
||||
templateUrl: './trigger-list.component.html',
|
||||
styleUrls: ['./trigger-list.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class TriggerListComponent implements OnInit {
|
||||
|
||||
newTriggers: Array<SimpleTrigger> = new Array<SimpleTrigger>();
|
||||
|
||||
loading = true;
|
||||
|
||||
triggerKeys: Array<TriggerKey> = new Array<TriggerKey>();
|
||||
|
||||
@Output() onNewTriggerClicked = new EventEmitter<void>();
|
||||
triggerFormIsOpen = false;
|
||||
|
||||
selectedTrigger: TriggerKey;
|
||||
@Output() onSelectedTrigger = new EventEmitter<TriggerKey>();
|
||||
|
||||
constructor(
|
||||
private triggerService: TriggerService,
|
||||
public dialog: MatDialog
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.loading = true;
|
||||
this.fetchTriggers();
|
||||
}
|
||||
|
||||
@Input()
|
||||
set openedNewTriggerForm(triggerFormIsOpen: boolean) {
|
||||
this.triggerFormIsOpen = triggerFormIsOpen;
|
||||
}
|
||||
|
||||
getTriggerKeyList = () => {
|
||||
const newTriggerKeys = this.newTriggers.map(simpleTrigger => simpleTrigger.triggerKeyDTO);
|
||||
return newTriggerKeys.concat(this.triggerKeys);
|
||||
}
|
||||
|
||||
private fetchTriggers() {
|
||||
this.triggerService.fetchTriggers()
|
||||
.subscribe((triggerKeys: Array<TriggerKey>) => {
|
||||
this.triggerKeys = triggerKeys;
|
||||
if (!triggerKeys || triggerKeys.length === 0) {
|
||||
this.onNewTriggerBtnClicked();
|
||||
} else {
|
||||
this.selectTrigger(this.triggerKeys[0]);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
selectTrigger(triggerKey: TriggerKey) {
|
||||
this.selectedTrigger = triggerKey;
|
||||
this.onSelectedTrigger.emit(triggerKey);
|
||||
}
|
||||
|
||||
onNewTriggerBtnClicked() {
|
||||
this.onNewTriggerClicked.emit();
|
||||
// if (this.getTriggerKeyList() && this.getTriggerKeyList().length > 0) {
|
||||
// this.dialog.open(UnsupportedMultipleJobsDialog)
|
||||
// } else {
|
||||
// this.onNewTriggerClicked.emit();
|
||||
// }
|
||||
}
|
||||
|
||||
onNewTrigger(newTrigger: SimpleTrigger) {
|
||||
this.newTriggers = [newTrigger, ...this.newTriggers];
|
||||
this.selectTrigger(newTrigger.triggerKeyDTO);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { TestBed, async, inject } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { NO_AUTH, UserService } from '../services';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import {jest} from '@jest/globals'
|
||||
|
||||
export class RouterStub {
|
||||
navigate(commands?: any[], extras?: any) {}
|
||||
}
|
||||
|
||||
const RouterSpy = jest.spyOn(RouterStub.prototype, 'navigate');
|
||||
|
||||
const MockUserServiceNoAuth = jest.fn(() => ({currentUser: NO_AUTH}));
|
||||
const MockUserService = jest.fn(() => ({
|
||||
currentUser: {
|
||||
authorities: ['ROLE_ADMIN']
|
||||
}
|
||||
}));
|
||||
const MockUserServiceForbidden = jest.fn(() => ({
|
||||
currentUser: {
|
||||
authorities: ['ROLE_GUEST']
|
||||
}
|
||||
}));
|
||||
|
||||
describe('AdminGuard NoAuth', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AdminGuard,
|
||||
{
|
||||
provide: Router,
|
||||
useClass: RouterStub
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useClass: MockUserServiceNoAuth
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should run', inject([AdminGuard], (guard: AdminGuard) => {
|
||||
expect(guard).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('returns true if user is NO_AUTH',inject([AdminGuard], (guard: AdminGuard) => {
|
||||
expect(guard.canActivate(null, null)).toBeTruthy();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('AdminGuard activates the route', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AdminGuard,
|
||||
{
|
||||
provide: Router,
|
||||
useClass: RouterStub
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useClass: MockUserService
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should run', inject([AdminGuard], (guard: AdminGuard) => {
|
||||
expect(guard).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('returns true if user has admin role',inject([AdminGuard], (guard: AdminGuard) => {
|
||||
expect(guard.canActivate(null, null)).toBeTruthy();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('AdminGuard redirects to 403', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AdminGuard,
|
||||
{
|
||||
provide: Router,
|
||||
useClass: RouterStub
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useClass: MockUserServiceForbidden
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should run', inject([AdminGuard], (guard: AdminGuard) => {
|
||||
expect(guard).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('returns false if user is not authorized',inject([AdminGuard], (guard: AdminGuard) => {
|
||||
expect(guard.canActivate(null, null)).toBeFalsy();
|
||||
expect(RouterSpy).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
|
||||
});
|
||||
@@ -1,25 +1,29 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { UserService } from '../services';
|
||||
import { Observable } from 'rxjs';
|
||||
import {Injectable} from '@angular/core';
|
||||
import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import {UserService} from '../services';
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class AdminGuard implements CanActivate {
|
||||
constructor(private router: Router, private userService: UserService) {}
|
||||
export class AdminGuard {
|
||||
constructor(private router: Router, private userService: UserService) {
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
|
||||
if (this.userService.isAnAnonymousUser) {
|
||||
return true;
|
||||
}
|
||||
if (this.userService.currentUser) {
|
||||
if(this.userService.currentUser === 'NO_AUTH')
|
||||
return true;
|
||||
if (JSON.stringify(this.userService.currentUser.authorities).search('ROLE_ADMIN') !== -1)
|
||||
return true;
|
||||
else {
|
||||
this.router.navigate(['/403']);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// to be enable again in the scope of the card #65
|
||||
// if (JSON.stringify(this.userService.currentUser.authorities).search('ROLE_ADMIN') !== -1) {
|
||||
// return true;
|
||||
// } else {
|
||||
// this.router.navigate(['/403']);
|
||||
// return false;
|
||||
// }
|
||||
} else {
|
||||
console.log('NOT AN ADMIN ROLE');
|
||||
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});
|
||||
this.router.navigate(['/login'], {queryParams: {returnUrl: state.url}});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router, CanActivate } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import { UserService } from '../services';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class GuestGuard implements CanActivate {
|
||||
export class GuestGuard {
|
||||
|
||||
constructor(private router: Router, private userService: UserService) {}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router, CanActivate } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import { UserService } from '../services';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class LoginGuard implements CanActivate {
|
||||
export class LoginGuard {
|
||||
|
||||
constructor(private router: Router, private userService: UserService) {}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export class SocketEndpoint{
|
||||
client : any
|
||||
stomp : any
|
||||
}
|
||||
import SockJS from 'sockjs-client';
|
||||
import Stomp from 'stompjs';
|
||||
|
||||
export class SocketEndpoint {
|
||||
client: SockJS;
|
||||
stomp: Stomp;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
export class SocketOption{
|
||||
socketUrl : string;
|
||||
topicName : string;
|
||||
brokerName : string;
|
||||
reconnectionTimeout : number = 30000
|
||||
export class SocketOption {
|
||||
socketUrl: string;
|
||||
topicName: string;
|
||||
brokerName: string;
|
||||
reconnectionTimeout = 30000
|
||||
|
||||
getAccessToken: Function = () => null;
|
||||
getAccessToken: Function = () => null;
|
||||
|
||||
constructor(socketUrl : string, topicName : string, getAccessToken?: Function, brokerName : string = null, reconnectionTimeout : number = 30000){
|
||||
this.socketUrl = socketUrl;
|
||||
this.topicName = topicName;
|
||||
this.brokerName = brokerName;
|
||||
this.reconnectionTimeout = reconnectionTimeout;
|
||||
this.getAccessToken = getAccessToken || (() => null);
|
||||
}
|
||||
|
||||
}
|
||||
constructor(socketUrl: string,
|
||||
topicName: string,
|
||||
getAccessToken?: Function,
|
||||
brokerName: string = null,
|
||||
reconnectionTimeout: number = 30000) {
|
||||
this.socketUrl = socketUrl;
|
||||
this.topicName = topicName;
|
||||
this.brokerName = brokerName;
|
||||
this.reconnectionTimeout = reconnectionTimeout;
|
||||
this.getAccessToken = getAccessToken || (() => null);
|
||||
}
|
||||
}
|
||||
|
||||
24
quartz-manager-frontend/src/app/model/calendar.model.ts
Normal file
24
quartz-manager-frontend/src/app/model/calendar.model.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {TriggerKey} from './triggerKey.model';
|
||||
|
||||
export type CalendarType = 'ANNUAL' | 'CRON' | 'DAILY' | 'HOLIDAY' | 'MONTHLY' | 'WEEKLY';
|
||||
|
||||
export class QuartzCalendar {
|
||||
name: string;
|
||||
type: CalendarType = 'WEEKLY';
|
||||
description: string;
|
||||
cronExpression: string;
|
||||
timeZone: string;
|
||||
rangeStartingTime: string;
|
||||
rangeEndingTime: string;
|
||||
invertTimeRange: boolean;
|
||||
excludedDaysOfWeek: number[];
|
||||
excludedDaysOfMonth: number[];
|
||||
excludedDates: Date[];
|
||||
triggerKeys: TriggerKey[];
|
||||
}
|
||||
|
||||
export class CalendarIncludedTimeTest {
|
||||
time: Date;
|
||||
included: boolean;
|
||||
nextIncludedTime: Date;
|
||||
}
|
||||
4
quartz-manager-frontend/src/app/model/jobDetail.model.ts
Normal file
4
quartz-manager-frontend/src/app/model/jobDetail.model.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export class JobDetail {
|
||||
jobClassName: string;
|
||||
description: string;
|
||||
}
|
||||
4
quartz-manager-frontend/src/app/model/jobKey.model.ts
Normal file
4
quartz-manager-frontend/src/app/model/jobKey.model.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export class JobKeyModel {
|
||||
name: string;
|
||||
group: string;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
export enum MisfireInstruction {
|
||||
MISFIRE_INSTRUCTION_FIRE_NOW = 1,
|
||||
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT = 2,
|
||||
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT = 3 ,
|
||||
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT = 4,
|
||||
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT = 5
|
||||
}
|
||||
|
||||
export function getMisfireInstructionByIndex(index: number) {
|
||||
return Object.keys(MisfireInstruction)[index];
|
||||
}
|
||||
|
||||
// function enumFromStringValue<T> (enm: { [s: string]: T}, value: string): T | undefined {
|
||||
// return (Object.values(enm) as unknown as string[]).includes(value)
|
||||
// ? value as unknown as T
|
||||
// : undefined;
|
||||
// }
|
||||
//
|
||||
// export function parseMisfireInstruction(str: string): MisfireInstruction {
|
||||
// return enumFromStringValue<MisfireInstruction>(MisfireInstruction, str);
|
||||
// // return (<any>MisfireInstruction)[str]
|
||||
// // const indexOfStr = Object.values(MisfireInstruction).indexOf(str as unknown as MisfireInstruction);
|
||||
// // const key = Object.keys(Sizes)[indexOfStr];
|
||||
// // return MisfireInstruction[k]
|
||||
// // return Object.values(MisfireInstruction).find(val => val === str);
|
||||
// }
|
||||
|
||||
export const MisfireInstructionCaption = new Map<number, string>([
|
||||
[MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW,
|
||||
`The job is executed immediately after the scheduler discovers misfire situation.<br/>
|
||||
In case of the trigger has been set with a repeat count, this policy is equals to RESCHEDULE NOW WITH REMAINING REPEAT COUNT`
|
||||
],
|
||||
[MisfireInstruction.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT,
|
||||
`First misfired trigger is executed immediately. Then the scheduler waits desired interval and executes all remaining triggers.<br/>
|
||||
Effectively the first fire time of the misfired trigger is moved to current time with no other changes.`
|
||||
],
|
||||
[MisfireInstruction.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT,
|
||||
`First misfired execution runs immediately. Remaining misfired executions are discarded. Remaining not-yet-fired triggers are executed
|
||||
with desired interval, starting from the recovered misfired execution.<br/>
|
||||
Use this policy if your constraint is to honor the end date time.<br/>
|
||||
<strong>Warning</strong> The actual number of job executions could be less than initially set,
|
||||
because some of the misfired triggers are ignored. The end date time you set is always `
|
||||
],
|
||||
[MisfireInstruction.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT,
|
||||
`In case of misfire event, the scheduler won't do anything immediately. Instead it will wait for next scheduled time the trigger and
|
||||
run all triggers with scheduled interval. Misfired trigger are simply post-poned but not ignored.<br/>
|
||||
Use this policy if your constraint is to execute the job for the all times equals to the repeation counter.<br/>' +
|
||||
'<strong>Warning</strong> The scheduler can completed over the end date time you set `],
|
||||
[MisfireInstruction.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT,
|
||||
`In case of misfire event, the trigger is re-scheduled to the next scheduled time after 'now'
|
||||
with the repeat count set to what it would be if it had not missed any firings.<br/>
|
||||
Use this policy if no jobs must run after the end date time.<br/>
|
||||
<strong>Warning</strong> The actual number of job executions could be less than initially set,
|
||||
because the misfired trigger are ignored.<br/>
|
||||
This policy could cause the Trigger to go directly to the 'COMPLETE' state if all fire-times where missed.`
|
||||
]
|
||||
]);
|
||||
@@ -0,0 +1,7 @@
|
||||
export class ScheduledJobCommand {
|
||||
jobClass: string;
|
||||
description: string;
|
||||
durable: boolean;
|
||||
requestsRecovery: boolean;
|
||||
jobDataMap: {[key: string]: unknown};
|
||||
}
|
||||
12
quartz-manager-frontend/src/app/model/scheduled-job.model.ts
Normal file
12
quartz-manager-frontend/src/app/model/scheduled-job.model.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {JobKeyModel} from './jobKey.model';
|
||||
import {TriggerKey} from './triggerKey.model';
|
||||
|
||||
export class ScheduledJob {
|
||||
jobKeyDTO: JobKeyModel;
|
||||
jobClassName: string;
|
||||
description: string;
|
||||
durable: boolean;
|
||||
requestsRecovery: boolean;
|
||||
jobDataMap: {[key: string]: unknown};
|
||||
triggerKeys: TriggerKey[];
|
||||
}
|
||||
23
quartz-manager-frontend/src/app/model/scheduler.model.ts
Normal file
23
quartz-manager-frontend/src/app/model/scheduler.model.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {TriggerKey} from './triggerKey.model';
|
||||
|
||||
export class Scheduler {
|
||||
name: string;
|
||||
instanceId: string;
|
||||
status: string;
|
||||
triggerKeys: TriggerKey[];
|
||||
quartzVersion: string;
|
||||
jobStoreClass: string;
|
||||
jobStoreSupportsPersistence: boolean;
|
||||
clustered: boolean;
|
||||
threadPoolClass: string;
|
||||
threadPoolSize: number;
|
||||
runningSince: string;
|
||||
numberOfJobsExecuted: number;
|
||||
|
||||
constructor(name: string, instanceId: string, status: string, triggerKeys: TriggerKey[]) {
|
||||
this.name = name;
|
||||
this.status = status;
|
||||
this.instanceId = instanceId;
|
||||
this.triggerKeys = triggerKeys;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export class SchedulerConfig {
|
||||
|
||||
triggerPerDay : number = 0
|
||||
maxCount : number = 0
|
||||
|
||||
constructor(triggerPerDay = 0, maxCount = 0) {
|
||||
this.triggerPerDay = triggerPerDay
|
||||
this.maxCount = maxCount
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export class SimpleTriggerCommand {
|
||||
triggerName: string;
|
||||
triggerGroup: string;
|
||||
jobClass: string;
|
||||
jobKey: {group: string; name: string};
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
repeatCount: number;
|
||||
repeatInterval: number;
|
||||
misfireInstruction: string;
|
||||
jobDataMap: {[key: string]: unknown};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class SimpleTriggerForm {
|
||||
triggerName: string;
|
||||
jobClass: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
repeatCount: number;
|
||||
repeatInterval: number;
|
||||
misfireInstruction: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {Trigger} from './trigger.model';
|
||||
|
||||
export class SimpleTrigger extends Trigger {
|
||||
repeatCount: number;
|
||||
repeatInterval: number;
|
||||
timesTriggered: number;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {JobKeyModel} from './jobKey.model';
|
||||
|
||||
export type TriggerType = 'SIMPLE' | 'CRON' | 'DAILY_TIME_INTERVAL' | 'CALENDAR_INTERVAL';
|
||||
|
||||
export class TriggerCommand {
|
||||
triggerType: TriggerType = 'SIMPLE';
|
||||
jobClass: string;
|
||||
jobKey: JobKeyModel;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
description: string;
|
||||
priority: number;
|
||||
calendarName: string;
|
||||
misfireInstruction: string;
|
||||
jobDataMap: {[key: string]: unknown};
|
||||
repeatCount: number;
|
||||
repeatInterval: number;
|
||||
repeatIntervalUnit: string;
|
||||
cronExpression: string;
|
||||
timeZone: string;
|
||||
startTimeOfDay: string;
|
||||
endTimeOfDay: string;
|
||||
daysOfWeek: number[];
|
||||
preserveHourOfDayAcrossDaylightSavings: boolean;
|
||||
skipDayIfHourDoesNotExist: boolean;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default class TriggerFiredBundle {
|
||||
timesTriggered: number;
|
||||
repeatCount: number;
|
||||
finalFireTime: string;
|
||||
nextFireTime: string;
|
||||
previousFireTime: string;
|
||||
jobKey: string;
|
||||
jobClass: string;
|
||||
percentage: number;
|
||||
}
|
||||
32
quartz-manager-frontend/src/app/model/trigger.model.ts
Normal file
32
quartz-manager-frontend/src/app/model/trigger.model.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {TriggerKey} from './triggerKey.model';
|
||||
import {JobKeyModel} from './jobKey.model';
|
||||
import {JobDetail} from './jobDetail.model';
|
||||
|
||||
export class Trigger {
|
||||
triggerKeyDTO: TriggerKey = new TriggerKey();
|
||||
priority: number;
|
||||
startTime: Date;
|
||||
description: string;
|
||||
endTime: Date;
|
||||
finalFireTime: Date;
|
||||
misfireInstruction: number;
|
||||
nextFireTime: Date;
|
||||
previousFireTime: Date;
|
||||
type: string;
|
||||
state: string;
|
||||
calendarName: string;
|
||||
jobKeyDTO: JobKeyModel;
|
||||
jobDetailDTO: JobDetail = new JobDetail();
|
||||
mayFireAgain: boolean;
|
||||
jobDataMap: {[key: string]: unknown};
|
||||
cronExpression: string;
|
||||
timeZone: string;
|
||||
repeatInterval: number;
|
||||
repeatCount: number;
|
||||
repeatIntervalUnit: string;
|
||||
startTimeOfDay: string;
|
||||
endTimeOfDay: string;
|
||||
daysOfWeek: number[];
|
||||
preserveHourOfDayAcrossDaylightSavings: boolean;
|
||||
skipDayIfHourDoesNotExist: boolean;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class TriggerKey {
|
||||
name: string;
|
||||
group: string;
|
||||
|
||||
constructor(name?: string, group?: string) {
|
||||
this.name = name;
|
||||
this.group = group;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
|
||||
// import 'core-js/es6/symbol';
|
||||
// import 'core-js/es6/object';
|
||||
// import 'core-js/es6/function';
|
||||
// import 'core-js/es6/parse-int';
|
||||
// import 'core-js/es6/parse-float';
|
||||
// import 'core-js/es6/number';
|
||||
// import 'core-js/es6/math';
|
||||
// import 'core-js/es6/string';
|
||||
// import 'core-js/es6/date';
|
||||
// import 'core-js/es6/array';
|
||||
// import 'core-js/es6/regexp';
|
||||
// import 'core-js/es6/map';
|
||||
// import 'core-js/es6/set';
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/** IE10 and IE11 requires the following to support `@angular/animation`. */
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
import 'core-js/es6/reflect';
|
||||
import 'core-js/es7/reflect';
|
||||
|
||||
|
||||
|
||||
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
|
||||
/**
|
||||
* Date, currency, decimal and percent pipes.
|
||||
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
|
||||
*/
|
||||
// import 'intl'; // Run `npm install --save intl`.
|
||||
|
||||
/***************************************************************************************************
|
||||
* MATERIAL 2
|
||||
*/
|
||||
import 'hammerjs/hammer';
|
||||
@@ -1,83 +1,83 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { ApiService } from './api.service';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Router} from '@angular/router';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||
import {ApiService} from './api.service';
|
||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||
import {Router} from '@angular/router';
|
||||
import {jest} from '@jest/globals'
|
||||
|
||||
class Data{
|
||||
name: string
|
||||
class Data {
|
||||
name: string
|
||||
}
|
||||
|
||||
class HttpResponseMock {
|
||||
constructor(
|
||||
public body: unknown,
|
||||
public opts?: {
|
||||
headers?:
|
||||
| HttpHeaders
|
||||
| {
|
||||
[name: string]: string | string[];
|
||||
};
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
}
|
||||
) {}
|
||||
constructor(
|
||||
public body: unknown,
|
||||
public opts?: {
|
||||
headers?:
|
||||
| HttpHeaders
|
||||
| {
|
||||
[name: string]: string | string[];
|
||||
};
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
const routerSpy = jest.spyOn(Router.prototype, 'navigateByUrl');
|
||||
|
||||
describe('ApiServiceTest', () => {
|
||||
|
||||
let apiService: ApiService;
|
||||
let httpClient: HttpClient;
|
||||
let httpTestingController: HttpTestingController;
|
||||
let apiService: ApiService;
|
||||
let httpClient: HttpClient;
|
||||
let httpTestingController: HttpTestingController;
|
||||
|
||||
const SAMPLE_URL = '/sample-url';
|
||||
const URL_401 = '/url-response-401';
|
||||
const testData: Data = {name: 'Test Data'};
|
||||
const SAMPLE_URL = '/sample-url';
|
||||
const URL_401 = '/url-response-401';
|
||||
const testData: Data = {name: 'Test Data'};
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [ApiService, {provide: Router, useValue: routerSpy}]
|
||||
});
|
||||
apiService = TestBed.inject(ApiService);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [ApiService, {provide: Router, useValue: routerSpy}]
|
||||
});
|
||||
apiService = TestBed.inject(ApiService);
|
||||
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
httpTestingController = TestBed.inject(HttpTestingController);
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
httpTestingController = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
it('should be created', (): void => {
|
||||
expect(apiService).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can test HttpClient.get', (): void => {
|
||||
|
||||
apiService.get(SAMPLE_URL).subscribe((res: Data) => {
|
||||
expect(res).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should be created', (): void => {
|
||||
expect(apiService).toBeTruthy();
|
||||
const req = httpTestingController.expectOne(SAMPLE_URL)
|
||||
expect(req.request.method).toEqual('GET');
|
||||
req.flush(new HttpResponseMock(testData));
|
||||
httpTestingController.verify();
|
||||
});
|
||||
|
||||
it('doesn\'t do anything if 401 is received', (): void => {
|
||||
|
||||
apiService.get(URL_401).subscribe((res: Data) => {
|
||||
expect(false);
|
||||
}, (error) => {
|
||||
expect(error.status).toBe(401);
|
||||
expect(routerSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('can test HttpClient.get', (): void => {
|
||||
const req = httpTestingController.expectOne(URL_401)
|
||||
expect(req.request.method).toEqual('GET');
|
||||
req.flush(null, {status: 401, statusText: 'unauthenticated'});
|
||||
httpTestingController.verify();
|
||||
});
|
||||
|
||||
apiService.get(SAMPLE_URL).subscribe((res: Data) => {
|
||||
expect(res).toEqual(testData);
|
||||
});
|
||||
|
||||
const req = httpTestingController.expectOne(SAMPLE_URL)
|
||||
expect(req.request.method).toEqual('GET');
|
||||
req.flush(new HttpResponseMock(testData));
|
||||
httpTestingController.verify();
|
||||
});
|
||||
|
||||
it('doesn\'t do anything if 401 is received', (): void => {
|
||||
|
||||
apiService.get(URL_401).subscribe((res: Data) => {
|
||||
expect(false);
|
||||
}, (error) =>
|
||||
{
|
||||
expect(error.status).toBe(401);
|
||||
expect(routerSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const req = httpTestingController.expectOne(URL_401)
|
||||
expect(req.request.method).toEqual('GET');
|
||||
req.flush(null, {status: 401, statusText: 'unauthenticated'});
|
||||
httpTestingController.verify();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { HttpClient, HttpHeaders, HttpResponse, HttpRequest, HttpEventType, HttpParams } from '@angular/common/http';
|
||||
import { Router} from '@angular/router';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError, map, filter, tap } from 'rxjs/operators'
|
||||
import { serialize } from '../shared/utilities/serialize';
|
||||
import {Router} from '@angular/router';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
import {catchError, map, filter, tap} from 'rxjs/operators'
|
||||
import {serialize} from '../shared/utilities/serialize';
|
||||
|
||||
export enum RequestMethod {
|
||||
Get = 'GET',
|
||||
@@ -18,17 +18,6 @@ export enum RequestMethod {
|
||||
@Injectable()
|
||||
export class ApiService {
|
||||
|
||||
private static extractTokenFromHttpResponse(res: HttpResponse<any>): string {
|
||||
let authorization: string = null;
|
||||
let headers: HttpHeaders = res.headers;
|
||||
if (headers && headers.has('Authorization')){
|
||||
authorization = headers.get('Authorization');
|
||||
if(authorization.startsWith('Bearer '))
|
||||
authorization = authorization.substring(7);
|
||||
}
|
||||
return authorization;
|
||||
}
|
||||
|
||||
headers = new HttpHeaders({
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
@@ -36,7 +25,20 @@ export class ApiService {
|
||||
|
||||
private jwtToken: string;
|
||||
|
||||
constructor( private http: HttpClient, private router: Router) { }
|
||||
private static extractTokenFromHttpResponse(res: HttpResponse<any>): string {
|
||||
let authorization: string = null;
|
||||
const headers: HttpHeaders = res.headers;
|
||||
if (headers && headers.has('Authorization')) {
|
||||
authorization = headers.get('Authorization');
|
||||
if (authorization.startsWith('Bearer ')) {
|
||||
authorization = authorization.substring(7);
|
||||
}
|
||||
}
|
||||
return authorization;
|
||||
}
|
||||
|
||||
constructor(private http: HttpClient, private router: Router) {
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this.jwtToken = token;
|
||||
@@ -50,8 +52,9 @@ export class ApiService {
|
||||
withCredentials: true
|
||||
};
|
||||
|
||||
if (args)
|
||||
if (args) {
|
||||
options['params'] = serialize(args);
|
||||
}
|
||||
|
||||
return this.http.get(path, options)
|
||||
.pipe(catchError(this.checkError.bind(this)));
|
||||
@@ -73,20 +76,21 @@ export class ApiService {
|
||||
const options = {
|
||||
headers: customHeaders || this.headers,
|
||||
withCredentials: true
|
||||
}
|
||||
}
|
||||
|
||||
const req = new HttpRequest(method, path, body, options);
|
||||
|
||||
return this.http.request(req)
|
||||
.pipe(
|
||||
filter(response => response instanceof HttpResponse),
|
||||
tap((resp: HttpResponse<any>) => {
|
||||
let jwtToken = ApiService.extractTokenFromHttpResponse(resp);
|
||||
if(jwtToken)
|
||||
this.setToken(jwtToken);
|
||||
}),
|
||||
map((response: HttpResponse<any>) => response.body),
|
||||
catchError(error => this.checkError(error))
|
||||
filter(response => response instanceof HttpResponse),
|
||||
tap((resp: HttpResponse<any>) => {
|
||||
const jwtToken = ApiService.extractTokenFromHttpResponse(resp);
|
||||
if (jwtToken) {
|
||||
this.setToken(jwtToken);
|
||||
}
|
||||
}),
|
||||
map((response: HttpResponse<any>) => response.body),
|
||||
catchError(error => this.checkError(error))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -100,6 +104,5 @@ export class ApiService {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {Injectable} from '@angular/core';
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||
import { ApiService } from './api.service';
|
||||
import { UserService } from './user.service';
|
||||
import { ConfigService } from './config.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import {ApiService} from './api.service';
|
||||
import {UserService} from './user.service';
|
||||
import {ConfigService} from './config.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -12,7 +12,8 @@ export class AuthService {
|
||||
private apiService: ApiService,
|
||||
private userService: UserService,
|
||||
private config: ConfigService,
|
||||
) { }
|
||||
) {
|
||||
}
|
||||
|
||||
login(user) {
|
||||
const loginHeaders = new HttpHeaders({
|
||||
@@ -21,24 +22,14 @@ export class AuthService {
|
||||
});
|
||||
const body = `username=${user.username}&password=${user.password}`;
|
||||
return this.apiService.post(this.config.login_url, body, loginHeaders)
|
||||
.pipe(
|
||||
map(() => {
|
||||
console.log("Login success");
|
||||
this.userService.getMyInfo().subscribe();
|
||||
})
|
||||
);
|
||||
.pipe(
|
||||
map(() => {
|
||||
console.log('Login success');
|
||||
this.userService.getUserInfo().subscribe();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
signup(user){
|
||||
const signupHeaders = new HttpHeaders({
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
return this.apiService.post(this.config.signup_url, JSON.stringify(user), signupHeaders).pipe(map(() =>{
|
||||
console.log("Sign up success");
|
||||
}));
|
||||
}
|
||||
|
||||
logout() {
|
||||
return this.apiService.post(this.config.logout_url, {})
|
||||
.pipe(map(() => {
|
||||
@@ -47,9 +38,4 @@ export class AuthService {
|
||||
}));
|
||||
}
|
||||
|
||||
changePassword(passwordChanger) {
|
||||
return this.apiService.post(this.config.change_password_url, passwordChanger);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {jest} from '@jest/globals';
|
||||
import {CalendarService} from './calendar.service';
|
||||
|
||||
describe('CalendarService', () => {
|
||||
let apiService: any;
|
||||
let calendarService: CalendarService;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
delete: jest.fn()
|
||||
};
|
||||
calendarService = new CalendarService(apiService);
|
||||
});
|
||||
|
||||
it('uses calendar registry endpoints', () => {
|
||||
const calendar: any = {name: 'weekends', type: 'WEEKLY'};
|
||||
const time = new Date('2026-05-12T12:00:00.000Z');
|
||||
|
||||
calendarService.fetchCalendars();
|
||||
calendarService.getCalendar('weekends');
|
||||
calendarService.createCalendar('weekends', calendar);
|
||||
calendarService.updateCalendar('weekends', calendar);
|
||||
calendarService.deleteCalendar('weekends');
|
||||
calendarService.testIncludedTime('weekends', time);
|
||||
|
||||
expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/calendars');
|
||||
expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/calendars/weekends');
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/calendars/weekends', calendar);
|
||||
expect(apiService.put).toHaveBeenCalledWith('/quartz-manager/calendars/weekends', calendar);
|
||||
expect(apiService.delete).toHaveBeenCalledWith('/quartz-manager/calendars/weekends');
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/calendars/weekends/included-time-test', {time});
|
||||
});
|
||||
});
|
||||
34
quartz-manager-frontend/src/app/services/calendar.service.ts
Normal file
34
quartz-manager-frontend/src/app/services/calendar.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
import {ApiService} from './api.service';
|
||||
import {CONTEXT_PATH, getBaseUrl} from './config.service';
|
||||
import {CalendarIncludedTimeTest, QuartzCalendar} from '../model/calendar.model';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
fetchCalendars = (): Observable<QuartzCalendar[]> => {
|
||||
return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/calendars`);
|
||||
}
|
||||
|
||||
getCalendar = (name: string): Observable<QuartzCalendar> => {
|
||||
return this.apiService.get(getBaseUrl() + `${CONTEXT_PATH}/calendars/${name}`);
|
||||
}
|
||||
|
||||
createCalendar = (name: string, calendar: QuartzCalendar): Observable<QuartzCalendar> => {
|
||||
return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/calendars/${name}`, calendar);
|
||||
}
|
||||
|
||||
updateCalendar = (name: string, calendar: QuartzCalendar): Observable<QuartzCalendar> => {
|
||||
return this.apiService.put(getBaseUrl() + `${CONTEXT_PATH}/calendars/${name}`, calendar);
|
||||
}
|
||||
|
||||
deleteCalendar = (name: string): Observable<void> => {
|
||||
return this.apiService.delete(getBaseUrl() + `${CONTEXT_PATH}/calendars/${name}`);
|
||||
}
|
||||
|
||||
testIncludedTime = (name: string, time: Date): Observable<CalendarIncludedTimeTest> => {
|
||||
return this.apiService.post(getBaseUrl() + `${CONTEXT_PATH}/calendars/${name}/included-time-test`, {time});
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,57 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { environment } from '../../environments/environment';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {environment} from '../../environments/environment';
|
||||
|
||||
|
||||
const WEBJAR_PATH = '/quartz-manager-ui/';
|
||||
|
||||
export function getHtmlBaseUrl(){
|
||||
const baseUrl = getBaseUrl() || '/';
|
||||
return environment.production ? getBaseUrl() + WEBJAR_PATH: '/';
|
||||
}
|
||||
export const CONTEXT_PATH = '/quartz-manager';
|
||||
|
||||
export function getBaseUrl(){
|
||||
if(environment.production){
|
||||
let contextPath: string = window.location.pathname.split('/')[1] || '';
|
||||
if(contextPath && ('/' + contextPath + '/') === WEBJAR_PATH)
|
||||
return '';
|
||||
if(contextPath)
|
||||
contextPath = '/' + contextPath;
|
||||
return contextPath;
|
||||
}
|
||||
return '';
|
||||
export function getHtmlBaseUrl() {
|
||||
const baseUrl = getBaseUrl() || '/';
|
||||
return environment.production ? getBaseUrl() + WEBJAR_PATH : '/';
|
||||
}
|
||||
|
||||
export function getBaseUrl() {
|
||||
if (environment.production) {
|
||||
let contextPath: string = window.location.pathname.split('/')[1] || '';
|
||||
if (contextPath && ('/' + contextPath + '/') === WEBJAR_PATH) {
|
||||
return '';
|
||||
}
|
||||
if (contextPath) {
|
||||
contextPath = '/' + contextPath;
|
||||
}
|
||||
return contextPath;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
|
||||
private _api_url = getBaseUrl() + '/quartz-manager/api'
|
||||
private _auth_url = getBaseUrl() + `${CONTEXT_PATH}/auth`
|
||||
|
||||
private _refresh_token_url = this._api_url + '/refresh';
|
||||
private _refresh_token_url = this._auth_url + '/refresh';
|
||||
|
||||
private _login_url = this._api_url + '/login';
|
||||
private _login_url = this._auth_url + '/login';
|
||||
|
||||
private _logout_url = this._api_url + '/logout';
|
||||
private _logout_url = this._auth_url + '/logout';
|
||||
|
||||
private _change_password_url = this._api_url + '/changePassword';
|
||||
|
||||
private _whoami_url = this._api_url + '/whoami';
|
||||
|
||||
private _user_url = this._api_url + '/user';
|
||||
|
||||
private _users_url = this._user_url + '/all';
|
||||
|
||||
private _reset_credentials_url = this._user_url + '/reset-credentials';
|
||||
|
||||
private _signup_url = this._api_url + '/signup';
|
||||
|
||||
get reset_credentials_url(): string {
|
||||
return this._reset_credentials_url;
|
||||
}
|
||||
private _whoami_url = this._auth_url + '/whoami';
|
||||
|
||||
get refresh_token_url(): string {
|
||||
return this._refresh_token_url;
|
||||
return this._refresh_token_url;
|
||||
}
|
||||
|
||||
get whoami_url(): string {
|
||||
return this._whoami_url;
|
||||
}
|
||||
|
||||
get users_url(): string {
|
||||
return this._users_url;
|
||||
return this._whoami_url;
|
||||
}
|
||||
|
||||
get login_url(): string {
|
||||
return this._login_url;
|
||||
return this._login_url;
|
||||
}
|
||||
|
||||
get logout_url(): string {
|
||||
return this._logout_url;
|
||||
}
|
||||
|
||||
get change_password_url(): string {
|
||||
return this._change_password_url;
|
||||
}
|
||||
|
||||
get signup_url():string {
|
||||
return this._signup_url;
|
||||
return this._logout_url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ 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 './calendar.service'
|
||||
export * from './job.service'
|
||||
|
||||
|
||||
|
||||
46
quartz-manager-frontend/src/app/services/job.service.spec.ts
Normal file
46
quartz-manager-frontend/src/app/services/job.service.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import JobService from './job.service';
|
||||
import {ScheduledJob} from '../model/scheduled-job.model';
|
||||
import {jest} from '@jest/globals';
|
||||
|
||||
describe('JobService', () => {
|
||||
let apiService: any;
|
||||
let jobService: JobService;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
delete: jest.fn()
|
||||
};
|
||||
jobService = new JobService(apiService);
|
||||
});
|
||||
|
||||
it('uses job class and scheduled job endpoints', () => {
|
||||
const job = new ScheduledJob();
|
||||
const command = {
|
||||
jobClass: 'SampleJob',
|
||||
description: '',
|
||||
durable: true,
|
||||
requestsRecovery: false,
|
||||
jobDataMap: {}
|
||||
};
|
||||
job.jobKeyDTO = {group: 'DEFAULT', name: 'sampleJob'};
|
||||
|
||||
jobService.fetchJobs();
|
||||
jobService.fetchScheduledJobs();
|
||||
jobService.getScheduledJob('DEFAULT', 'sampleJob');
|
||||
jobService.createJob('DEFAULT', 'sampleJob', command);
|
||||
jobService.updateJob('DEFAULT', 'sampleJob', command);
|
||||
jobService.triggerJob(job);
|
||||
jobService.deleteJob(job);
|
||||
|
||||
expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/job-classes');
|
||||
expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/jobs');
|
||||
expect(apiService.get).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob');
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob', command);
|
||||
expect(apiService.put).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob', command);
|
||||
expect(apiService.post).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob/trigger', {});
|
||||
expect(apiService.delete).toHaveBeenCalledWith('/quartz-manager/jobs/DEFAULT/sampleJob');
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user