mirror of
https://github.com/fabioformosa/quartz-manager.git
synced 2026-05-15 06:10:29 +09:00
Compare commits
164 Commits
v4.0.2
...
feature/#1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0148056b1f | ||
|
|
ad6a61f3df | ||
|
|
5cb73019de | ||
|
|
8a8e878e47 | ||
|
|
fae82e1d4e | ||
|
|
e0011913c2 | ||
|
|
a59b6a6c96 | ||
|
|
68aaab6ac4 | ||
|
|
a1d8b12e98 | ||
|
|
45d6a4c59a | ||
|
|
e6f6fb5f06 | ||
|
|
13c438d097 | ||
|
|
412e455907 | ||
|
|
75d630aad0 | ||
|
|
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 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
# .dockerignore
|
||||
quartz-manager-frontend/node_modules
|
||||
10
.github/workflows/maven-release.yml
vendored
10
.github/workflows/maven-release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'temurin'
|
||||
server-id: ossrh
|
||||
server-id: maven-central-release
|
||||
server-username: MAVEN_USERNAME
|
||||
server-password: MAVEN_PASSWORD
|
||||
gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }}
|
||||
@@ -29,10 +29,10 @@ jobs:
|
||||
run: mvn -B package --file quartz-manager-parent/pom.xml
|
||||
|
||||
- name: Publish to maven central
|
||||
run: mvn deploy --file quartz-manager-parent/pom.xml --batch-mode -P release-maven-central, build-webjar
|
||||
run: mvn deploy --file quartz-manager-parent/pom.xml --batch-mode -P "release-maven-central,build-webjar"
|
||||
env:
|
||||
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
|
||||
MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
|
||||
MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_TOKEN_USERNAME }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASSWORD }}
|
||||
MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }}
|
||||
|
||||
- name: Set up Java 11 for publishing to GitHub Packages
|
||||
@@ -41,6 +41,6 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'temurin'
|
||||
- name: Publish to GitHub Packages Apache Maven
|
||||
run: mvn deploy --file quartz-manager-parent/pom.xml -P deploy-github, build-webjar
|
||||
run: mvn deploy --file quartz-manager-parent/pom.xml -P "deploy-github,build-webjar"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
15
.github/workflows/sonar-java.yml
vendored
15
.github/workflows/sonar-java.yml
vendored
@@ -7,27 +7,28 @@ on:
|
||||
# paths: [ 'quartz-manager-parent/**' ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build:
|
||||
name: Build and analyze
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
distribution: 'zulu' # Alternative distribution options are available.
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.sonar/cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
- name: Cache Maven packages
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.m2
|
||||
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
/.project
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,3 +1,26 @@
|
||||
## **v4.0.10**
|
||||
Migrated to the new maven central repo
|
||||
|
||||
## **v4.0.9**
|
||||
Fixed a bug which prevented to run the liquibase migration scripts in case of usage of quartz-manager-starter-persistence
|
||||
|
||||
## **v4.0.8**
|
||||
Upgraded the frontend to angular v14
|
||||
|
||||
## **v4.0.6**
|
||||
Minor bug fixes
|
||||
|
||||
## **v4.0.5**
|
||||
Fixed potential security issues
|
||||
|
||||
## **v4.0.4**
|
||||
* Conformed the trigger configuration to the Simple Trigger of Quartz
|
||||
* **BREAKING CHANGE** Changed accordingly the API and the UI
|
||||
* Made Quartz Manager embeddable in projects with existing quartz instance, security layer, swagger ui.
|
||||
|
||||
## **v3.1.0**
|
||||
* Added a new persistence module to persist the quartz triggers in a postgresql database
|
||||
|
||||
## **v3.0.1**
|
||||
|
||||
Quartz-Manager is now publicly available into the maven central repo into 3 different packages.
|
||||
|
||||
34
Dockerfile
Normal file
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"]
|
||||
84
README.md
84
README.md
@@ -1,6 +1,25 @@
|
||||
[](https://github.com/fabioformosa/quartz-manager/actions/workflows/maven.yml)
|
||||
[](https://github.com/fabioformosa/quartz-manager/actions/workflows/npm.yml)
|
||||
[](https://maven-badges.herokuapp.com/maven-central/it.fabioformosa.quartz-manager/quartz-manager-starter-api)
|
||||
[](https://maven-badges.herokuapp.com/maven-central/it.fabioformosa.quartz-manager/quartz-manager-starter-api)
|
||||
[](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)
|
||||
|
||||
# Table Of Contents
|
||||
|
||||
|
||||
[QUARTZ MANAGER](https://github.com/fabioformosa/quartz-manager#quartz-manager)
|
||||
[Quartz Manager UI](https://github.com/fabioformosa/quartz-manager#quartz-manager-ui)
|
||||
[Quartz Manager API](https://github.com/fabioformosa/quartz-manager#quartz-manager-api)
|
||||
[HOW IT WORKS](https://github.com/fabioformosa/quartz-managerhttps://github.com/fabioformosa/quartz-manager#get-started)
|
||||
[Quartz Manager Starter API Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-api-lib)
|
||||
[Quartz Manager Starter UI Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-ui-lib)
|
||||
[Quartz Manager Starter Security Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-security-lib)
|
||||
[Quartz Manager Persistence Lib](https://github.com/fabioformosa/quartz-manager#quartz-manager-starter-persistence-lib)
|
||||
[EXAMPLES](https://github.com/fabioformosa/quartz-manager#examples)
|
||||
[LIMITATIONS](https://github.com/fabioformosa/quartz-manager#limitations)
|
||||
[ROADMAP](https://github.com/fabioformosa/quartz-manager#roadmap)
|
||||
[REPOSITORY](https://github.com/fabioformosa/quartz-manager#repository)
|
||||
[HOW TO CONTRIBUTE](https://github.com/fabioformosa/quartz-manager#how-to-contribute)
|
||||
[SUPPORT THE PROJECT](https://github.com/fabioformosa/quartz-manager#%EF%B8%8F-support-the-project-%EF%B8%8F)
|
||||
|
||||
# QUARTZ MANAGER
|
||||
In the last decade, the [Quartz Scheduler](http://www.quartz-scheduler.org/) has become the most adopted opensource job scheduling library for Java applications.
|
||||
@@ -14,13 +33,13 @@ Quartz Manager is a Java library you can import in your Spring-Based Web Applica
|
||||
The **Quartz Manager UI** is a dashboard in the form of a single-page-application provided by the Quartz Manager Java library itself. You can have it embedded in your project, as well as you get embedded the Swagger UI.
|
||||
It leverages the websockets to receive in real-time the trigger updates and the outcomes of the job executions.
|
||||
|
||||

|
||||

|
||||
|
||||
## QUARTZ MANAGER API
|
||||
Quart-Manager exposes REST endpoints to interact with the Quartz Scheduler. This endpoints are invoked by Quartz Manager UI also.
|
||||
The REST API are documented by an OpenAPI Specification interface.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
# HOW IT WORKS
|
||||
@@ -37,7 +56,7 @@ In the latter case, in which there isn't an existing quartz instance, you can re
|
||||
* You can enable a secure layer, if your project doesn't have any, to give access at the API and the UI only to authenticated users.
|
||||
* You can enable a persistent layer, to persist the config and the progress of your trigger, in a postgresql database.
|
||||
|
||||
# QUICK START
|
||||
# GET STARTED
|
||||
|
||||
**Requirements**
|
||||
Java 9+, Spring Framework 5+ (Spring Boot 2.x)
|
||||
@@ -52,7 +71,7 @@ In order to decrease the overall configuration time for the project, all modules
|
||||
|
||||
Below the list of the quartz-manager modules you can import.
|
||||
|
||||
## Quartz Manager API Lib
|
||||
## Quartz Manager Starter API Lib
|
||||
This is the only mandatory module of the library.
|
||||
Add the dependency, make eligible for Quart Manager the job classes and set the props in your `application.properties` file.
|
||||
|
||||
@@ -63,12 +82,12 @@ Add the dependency, make eligible for Quart Manager the job classes and set the
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-api</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<version>4.0.9</version>
|
||||
</dependency>
|
||||
```
|
||||
#### Gradle
|
||||
```
|
||||
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-api', version: '4.0.0'
|
||||
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-api', version: '4.0.9'
|
||||
```
|
||||
|
||||
### Step 2. Quartz Manager Job Classes
|
||||
@@ -96,11 +115,17 @@ In this way, Quartz Manager is able to collect and display the outcomes at the U
|
||||
|
||||
|
||||
### REST API & OpenAPI Specification
|
||||
Set the app prop `quartz-manager.oas.enabled=true` you want to expose the OpenApi Specification of the Quartz Manager APIs.
|
||||
Set the app prop `quartz-manager.oas.enabled=true` if you want to expose the OpenApi Specification of the Quartz Manager APIs.
|
||||
Reach out the swagger-ui at the URL:
|
||||
[http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html)
|
||||
|
||||
If your project has already an OpenAPI instance and you've set `quartz-manager.oas.enabled=true`, then make sure to add an OpenApiGroup to group the API of your application. Quart Manager exposes its API in group called "Quartz Manager".
|
||||
If your project has already an OpenAPI instance and you've set `quartz-manager.oas.enabled=true`, then make sure to add an OpenApiGroup to group the API of your application. Quart Manager exposes its API in group called "Quartz Manager". If you use OAS Annotations:
|
||||
```
|
||||
@Bean
|
||||
public GroupedOpenApi simplySpringDemoGroupedOpenApi() {
|
||||
return GroupedOpenApi.builder().group("myapp").packagesToScan("com.example.myapp").build();
|
||||
}
|
||||
```
|
||||
|
||||
### QUARTZ SETTINGS
|
||||
Quartz Manager creates its own instance of a [Quartz Scheduler](http://www.quartz-scheduler.org/).
|
||||
@@ -122,7 +147,7 @@ The prerequesite is that you've imported a quartz scheduler ver 2.3.x.
|
||||
|
||||
You can configure the Quartz instance managed by Quartz Manager through the file `managed-quartz.properties` and your own Quartz instance through the file `quartz.properties`.
|
||||
|
||||
If you've created a `SchedulerFactoryBean`, tag it as @Primary to avoid conflicts in-type, since Quartz Manager creates another bean of the same type.
|
||||
If you've created a `SchedulerFactoryBean`, tag it as `@Primary` to avoid conflicts in-type, since Quartz Manager creates another bean of the same type.
|
||||
|
||||
```
|
||||
@Primary
|
||||
@@ -135,7 +160,7 @@ If you've created a `SchedulerFactoryBean`, tag it as @Primary to avoid conflict
|
||||
```
|
||||
|
||||
|
||||
## Quartz Manager UI Lib
|
||||
## Quartz Manager Starter UI Lib
|
||||
You can optionally import the following dependency to have the UI Dashboard to interact with the Quartz Manager API.
|
||||
|
||||
### Dependency
|
||||
@@ -145,12 +170,12 @@ You can optionally import the following dependency to have the UI Dashboard to i
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-ui</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<version>4.0.9</version>
|
||||
</dependency>
|
||||
```
|
||||
#### Gradle
|
||||
```
|
||||
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-ui', version: '4.0.0'
|
||||
implementation group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-ui', version: '4.0.9'
|
||||
```
|
||||
|
||||
### Reach out the UI Console at URL
|
||||
@@ -158,7 +183,7 @@ if you run locally [http://localhost:8080/quartz-manager-ui/index.html](http://l
|
||||
|
||||
|
||||
|
||||
## Quartz Manager Security Lib
|
||||
## Quartz Manager Starter Security Lib
|
||||
|
||||
Import this optional dependency, if you want to enable a security layer and allow the access to the REST API and UI only to authenticated users.
|
||||
The authentication model of Quartz Manager is based on [JWT](https://jwt.io/).
|
||||
@@ -180,14 +205,14 @@ Future development: the Quart Manager Security OAuth2 client.
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-security</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<version>4.0.9</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
#### Gradle
|
||||
|
||||
```
|
||||
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-security', version: '4.0.0'
|
||||
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-security', version: '4.0.9'
|
||||
```
|
||||
|
||||
|
||||
@@ -203,11 +228,11 @@ compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-s
|
||||
|quartz-manager.security.accounts.in-memory.users[0].roles[0] | string | yes | | set the value ADMIN |
|
||||
|
||||
|
||||
## Quart Manager Persistence Lib
|
||||
## Quart Manager Starter Persistence Lib
|
||||
|
||||
By default, Quartz Manager runs with a `org.quartz.simpl.RAMJobStore` that stores your managed scheduler in memory. The RAMJobStore is the simplest store and also the most performant. However it comes with the drawback that all scheduling data are lost if your applications ends or crashes. In case of a restarting of your app, you can't resume the scheduler from the point it stopped.
|
||||
Import the Quartz Manager Persistence Module if you want to persist your scheduler data.
|
||||
The pre-requesite is the availability of Postgresql database where Quartz Manager can store its information. You have to provide it a bare database and a postresql user granted for DDL and DML queries. About the DDL, consider that Quartz Manager Persistence will create all tables it needs to work at the bootstrap.
|
||||
By default, Quartz Manager runs with a `org.quartz.simpl.RAMJobStore` that stores your managed scheduler in memory. The RAMJobStore is the simplest store and also the most performant. However it comes with the drawback that all scheduling data are lost if your applications ends or crashes. In case of a restarting of your app, you can't resume the scheduler from the point it stopped.
|
||||
Import the Quartz Manager Persistence Module if you want to persist your scheduler data.
|
||||
The pre-requesite is the availability of Postgresql database where Quartz Manager can store its information. You have to provide it a bare database and a postresql user granted for DDL and DML queries. About the DDL, consider that Quartz Manager Persistence will create all tables, it needs to work, at the bootstrap.
|
||||
|
||||
### Dependency
|
||||
|
||||
@@ -217,14 +242,14 @@ The pre-requesite is the availability of Postgresql database where Quartz Manage
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-persistence</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<version>4.0.9</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
#### Gradle
|
||||
|
||||
```
|
||||
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-persistence', version: '4.0.0'
|
||||
compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-persistence', version: '4.0.9'
|
||||
```
|
||||
|
||||
### Quartz Manager Persistence Lib - App Props
|
||||
@@ -239,13 +264,14 @@ compile group: 'it.fabioformosa.quartz-manager', name: 'quartz-manager-starter-p
|
||||
|
||||
## Examples
|
||||
|
||||
You can find some examples of different scenarios of projects which import Quartz Manager at the repository [quartz-manager-use-cases] (https://github.com/fabioformosa/quartz-manager-use-cases)
|
||||
* *simply-spring* - tipical scenario in which you create a minimal spring project from scratch dedicated to launch your scheduled jobs. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager security.
|
||||
* *simply-spring-no-security* - as simple-spring, without the security
|
||||
* *existing-security-cookie-based* - It demonstrates how Quartz Manager stays under the security of your project, in case of an auth model based on cookies
|
||||
* *existing-security-header-based* - It demonstrates how Quartz Manager Security can coexists with another Spring Security Config present in your project
|
||||
* *existing-quartz* - It demonstrates how to Quartz Manager can coexist with a Quartz instance already present in your project
|
||||
* *existing-quartz-and-storage* - It demonstrates how to Quartz Manager Persistence can coexist with a Quartz instance already present in your project
|
||||
You can find some examples of different scenarios of projects which import Quartz Manager at the repository [quartz-manager-use-cases](https://github.com/fabioformosa/quartz-manager-use-cases)
|
||||
* *simply-spring* - tipical scenario in which you create a minimal spring project from scratch dedicated to launch your scheduled jobs. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Security.
|
||||
* *simply-spring-no-security* - as simple-spring, without the security. Imported libraries: Quartz Manager API, Quartz Manager UI.
|
||||
* *existing-security-cookie-based* - It demonstrates how Quartz Manager stays under the security of your project, in case of an auth model based on cookies. Imported libraries: Quartz Manager API, Quartz Manager UI.
|
||||
* *existing-security-header-based* - It demonstrates how Quartz Manager Security can coexists with another Spring Security Config present in your project. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Security.
|
||||
* *existing-quartz* - It demonstrates how to Quartz Manager can coexist with a Quartz instance already present in your project Imported libraries: Quartz Manager API, Quartz Manager UI.
|
||||
* *existing-quartz-and-storage* - It demonstrates how to Quartz Manager Persistence can coexist with a Quartz instance already present in your project. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Persistence.
|
||||
* *with-persistence* - It demonstrates how to import the Quartz Manager Persistence and get created the quartz tables automatically at the bootstrap
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
45
cloudbuild.yaml
Normal file
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
|
||||
214
quartz-manager-frontend/.eslintrc.json
Normal file
214
quartz-manager-frontend/.eslintrc.json
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
👋 Hi! This file was autogenerated by tslint-to-eslint-config.
|
||||
https://github.com/typescript-eslint/tslint-to-eslint-config
|
||||
|
||||
It represents the closest reasonable ESLint configuration to this
|
||||
project's original TSLint configuration.
|
||||
|
||||
We recommend eventually switching this configuration to extend from
|
||||
the recommended rulesets in typescript-eslint.
|
||||
https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md
|
||||
|
||||
Happy linting! 💖
|
||||
*/
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "tsconfig.json",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"eslint-plugin-import",
|
||||
"@angular-eslint/eslint-plugin",
|
||||
"@typescript-eslint",
|
||||
"@typescript-eslint/tslint"
|
||||
],
|
||||
"root": true,
|
||||
"rules": {
|
||||
"@angular-eslint/component-class-suffix": "error",
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/directive-class-suffix": "error",
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/no-host-metadata-property": "error",
|
||||
"@angular-eslint/no-input-rename": "error",
|
||||
"@angular-eslint/no-inputs-metadata-property": "error",
|
||||
"@angular-eslint/no-output-rename": "error",
|
||||
"@angular-eslint/no-outputs-metadata-property": "error",
|
||||
"@angular-eslint/use-lifecycle-interface": "error",
|
||||
"@angular-eslint/use-pipe-transform-interface": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"@typescript-eslint/dot-notation": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"off",
|
||||
{
|
||||
"accessibility": "explicit"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/indent": "off",
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"off",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/member-ordering": "error",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "variable",
|
||||
"format": [
|
||||
"camelCase",
|
||||
"UPPER_CASE"
|
||||
],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-inferrable-types": [
|
||||
"error",
|
||||
{
|
||||
"ignoreParameters": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-shadow": [
|
||||
"error",
|
||||
{
|
||||
"hoist": "all"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-expressions": "error",
|
||||
"@typescript-eslint/prefer-function-type": "error",
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"@typescript-eslint/semi": [
|
||||
"off",
|
||||
null
|
||||
],
|
||||
"@typescript-eslint/tslint/config": [
|
||||
"error",
|
||||
{
|
||||
"rules": {
|
||||
"import-spacing": true,
|
||||
"invoke-injectable": true,
|
||||
"no-access-missing-member": true,
|
||||
"templates-use-public": true,
|
||||
"whitespace": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/type-annotation-spacing": "off",
|
||||
"@typescript-eslint/unified-signatures": "error",
|
||||
"brace-style": [
|
||||
"error",
|
||||
"1tbs"
|
||||
],
|
||||
"curly": "error",
|
||||
"dot-notation": "off",
|
||||
"eol-last": "off",
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"smart"
|
||||
],
|
||||
"guard-for-in": "error",
|
||||
"id-denylist": "off",
|
||||
"id-match": "off",
|
||||
"import/no-deprecated": "warn",
|
||||
"indent": "off",
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
"code": 140
|
||||
}
|
||||
],
|
||||
"no-bitwise": "error",
|
||||
"no-caller": "error",
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"log",
|
||||
"warn",
|
||||
"dir",
|
||||
"timeLog",
|
||||
"assert",
|
||||
"clear",
|
||||
"count",
|
||||
"countReset",
|
||||
"group",
|
||||
"groupEnd",
|
||||
"table",
|
||||
"dirxml",
|
||||
"error",
|
||||
"groupCollapsed",
|
||||
"Console",
|
||||
"profile",
|
||||
"profileEnd",
|
||||
"timeStamp",
|
||||
"context"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-debugger": "error",
|
||||
"no-empty": "off",
|
||||
"no-empty-function": "off",
|
||||
"no-eval": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-restricted-imports": "error",
|
||||
"no-shadow": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-labels": "error",
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"quotes": "off",
|
||||
"radix": "error",
|
||||
"semi": "off",
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"markers": [
|
||||
"/"
|
||||
]
|
||||
}
|
||||
],
|
||||
"valid-typeof": "error"
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
1
quartz-manager-frontend/.gitignore
vendored
1
quartz-manager-frontend/.gitignore
vendored
@@ -40,3 +40,4 @@ testem.log
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
/.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,7 +3,7 @@
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-spring-starter": {
|
||||
"quartz-manager-ui": {
|
||||
"root": "",
|
||||
"prefix": "qrzmng",
|
||||
"sourceRoot": "src",
|
||||
@@ -18,6 +18,9 @@
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"allowedCommonJsDependencies": [
|
||||
"stompjs", "sockjs-client", "moment", "angular2-uuid"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico"
|
||||
@@ -28,6 +31,14 @@
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
@@ -38,7 +49,6 @@
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
@@ -56,33 +66,30 @@
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "angular-spring-starter:build"
|
||||
"browserTarget": "quartz-manager-ui:build:development"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "angular-spring-starter:build:production"
|
||||
"browserTarget": "quartz-manager-ui:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "angular-spring-starter:build"
|
||||
"browserTarget": "quartz-manager-ui:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": []
|
||||
"eslintConfig": ".eslintrc.json",
|
||||
"lintFilePatterns": ["**/*.spec.ts", "**/*.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"angular-spring-starter-e2e": {
|
||||
"quartz-manager-ui-e2e": {
|
||||
"root": "e2e",
|
||||
"sourceRoot": "e2e",
|
||||
"projectType": "application",
|
||||
@@ -91,7 +98,7 @@
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "./protractor.conf.js",
|
||||
"devServerTarget": "angular-spring-starter:serve"
|
||||
"devServerTarget": "quartz-manager-ui:serve"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
@@ -106,7 +113,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "angular-spring-starter",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "qrzmng",
|
||||
@@ -115,5 +121,8 @@
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "qrzmng"
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
import 'jest-preset-angular';
|
||||
import 'jest-preset-angular/setup-jest';
|
||||
|
||||
39860
quartz-manager-frontend/package-lock.json
generated
39860
quartz-manager-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,67 +5,84 @@
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
"build": "ng build --prod",
|
||||
"build": "ng build --configuration production",
|
||||
"test": "jest",
|
||||
"lint": "ng lint",
|
||||
"lint:sonar": "eslint --no-eslintrc -c .eslintrc.sonar.json \"src/**/*.ts\"",
|
||||
"lint:sonar:fix": "eslint --no-eslintrc -c .eslintrc.sonar.json \"src/**/*.ts\" --fix",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-material-components/datetime-picker": "2.0.4",
|
||||
"@angular-material-components/moment-adapter": "2.0.2",
|
||||
"@angular/animations": "9.1.4",
|
||||
"@angular/cdk": "9.2.1",
|
||||
"@angular/common": "9.1.4",
|
||||
"@angular/compiler": "9.1.4",
|
||||
"@angular/core": "9.1.4",
|
||||
"@angular/flex-layout": "9.0.0-beta.29",
|
||||
"@angular/forms": "9.1.4",
|
||||
"@angular/material": "9.2.1",
|
||||
"@angular/platform-browser": "9.1.4",
|
||||
"@angular/platform-browser-dynamic": "9.1.4",
|
||||
"@angular/platform-server": "9.1.4",
|
||||
"@angular/router": "9.1.4",
|
||||
"@auth0/angular-jwt": "^4.0.0",
|
||||
"@angular-material-components/datetime-picker": "8.0.0",
|
||||
"@angular-material-components/moment-adapter": "8.0.0",
|
||||
"@angular/animations": "14.2.12",
|
||||
"@angular/cdk": "^14.0.1",
|
||||
"@angular/common": "14.2.12",
|
||||
"@angular/compiler": "14.2.12",
|
||||
"@angular/core": "14.2.12",
|
||||
"@angular/flex-layout": "14.0.0-beta.41",
|
||||
"@angular/forms": "14.2.12",
|
||||
"@angular/material": "^14.0.1",
|
||||
"@angular/platform-browser": "14.2.12",
|
||||
"@angular/platform-browser-dynamic": "14.2.12",
|
||||
"@angular/platform-server": "14.2.12",
|
||||
"@angular/router": "14.2.12",
|
||||
"@auth0/angular-jwt": "5.1.0",
|
||||
"@fortawesome/fontawesome": "^1.1.4",
|
||||
"@fortawesome/fontawesome-free-regular": "^5.0.8",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.8",
|
||||
"@stomp/ng2-stompjs": "^0.6.3",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@stomp/rx-stomp": "1.2.0",
|
||||
"core-js": "2.5.1",
|
||||
"hammerjs": "2.0.8",
|
||||
"moment": "^2.29.1",
|
||||
"net": "^1.0.2",
|
||||
"rxjs": "6.5.5",
|
||||
"sockjs-client": "^1.1.1",
|
||||
"stompjs": "^2.3.3",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.10.2"
|
||||
"tslib": "~2.4.1",
|
||||
"zone.js": "~0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.901.4",
|
||||
"@angular-devkit/core": "^9.1.4",
|
||||
"@angular/cli": "9.1.4",
|
||||
"@angular/compiler-cli": "9.1.4",
|
||||
"@angular/language-service": "9.1.4",
|
||||
"@angular-devkit/build-angular": "14.2.10",
|
||||
"@angular-devkit/core": "^14.2.10",
|
||||
"@angular-eslint/builder": "14.4.0",
|
||||
"@angular-eslint/eslint-plugin": "14.4.0",
|
||||
"@angular-eslint/eslint-plugin-template": "14.4.0",
|
||||
"@angular-eslint/schematics": "14.4.0",
|
||||
"@angular-eslint/template-parser": "14.4.0",
|
||||
"@angular/cli": "14.2.10",
|
||||
"@angular/compiler-cli": "14.2.12",
|
||||
"@angular/language-service": "14.2.12",
|
||||
"@types/hammerjs": "2.0.34",
|
||||
"@types/jasmine": "2.5.54",
|
||||
"@types/jasminewd2": "2.0.3",
|
||||
"@types/jest": "28.1.1",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^5.1.2",
|
||||
"jasmine-core": "2.6.4",
|
||||
"jasmine-spec-reporter": "4.1.1",
|
||||
"jest": "^26.0.1",
|
||||
"jest-preset-angular": "^8.2.0",
|
||||
"karma": "1.7.1",
|
||||
"karma-chrome-launcher": "2.1.1",
|
||||
"karma-cli": "1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "1.3.0",
|
||||
"karma-jasmine": "1.1.0",
|
||||
"karma-jasmine-html-reporter": "0.2.2",
|
||||
"protractor": "5.1.2",
|
||||
"ts-node": "3.0.6",
|
||||
"tslint": "5.7.0",
|
||||
"typescript": "3.8.3"
|
||||
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "^5.46.0",
|
||||
"@typescript-eslint/parser": "5.43.0",
|
||||
"codelyzer": "~6.0.2",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-sonarjs": "^0.16.0",
|
||||
"jasmine-core": "~4.5.0",
|
||||
"jasmine-spec-reporter": "~7.0.0",
|
||||
"jest": "28.1.3",
|
||||
"jest-preset-angular": "~12.2.3",
|
||||
"karma": "~6.4.1",
|
||||
"karma-chrome-launcher": "~3.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"prettier": "^2.8.1",
|
||||
"prettier-eslint": "^15.0.1",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.6.4"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-preset-angular",
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
|
||||
declare var __karma__: any;
|
||||
declare var require: any;
|
||||
declare let __karma__: any;
|
||||
declare let require: any;
|
||||
|
||||
// Prevent Karma from running prematurely.
|
||||
__karma__.loaded = function () {};
|
||||
|
||||
@@ -46,7 +46,7 @@ export const routes: Routes = [
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: false
|
||||
initialNavigation: 'disabled'
|
||||
})],
|
||||
exports: [RouterModule],
|
||||
providers: []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TestBed, async } from '@angular/core/testing';
|
||||
import {TestBed, async, waitForAsync} from '@angular/core/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from './services';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
||||
@@ -43,7 +43,8 @@ import {
|
||||
SchedulerControlComponent,
|
||||
LogsPanelComponent,
|
||||
ProgressPanelComponent,
|
||||
TriggerListComponent
|
||||
TriggerListComponent,
|
||||
SimpleTriggerConfigComponent
|
||||
} from './components';
|
||||
|
||||
import {
|
||||
@@ -52,14 +53,13 @@ import {
|
||||
UserService,
|
||||
SchedulerService,
|
||||
ConfigService,
|
||||
ProgressWebsocketService,
|
||||
LogsWebsocketService,
|
||||
getHtmlBaseUrl,
|
||||
LogsRxWebsocketService,
|
||||
ProgressRxWebsocketService,
|
||||
TriggerService
|
||||
} from './services';
|
||||
import { ForbiddenComponent } from './views/forbidden/forbidden.component';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import {SimpleTriggerConfigComponent} from './components/simple-trigger-config';
|
||||
import JobService from './services/job.service';
|
||||
import {GenericErrorComponent} from './views/error/genericError.component';
|
||||
|
||||
@@ -67,31 +67,6 @@ export function initUserFactory(userService: UserService) {
|
||||
return () => userService.fetchLoggedUser();
|
||||
}
|
||||
|
||||
|
||||
// const stompConfig: StompConfig = {
|
||||
// // Which server?
|
||||
// url: 'ws://localhost:8080/quartz-manager/progress',
|
||||
|
||||
// // Headers
|
||||
// // Typical keys: login, passcode, host
|
||||
// headers: {
|
||||
// login: 'admin',
|
||||
// passcode: 'admin'
|
||||
// },
|
||||
|
||||
// // How often to heartbeat?
|
||||
// // Interval in milliseconds, set to 0 to disable
|
||||
// heartbeat_in: 0, // Typical value 0 - disabled
|
||||
// heartbeat_out: 20000, // Typical value 20000 - every 20 seconds
|
||||
// // Wait in milliseconds before attempting auto reconnect
|
||||
// // Set to 0 to disable
|
||||
// // Typical value 5000 (5 seconds)
|
||||
// reconnect_delay: 5000,
|
||||
|
||||
// // Will log diagnostics on console
|
||||
// debug: true
|
||||
// };
|
||||
|
||||
export function jwtOptionsFactory(apiService: ApiService) {
|
||||
return {
|
||||
tokenGetter: () => {
|
||||
@@ -168,19 +143,13 @@ export function jwtOptionsFactory(apiService: ApiService) {
|
||||
SchedulerService,
|
||||
JobService,
|
||||
TriggerService,
|
||||
ProgressWebsocketService,
|
||||
LogsWebsocketService,
|
||||
ProgressRxWebsocketService,
|
||||
LogsRxWebsocketService,
|
||||
AuthService,
|
||||
ApiService,
|
||||
UserService,
|
||||
ConfigService,
|
||||
MatIconRegistry
|
||||
// StompService,
|
||||
// ServerSocket
|
||||
// {
|
||||
// provide: StompConfig,
|
||||
// useValue: stompConfig
|
||||
// }
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './logs-panel';
|
||||
export * from './scheduler-control';
|
||||
export * from './progress-panel';
|
||||
export * from './trigger-list';
|
||||
export * from './simple-trigger-config';
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
<mat-card-subtitle><b>JOB LOGS</b></mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content style="position: relative; height: calc(100% - 3em);">
|
||||
<div *ngIf="!logs || logs.length == 0" fxLayout="row" fxFlexAlign="center stretch" style="text-align: center">
|
||||
<div fxFill style="height: 100%;">
|
||||
<img src="assets/image/logs.svg" alt="no logs" width="320" style="margin-top: 6em;" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="logs" style="overflow-y: auto; position: absolute; left: 0; right: 0; top: 0; bottom: 0; overflow: auto;">
|
||||
<div *ngIf="!selectedTriggerName && (!logs || logs.length == 0)" fxLayout="row" fxFlexAlign="center stretch" style="text-align: center">
|
||||
<div fxFill style="height: 100%;">
|
||||
<img src="assets/image/logs.svg" alt="no logs" width="320" style="margin-top: 6em;" />
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="isWaitingForLogs()" class="waitingLogs" fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="12px">
|
||||
<mat-spinner diameter="36"></mat-spinner>
|
||||
<div>Waiting for logs from {{selectedTriggerName}}...</div>
|
||||
</div>
|
||||
<div id="logs" style="overflow-y: auto; position: absolute; left: 0; right: 0; top: 0; bottom: 0; overflow: auto;">
|
||||
<div
|
||||
*ngFor = "let log of logs; let first = first" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start" fxLayoutGap="10px">
|
||||
<div fxFlex="0 1 300px">
|
||||
|
||||
@@ -9,11 +9,18 @@
|
||||
color: gold;
|
||||
}
|
||||
|
||||
#logs{
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* ===== Scrollbar CSS ===== */
|
||||
#logs{
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.waitingLogs {
|
||||
color: #6b7280;
|
||||
height: 100%;
|
||||
min-height: 180px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ===== Scrollbar CSS ===== */
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import {Subject} from 'rxjs';
|
||||
import {LogsPanelComponent} from './logs-panel.component';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {jest} from '@jest/globals';
|
||||
|
||||
describe('LogsPanelComponent', () => {
|
||||
|
||||
it('should subscribe to the selected trigger logs topic', () => {
|
||||
const messages = new Subject<any>();
|
||||
const logsRxWebsocketService = {
|
||||
watch: jest.fn(() => messages.asObservable())
|
||||
};
|
||||
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
|
||||
expect(logsRxWebsocketService.watch).toHaveBeenCalledWith('/topic/logs/trigger-1');
|
||||
expect(component.selectedTriggerName).toEqual('trigger-1');
|
||||
expect(component.isWaitingForLogs()).toBeTruthy();
|
||||
|
||||
const logRecord = {
|
||||
date: new Date(),
|
||||
type: 'INFO',
|
||||
message: 'job completed',
|
||||
threadName: 'worker-1'
|
||||
};
|
||||
messages.next({body: JSON.stringify(logRecord)});
|
||||
|
||||
expect(component.logs[0]).toEqual({
|
||||
time: logRecord.date.toISOString(),
|
||||
type: 'INFO',
|
||||
msg: 'job completed',
|
||||
threadName: 'worker-1'
|
||||
});
|
||||
expect(component.isWaitingForLogs()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should unsubscribe from the previous topic when the trigger changes', () => {
|
||||
const firstMessages = new Subject<any>();
|
||||
const secondMessages = new Subject<any>();
|
||||
const logsRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
.mockReturnValueOnce(secondMessages.asObservable())
|
||||
};
|
||||
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
const firstSubscription = component.topicSubscription;
|
||||
jest.spyOn(firstSubscription, 'unsubscribe');
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-2', null);
|
||||
|
||||
expect(firstSubscription.unsubscribe).toHaveBeenCalled();
|
||||
expect(logsRxWebsocketService.watch).toHaveBeenCalledWith('/topic/logs/trigger-2');
|
||||
});
|
||||
|
||||
it('should clear logs when the trigger changes', () => {
|
||||
const firstMessages = new Subject<any>();
|
||||
const secondMessages = new Subject<any>();
|
||||
const logsRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
.mockReturnValueOnce(secondMessages.asObservable())
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
};
|
||||
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
firstMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})});
|
||||
expect(component.logs.length).toEqual(1);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-2', null);
|
||||
expect(component.logs).toEqual([]);
|
||||
expect(component.selectedTriggerName).toEqual('trigger-2');
|
||||
expect(component.isWaitingForLogs()).toBeTruthy();
|
||||
|
||||
secondMessages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'second log', threadName: 'worker-2'})});
|
||||
expect(component.logs.length).toEqual(1);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
expect(component.logs).toEqual([]);
|
||||
expect(component.selectedTriggerName).toEqual('trigger-1');
|
||||
expect(component.isWaitingForLogs()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should clear logs when no trigger is selected', () => {
|
||||
const messages = new Subject<any>();
|
||||
const logsRxWebsocketService = {
|
||||
watch: jest.fn(() => messages.asObservable())
|
||||
};
|
||||
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
messages.next({body: JSON.stringify({date: new Date(), type: 'INFO', message: 'first log', threadName: 'worker-1'})});
|
||||
|
||||
component.triggerKey = null;
|
||||
|
||||
expect(component.logs).toEqual([]);
|
||||
expect(component.selectedTriggerName).toBeNull();
|
||||
expect(component.isWaitingForLogs()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should ignore destroy when no topic was selected', () => {
|
||||
const logsRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
};
|
||||
const component = new LogsPanelComponent(logsRxWebsocketService as any, null);
|
||||
|
||||
expect(() => component.ngOnDestroy()).not.toThrow();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,42 +1,84 @@
|
||||
import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core';
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
|
||||
import {ApiService} from '../../services';
|
||||
import {LogsRxWebsocketService} from '../../services/logs.rx-websocket.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
|
||||
import {LogsWebsocketService, ApiService} from '../../services';
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'logs-panel',
|
||||
templateUrl: './logs-panel.component.html',
|
||||
styleUrls: ['./logs-panel.component.scss']
|
||||
})
|
||||
export class LogsPanelComponent implements OnInit {
|
||||
export class LogsPanelComponent implements OnInit, OnDestroy {
|
||||
|
||||
MAX_LOGS = 30;
|
||||
|
||||
logs = new Array();
|
||||
logs = new Array();
|
||||
|
||||
selectedTriggerName: string;
|
||||
|
||||
topicSubscription;
|
||||
|
||||
private selectedTriggerKey: TriggerKey;
|
||||
|
||||
constructor(
|
||||
private logsWebsocketService: LogsWebsocketService,
|
||||
private logsRxWebsocketService: LogsRxWebsocketService,
|
||||
private apiService: ApiService
|
||||
) {
|
||||
}
|
||||
|
||||
@Input()
|
||||
set triggerKey(triggerKey: TriggerKey) {
|
||||
if (!triggerKey || !triggerKey.name) {
|
||||
this._unsubscribeFromTopic();
|
||||
this.selectedTriggerKey = null;
|
||||
this.selectedTriggerName = null;
|
||||
this._resetLogs();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedTriggerKey?.name === triggerKey.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._resetLogs();
|
||||
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
||||
this.selectedTriggerName = triggerKey.name;
|
||||
this._subscribeToTheTopic(this.selectedTriggerKey);
|
||||
}
|
||||
|
||||
isWaitingForLogs = (): boolean => !!this.selectedTriggerName && (!this.logs || this.logs.length === 0);
|
||||
|
||||
ngOnInit() {
|
||||
const obs = this.logsWebsocketService.getObservable()
|
||||
obs.subscribe({
|
||||
'next': this.onNewLogMsg,
|
||||
'error': (err) => {
|
||||
console.log(err)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onNewLogMsg = (receivedMsg) => {
|
||||
if (receivedMsg.type === 'SUCCESS') {
|
||||
this._showNewLog(receivedMsg.message);
|
||||
} else if (receivedMsg.type === 'ERROR') {
|
||||
this._refreshSession();
|
||||
} // if websocket has been closed for session expiration, try to refresh it
|
||||
};
|
||||
private _subscribeToTheTopic = (triggerKey: TriggerKey) => {
|
||||
this._unsubscribeFromTopic();
|
||||
this.topicSubscription = this.logsRxWebsocketService.watch(`/topic/logs/${triggerKey.name}`)
|
||||
.pipe(map((msg: any) => JSON.parse(msg.body)))
|
||||
.subscribe(this._showNewLog, (err) => {
|
||||
console.log(err);
|
||||
// TODO in case of 401
|
||||
// this.apiService.get('/quartz-manager/session/refresh');
|
||||
});
|
||||
};
|
||||
|
||||
ngOnDestroy() {
|
||||
this._unsubscribeFromTopic();
|
||||
}
|
||||
|
||||
private _unsubscribeFromTopic() {
|
||||
if (this.topicSubscription) {
|
||||
this.topicSubscription.unsubscribe();
|
||||
this.topicSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _resetLogs() {
|
||||
this.logs = [];
|
||||
}
|
||||
|
||||
_showNewLog = (logRecord) => {
|
||||
if (this.logs.length > this.MAX_LOGS) {
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<mat-card style="padding-bottom: 0">
|
||||
<mat-card style="padding-bottom: 0" [ngClass]="{'progress-updated': progressUpdated}">
|
||||
<mat-card-header>
|
||||
<mat-card-subtitle><b>JOB PROGRESS</b></mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div id="progressBarBox" *ngIf="progress.percentage !== -1">
|
||||
<div id="progressBarBox" *ngIf="progress.percentage !== -1">
|
||||
<mat-progress-bar mode="determinate" value="{{progress.percentage}}"></mat-progress-bar>
|
||||
{{percentageStr}}
|
||||
</div>
|
||||
|
||||
@@ -31,3 +31,21 @@
|
||||
.fireBoxContent{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-updated {
|
||||
animation: progressUpdatePulse 700ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes progressUpdatePulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(63, 81, 181, 0.35);
|
||||
}
|
||||
|
||||
45% {
|
||||
box-shadow: 0 0 0 6px rgba(63, 81, 181, 0.16);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(63, 81, 181, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import {Subject} from 'rxjs';
|
||||
import {ProgressPanelComponent} from './progress-panel.component';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {jest} from '@jest/globals';
|
||||
|
||||
describe('ProgressPanelComponent', () => {
|
||||
|
||||
it('should subscribe to the selected trigger progress topic', () => {
|
||||
jest.useFakeTimers();
|
||||
const messages = new Subject<any>();
|
||||
const progressRxWebsocketService = {
|
||||
watch: jest.fn(() => messages.asObservable())
|
||||
};
|
||||
const component = new ProgressPanelComponent(progressRxWebsocketService as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
|
||||
expect(progressRxWebsocketService.watch).toHaveBeenCalledWith('/topic/progress/trigger-1');
|
||||
|
||||
messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(component.progress.percentage).toEqual(75);
|
||||
expect(component.percentageStr).toEqual('75%');
|
||||
expect(component.progressUpdated).toBeTruthy();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should unsubscribe from the previous topic when the trigger changes', () => {
|
||||
const firstMessages = new Subject<any>();
|
||||
const secondMessages = new Subject<any>();
|
||||
const progressRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
.mockReturnValueOnce(secondMessages.asObservable())
|
||||
};
|
||||
const component = new ProgressPanelComponent(progressRxWebsocketService as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
const firstSubscription = component.topicSubscription;
|
||||
jest.spyOn(firstSubscription, 'unsubscribe');
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-2', null);
|
||||
|
||||
expect(firstSubscription.unsubscribe).toHaveBeenCalled();
|
||||
expect(progressRxWebsocketService.watch).toHaveBeenCalledWith('/topic/progress/trigger-2');
|
||||
});
|
||||
|
||||
it('should reset progress when the trigger changes', () => {
|
||||
const firstMessages = new Subject<any>();
|
||||
const secondMessages = new Subject<any>();
|
||||
const progressRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
.mockReturnValueOnce(secondMessages.asObservable())
|
||||
.mockReturnValueOnce(firstMessages.asObservable())
|
||||
};
|
||||
const component = new ProgressPanelComponent(progressRxWebsocketService as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
firstMessages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
|
||||
expect(component.progress.percentage).toEqual(75);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-2', null);
|
||||
expect(component.progress.percentage).toEqual(-1);
|
||||
expect(component.percentageStr).toBeNull();
|
||||
expect(component.progressUpdated).toBeFalsy();
|
||||
|
||||
secondMessages.next({body: JSON.stringify({percentage: 20, timesTriggered: 1})});
|
||||
expect(component.progress.percentage).toEqual(20);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
expect(component.progress.percentage).toEqual(-1);
|
||||
});
|
||||
|
||||
it('should reset progress when no trigger is selected', () => {
|
||||
const messages = new Subject<any>();
|
||||
const progressRxWebsocketService = {
|
||||
watch: jest.fn(() => messages.asObservable())
|
||||
};
|
||||
const component = new ProgressPanelComponent(progressRxWebsocketService as any);
|
||||
|
||||
component.triggerKey = new TriggerKey('trigger-1', null);
|
||||
messages.next({body: JSON.stringify({percentage: 75, timesTriggered: 3})});
|
||||
|
||||
component.triggerKey = null;
|
||||
|
||||
expect(component.progress.percentage).toEqual(-1);
|
||||
expect(component.percentageStr).toBeNull();
|
||||
expect(component.progressUpdated).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should ignore destroy when no topic was selected', () => {
|
||||
const progressRxWebsocketService = {
|
||||
watch: jest.fn()
|
||||
};
|
||||
const component = new ProgressPanelComponent(progressRxWebsocketService as any);
|
||||
|
||||
expect(() => component.ngOnDestroy()).not.toThrow();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,84 +1,91 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'
|
||||
import {ProgressWebsocketService, QuartzManagerWebsocketMessage} from '../../services';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core'
|
||||
import TriggerFiredBundle from '../../model/trigger-fired-bundle.model';
|
||||
// import {Message} from '@stomp/stompjs';
|
||||
|
||||
// import { Subscription } from 'rxjs/Subscription';
|
||||
// import {StompService} from '@stomp/ng2-stompjs';
|
||||
|
||||
// import { QueueingSubject } from 'queueing-subject'
|
||||
// import websocketConnect from 'rxjs-websockets'
|
||||
// import 'rxjs/add/operator/share'
|
||||
// import {ServerSocket} from '../../services/qz.socket.service'
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {ProgressRxWebsocketService} from '../../services/progress.rx-websocket.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'progress-panel',
|
||||
templateUrl: './progress-panel.component.html',
|
||||
styleUrls: ['./progress-panel.component.scss']
|
||||
})
|
||||
export class ProgressPanelComponent implements OnInit {
|
||||
|
||||
progress: TriggerFiredBundle = new TriggerFiredBundle();
|
||||
percentageStr: string;
|
||||
|
||||
// // Stream of messages
|
||||
// private subscription: Subscription;
|
||||
// public messages: Observable<Message>;
|
||||
// // Subscription status
|
||||
// public subscribed: boolean;
|
||||
// // Array of historic message (bodies)
|
||||
// public mq: Array<string> = [];
|
||||
export class ProgressPanelComponent implements OnInit, OnDestroy {
|
||||
|
||||
progress: TriggerFiredBundle = ProgressPanelComponent._buildEmptyProgress();
|
||||
percentageStr: string;
|
||||
progressUpdated = false;
|
||||
|
||||
topicSubscription;
|
||||
private selectedTriggerKey: TriggerKey;
|
||||
|
||||
constructor(
|
||||
private progressWebsocketService: ProgressWebsocketService,
|
||||
// private _stompService: StompService,
|
||||
// private serverSocket : ServerSocket
|
||||
private progressRxWebsocketService: ProgressRxWebsocketService
|
||||
) { }
|
||||
|
||||
onNewProgressMsg = (receivedMsg: QuartzManagerWebsocketMessage) => {
|
||||
if (receivedMsg.type === 'SUCCESS') {
|
||||
const newStatus = receivedMsg.message;
|
||||
this.progress = newStatus;
|
||||
this.percentageStr = this.progress.percentage + '%';
|
||||
}
|
||||
}
|
||||
@Input()
|
||||
set triggerKey(triggerKey: TriggerKey) {
|
||||
if (!triggerKey || !triggerKey.name) {
|
||||
this._unsubscribeFromTopic();
|
||||
this.selectedTriggerKey = null;
|
||||
this._resetProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedTriggerKey?.name === triggerKey.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._resetProgress();
|
||||
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
||||
this._subscribeToTheTopic(this.selectedTriggerKey);
|
||||
}
|
||||
|
||||
private _subscribeToTheTopic = (triggerKey: TriggerKey) => {
|
||||
this._unsubscribeFromTopic();
|
||||
this.topicSubscription = this.progressRxWebsocketService.watch(`/topic/progress/${triggerKey.name}`)
|
||||
.pipe(map((msg: any) => JSON.parse(msg.body)))
|
||||
.subscribe(this.onNewProgressMsg, (err) => {
|
||||
console.log(err);
|
||||
// TODO in case of 401
|
||||
// this.apiService.get('/quartz-manager/session/refresh');
|
||||
});
|
||||
};
|
||||
|
||||
onNewProgressMsg = (receivedMsg) => {
|
||||
this.progress = receivedMsg;
|
||||
this.percentageStr = this.progress.percentage + '%';
|
||||
this._markProgressUpdated();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const obs = this.progressWebsocketService.getObservable()
|
||||
obs.subscribe({
|
||||
'next' : this.onNewProgressMsg,
|
||||
'error' : (err) => {console.log(err)}
|
||||
});
|
||||
|
||||
// this.subscribed = false;
|
||||
// this.subscribe();
|
||||
|
||||
// this.serverSocket.connect()
|
||||
// this.socketSubscription = this.serverSocket.messages.subscribe((message: string) => {
|
||||
// console.log('received message from server: ', message)
|
||||
// })
|
||||
}
|
||||
|
||||
// public subscribe() {
|
||||
// if (this.subscribed) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Stream of messages
|
||||
// this.messages = this._stompService.subscribe('/topic/progress');
|
||||
|
||||
// // Subscribe a function to be run on_next message
|
||||
// this.subscription = this.messages.subscribe(this.on_next);
|
||||
|
||||
// this.subscribed = true;
|
||||
// }
|
||||
|
||||
// public on_next = (message: Message) => {
|
||||
// this.mq.push(message.body + '\n');
|
||||
// console.log(message);
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._unsubscribeFromTopic();
|
||||
}
|
||||
|
||||
private _unsubscribeFromTopic() {
|
||||
if (this.topicSubscription) {
|
||||
this.topicSubscription.unsubscribe();
|
||||
this.topicSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _resetProgress() {
|
||||
this.progress = ProgressPanelComponent._buildEmptyProgress();
|
||||
this.percentageStr = null;
|
||||
this.progressUpdated = false;
|
||||
}
|
||||
|
||||
private _markProgressUpdated() {
|
||||
this.progressUpdated = false;
|
||||
setTimeout(() => this.progressUpdated = true);
|
||||
}
|
||||
|
||||
private static _buildEmptyProgress() {
|
||||
const progress = new TriggerFiredBundle();
|
||||
progress.percentage = -1;
|
||||
return progress;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
import {SchedulerControlComponent} from './scheduler-control.component';
|
||||
import {ApiService, ConfigService, SchedulerService, UserService} from '../../services';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
@@ -13,13 +13,19 @@ import {MatDividerModule} from '@angular/material/divider';
|
||||
|
||||
describe('SchedulerControlComponent', () => {
|
||||
|
||||
const schedulerUrl = '/quartz-manager/scheduler';
|
||||
const schedulerButtonSelector = '#schedulerControllerBtn';
|
||||
const schedulerName = 'test-scheduler';
|
||||
const schedulerId = 'test-id';
|
||||
const stoppedStatus = 'STOPPED';
|
||||
|
||||
let component: SchedulerControlComponent;
|
||||
let fixture: ComponentFixture<SchedulerControlComponent>;
|
||||
|
||||
let httpClient: HttpClient;
|
||||
let httpTestingController: HttpTestingController;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatCardModule, MatDividerModule, MatIconModule, HttpClientTestingModule, RouterTestingModule],
|
||||
declarations: [SchedulerControlComponent],
|
||||
@@ -38,16 +44,16 @@ describe('SchedulerControlComponent', () => {
|
||||
|
||||
it('should display the play button at the beginning since the scheduler is stopped', () => {
|
||||
expect(component).toBeDefined();
|
||||
const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
|
||||
const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'STOPPED', []);
|
||||
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
|
||||
const mockScheduler = new Scheduler(schedulerName, schedulerId, stoppedStatus, []);
|
||||
getSchedulerReq.flush(mockScheduler);
|
||||
|
||||
expect(component.scheduler).toEqual(mockScheduler);
|
||||
expect(component.scheduler.status).toEqual('STOPPED');
|
||||
expect(component.scheduler.status).toEqual(stoppedStatus);
|
||||
fixture.detectChanges();
|
||||
|
||||
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
|
||||
const schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
|
||||
const schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
|
||||
expect(schedulerBtnDe).toBeTruthy();
|
||||
|
||||
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
|
||||
@@ -56,13 +62,13 @@ describe('SchedulerControlComponent', () => {
|
||||
|
||||
it('should switch the button to pause when the scheduler is started', () => {
|
||||
expect(component).toBeDefined();
|
||||
const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
|
||||
const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'STOPPED', []);
|
||||
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
|
||||
const mockScheduler = new Scheduler(schedulerName, schedulerId, stoppedStatus, []);
|
||||
getSchedulerReq.flush(mockScheduler);
|
||||
fixture.detectChanges();
|
||||
|
||||
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
|
||||
let schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
|
||||
let schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
|
||||
expect(schedulerBtnDe).toBeTruthy();
|
||||
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
|
||||
expect(playIconDe).toBeTruthy();
|
||||
@@ -72,7 +78,7 @@ describe('SchedulerControlComponent', () => {
|
||||
startSchedulerReq.flush(null);
|
||||
fixture.detectChanges();
|
||||
|
||||
schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
|
||||
schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
|
||||
const pauseIconDe = schedulerBtnDe.query(By.css('.fa-pause'));
|
||||
expect(pauseIconDe).toBeTruthy();
|
||||
|
||||
@@ -80,13 +86,13 @@ describe('SchedulerControlComponent', () => {
|
||||
|
||||
it('should switch the button to play when the scheduler is stopped', () => {
|
||||
expect(component).toBeDefined();
|
||||
const getSchedulerReq = httpTestingController.expectOne('/quartz-manager/scheduler');
|
||||
const mockScheduler = new Scheduler('test-scheduler', 'test-id', 'RUNNING', []);
|
||||
const getSchedulerReq = httpTestingController.expectOne(schedulerUrl);
|
||||
const mockScheduler = new Scheduler(schedulerName, schedulerId, 'RUNNING', []);
|
||||
getSchedulerReq.flush(mockScheduler);
|
||||
fixture.detectChanges();
|
||||
|
||||
const schedulerControlComponentDe: DebugElement = fixture.debugElement;
|
||||
let schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
|
||||
let schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
|
||||
expect(schedulerBtnDe).toBeTruthy();
|
||||
const pauseIconDe = schedulerBtnDe.query(By.css('.fa-pause'));
|
||||
expect(pauseIconDe).toBeTruthy();
|
||||
@@ -96,7 +102,7 @@ describe('SchedulerControlComponent', () => {
|
||||
startSchedulerReq.flush(null);
|
||||
fixture.detectChanges();
|
||||
|
||||
schedulerBtnDe = schedulerControlComponentDe.query(By.css('#schedulerControllerBtn'));
|
||||
schedulerBtnDe = schedulerControlComponentDe.query(By.css(schedulerButtonSelector));
|
||||
const playIconDe = schedulerBtnDe.query(By.css('.fa-play'));
|
||||
expect(playIconDe).toBeTruthy();
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<mat-form-field
|
||||
[appearance]="enabledTriggerForm && !trigger ? 'standard': 'none'"
|
||||
class="full-size-input">
|
||||
<mat-label>Trigger Name *</mat-label>
|
||||
<mat-label>Trigger Name</mat-label>
|
||||
<input id="triggerName"
|
||||
[readonly]="!enabledTriggerForm || trigger"
|
||||
matInput placeholder="name of the trigger (unique)"
|
||||
@@ -36,7 +36,7 @@
|
||||
[appearance]="enabledTriggerForm ? 'standard': 'none'"
|
||||
class="full-size-input"
|
||||
>
|
||||
<mat-label>Job Class *</mat-label>
|
||||
<mat-label>Job Class</mat-label>
|
||||
<mat-select id="jobClass" name="jobClass" formControlName="jobClass" [disabled]="!enabledTriggerForm">
|
||||
<mat-option *ngFor="let job of jobs" [value]="job" style="font-size: 0.8em">
|
||||
{{job}}
|
||||
@@ -53,7 +53,7 @@
|
||||
[appearance]="enabledTriggerForm ? 'standard': 'none'"
|
||||
class="full-size-input"
|
||||
>
|
||||
<mat-label>Misfire Instruction *</mat-label>
|
||||
<mat-label>Misfire Instruction</mat-label>
|
||||
<mat-select id="misfireInstruction" name="misfireInstruction" formControlName="misfireInstruction"
|
||||
[disabled]="!enabledTriggerForm" style="font-size: 0.8em">
|
||||
<mat-option value="MISFIRE_INSTRUCTION_FIRE_NOW">FIRE NOW</mat-option>
|
||||
@@ -93,9 +93,6 @@
|
||||
formControlName="startDate" name="startDate">
|
||||
<mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
|
||||
<ngx-mat-datetime-picker #startDatePicker showSpinners="true" showSeconds="true">
|
||||
<!-- [stepHour]="stepHour"-->
|
||||
<!-- [stepMinute]="stepMinute" [stepSecond]="stepSecond" [touchUi]="touchUi" [color]="color"-->
|
||||
<!-- [enableMeridian]="enableMeridian" [disableMinute]="disableMinute" [hideTime]="hideTime">-->
|
||||
</ngx-mat-datetime-picker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@@ -114,9 +111,6 @@
|
||||
>
|
||||
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
|
||||
<ngx-mat-datetime-picker #endDatePicker showSpinners="true" showSeconds="true">
|
||||
<!-- [stepHour]="stepHour"-->
|
||||
<!-- [stepMinute]="stepMinute" [stepSecond]="stepSecond" [touchUi]="touchUi" [color]="color"-->
|
||||
<!-- [enableMeridian]="enableMeridian" [disableMinute]="disableMinute" [hideTime]="hideTime">-->
|
||||
</ngx-mat-datetime-picker>
|
||||
</mat-form-field>
|
||||
<mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerPeriod.errors?.invalidTriggerPeriod" style="font-size: small">
|
||||
@@ -141,9 +135,6 @@
|
||||
repeatCount and repeatInterval must be <strong>both</strong> set or unset
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<!-- <mat-error *ngIf="simpleTriggerReactiveForm.controls.triggerRecurrence.errors?.invalidTriggerRecurrence" style="font-size: small">-->
|
||||
<!-- repeatCount and repeatInterval must be <strong>both</strong> set or unset-->
|
||||
<!-- </mat-error>-->
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {SimpleTriggerConfigComponent} from './simple-trigger-config.component';
|
||||
import {ApiService, ConfigService, CONTEXT_PATH, SchedulerService} from '../../services';
|
||||
@@ -12,7 +12,7 @@ import {FormBuilder, FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatNativeDateModule} from '@angular/material/core';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {Trigger} from '../../model/trigger.model';
|
||||
import {JobDetail} from '../../model/jobDetail.model';
|
||||
@@ -23,15 +23,20 @@ import {MisfireInstruction} from '../../model/misfire-instruction.model';
|
||||
|
||||
describe('SimpleTriggerConfig', () => {
|
||||
|
||||
const submitButtonSelector = 'form button[color="primary"]';
|
||||
const repeatIntervalSelector = '#repeatInterval';
|
||||
const testTriggerName = 'test-trigger';
|
||||
const testJobName = 'TestJob';
|
||||
|
||||
let component: SimpleTriggerConfigComponent;
|
||||
let fixture: ComponentFixture<SimpleTriggerConfigComponent>;
|
||||
|
||||
let httpClient: HttpClient;
|
||||
let httpTestingController: HttpTestingController;
|
||||
|
||||
beforeEach(async( () => {
|
||||
beforeEach(waitForAsync( () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [FormsModule, MatFormFieldModule, MatFormFieldModule, MatSelectModule, MatInputModule, BrowserAnimationsModule,
|
||||
imports: [FormsModule, MatFormFieldModule, MatFormFieldModule, MatSelectModule, MatInputModule, NoopAnimationsModule,
|
||||
MatNativeDateModule, ReactiveFormsModule,
|
||||
MatCardModule, MatIconModule, HttpClientTestingModule, RouterTestingModule],
|
||||
declarations: [SimpleTriggerConfigComponent],
|
||||
@@ -91,16 +96,16 @@ describe('SimpleTriggerConfig', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const getJobsReq = httpTestingController.expectOne(`${CONTEXT_PATH}/jobs`);
|
||||
getJobsReq.flush(['TestJob']);
|
||||
getJobsReq.flush([testJobName]);
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
|
||||
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
|
||||
const submitButton = componentDe.query(By.css(submitButtonSelector));
|
||||
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
|
||||
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
|
||||
|
||||
setInputValue(componentDe, '#triggerName', 'test-trigger');
|
||||
expect(component.simpleTriggerReactiveForm.controls.triggerName.value).toEqual('test-trigger');
|
||||
setInputValue(componentDe, '#triggerName', testTriggerName);
|
||||
expect(component.simpleTriggerReactiveForm.controls.triggerName.value).toEqual(testTriggerName);
|
||||
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
|
||||
setMatSelectValueByIndex(componentDe, '#misfireInstruction', 0);
|
||||
expect(component.simpleTriggerReactiveForm.controls.misfireInstruction.value).toEqual('MISFIRE_INSTRUCTION_FIRE_NOW');
|
||||
@@ -111,7 +116,7 @@ describe('SimpleTriggerConfig', () => {
|
||||
setInputValue(componentDe, '#repeatCount', '1000');
|
||||
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual('');
|
||||
|
||||
setInputValue(componentDe, '#repeatInterval', '2000');
|
||||
setInputValue(componentDe, repeatIntervalSelector, '2000');
|
||||
expect(submitButton.nativeElement.getAttribute('disabled')).toEqual(null);
|
||||
}
|
||||
|
||||
@@ -122,18 +127,18 @@ describe('SimpleTriggerConfig', () => {
|
||||
it('should emit an event when a new trigger is submitted', () => {
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
const mockTrigger = new Trigger();
|
||||
mockTrigger.triggerKeyDTO = new TriggerKey('test-trigger', null);
|
||||
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: 'TestJob', description: null};
|
||||
mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null);
|
||||
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: testJobName, description: null};
|
||||
mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
|
||||
|
||||
openFormAndFillAllMandatoryFields();
|
||||
|
||||
setInputValue(componentDe, '#repeatInterval', '2000');
|
||||
setInputValue(componentDe, repeatIntervalSelector, '2000');
|
||||
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatInterval).toEqual(2000);
|
||||
setInputValue(componentDe, '#repeatCount', '100');
|
||||
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatCount).toEqual(100);
|
||||
|
||||
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
|
||||
const submitButton = componentDe.query(By.css(submitButtonSelector));
|
||||
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
|
||||
|
||||
let actualNewTrigger;
|
||||
@@ -141,28 +146,28 @@ describe('SimpleTriggerConfig', () => {
|
||||
|
||||
submitButton.nativeElement.click();
|
||||
|
||||
const postSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
|
||||
const postSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
|
||||
postSimpleTriggerReq.flush(mockTrigger);
|
||||
|
||||
expect(actualNewTrigger).toEqual(mockTrigger);
|
||||
});
|
||||
|
||||
it('should not emit an event when an existing trigger is edited', () => {
|
||||
const mockTriggerKey = new TriggerKey('test-trigger', null);
|
||||
const mockTriggerKey = new TriggerKey(testTriggerName, null);
|
||||
component.triggerKey = mockTriggerKey;
|
||||
fixture.detectChanges();
|
||||
|
||||
const mockTrigger = new SimpleTrigger();
|
||||
mockTrigger.triggerKeyDTO = new TriggerKey('test-trigger', null);
|
||||
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: 'TestJob', description: null};
|
||||
mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null);
|
||||
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: testJobName, description: null};
|
||||
mockTrigger.mayFireAgain = true;
|
||||
mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
|
||||
const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
|
||||
const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
|
||||
getSimpleTriggerReq.flush(mockTrigger);
|
||||
|
||||
component.simpleTriggerReactiveForm.setValue({
|
||||
triggerName: 'test-trigger',
|
||||
jobClass: 'TestJob',
|
||||
triggerName: testTriggerName,
|
||||
jobClass: testJobName,
|
||||
triggerRecurrence: {
|
||||
repeatInterval: 2000,
|
||||
repeatCount: 100,
|
||||
@@ -178,10 +183,10 @@ describe('SimpleTriggerConfig', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
setInputValue(componentDe, '#repeatInterval', '4000');
|
||||
setInputValue(componentDe, repeatIntervalSelector, '4000');
|
||||
expect(component.simpleTriggerReactiveForm.controls.triggerRecurrence.value.repeatInterval).toEqual(4000);
|
||||
|
||||
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
|
||||
const submitButton = componentDe.query(By.css(submitButtonSelector));
|
||||
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
|
||||
|
||||
let actualNewTrigger;
|
||||
@@ -189,7 +194,7 @@ describe('SimpleTriggerConfig', () => {
|
||||
|
||||
submitButton.nativeElement.click();
|
||||
|
||||
const putSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/test-trigger`);
|
||||
const putSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
|
||||
putSimpleTriggerReq.flush(mockTrigger);
|
||||
|
||||
expect(actualNewTrigger).toBeUndefined();
|
||||
@@ -198,6 +203,10 @@ describe('SimpleTriggerConfig', () => {
|
||||
it('should fetch and display the trigger when the triggerKey is passed as input', () => {
|
||||
const mockTriggerKey = new TriggerKey('my-simple-trigger', null);
|
||||
component.triggerKey = mockTriggerKey;
|
||||
|
||||
component.trigger = new SimpleTrigger();
|
||||
component.trigger.triggerKeyDTO = mockTriggerKey;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const mockTrigger = new Trigger();
|
||||
@@ -216,8 +225,34 @@ describe('SimpleTriggerConfig', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
const submitButton = componentDe.query(By.css('form button[color="primary"]'));
|
||||
const submitButton = componentDe.query(By.css(submitButtonSelector));
|
||||
expect(submitButton.nativeElement.textContent.trim()).toEqual('Submit');
|
||||
|
||||
expect(component.simpleTriggerReactiveForm.value.triggerName).toBeNull();
|
||||
|
||||
});
|
||||
|
||||
it('should reset the form when a new trigger is selected', () => {
|
||||
const mockTriggerKey = new TriggerKey(testTriggerName, null);
|
||||
component.triggerKey = mockTriggerKey;
|
||||
|
||||
const mockTrigger = new SimpleTrigger();
|
||||
mockTrigger.triggerKeyDTO = mockTriggerKey;
|
||||
mockTrigger.jobDetailDTO = <JobDetail>{jobClassName: testJobName, description: null};
|
||||
mockTrigger.mayFireAgain = true;
|
||||
mockTrigger.misfireInstruction = MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW;
|
||||
|
||||
const getSimpleTriggerReq = httpTestingController.expectOne(`${CONTEXT_PATH}/simple-triggers/${testTriggerName}`);
|
||||
getSimpleTriggerReq.flush(mockTrigger);
|
||||
|
||||
expect(component.simpleTriggerReactiveForm.value.triggerName).toEqual(testTriggerName);
|
||||
|
||||
component.triggerKey = null;
|
||||
|
||||
expect(component.simpleTriggerReactiveForm.value.triggerName).toBeNull();
|
||||
expect(component.simpleTriggerReactiveForm.value.jobClass).toBeNull();
|
||||
expect(component.shouldShowTheTriggerCardContent()).toBeTruthy();
|
||||
|
||||
});
|
||||
|
||||
it('should display the warning if there are no eligible jobs', () => {
|
||||
@@ -226,6 +261,9 @@ describe('SimpleTriggerConfig', () => {
|
||||
getJobsReq.flush([]);
|
||||
fixture.detectChanges();
|
||||
|
||||
component.openTriggerForm();
|
||||
fixture.detectChanges();
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
const warningCard = componentDe.query(By.css('#noEligibleJobsAlert'));
|
||||
expect(warningCard).toBeTruthy();
|
||||
@@ -237,6 +275,9 @@ describe('SimpleTriggerConfig', () => {
|
||||
getJobsReq.flush(['sampleJob']);
|
||||
fixture.detectChanges();
|
||||
|
||||
component.openTriggerForm();
|
||||
fixture.detectChanges();
|
||||
|
||||
const componentDe: DebugElement = fixture.debugElement;
|
||||
const warningCard = componentDe.query(By.css('#noEligibleJobsAlert'));
|
||||
expect(warningCard).toBeFalsy();
|
||||
|
||||
@@ -3,12 +3,11 @@ import {SchedulerService} from '../../services';
|
||||
import {Scheduler} from '../../model/scheduler.model';
|
||||
import {SimpleTriggerCommand} from '../../model/simple-trigger.command';
|
||||
import {SimpleTrigger} from '../../model/simple-trigger.model';
|
||||
import {SimpleTriggerForm} from '../../model/simple-trigger.form';
|
||||
import * as moment from 'moment';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import JobService from '../../services/job.service';
|
||||
import {MisfireInstruction, MisfireInstructionCaption} from '../../model/misfire-instruction.model';
|
||||
import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators} from '@angular/forms';
|
||||
import {AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, Validators} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'qrzmng-simple-trigger-config',
|
||||
@@ -17,9 +16,9 @@ import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors,
|
||||
})
|
||||
export class SimpleTriggerConfigComponent implements OnInit {
|
||||
|
||||
trigger: SimpleTrigger;
|
||||
trigger: SimpleTrigger = null;
|
||||
|
||||
simpleTriggerReactiveForm: FormGroup = this.formBuilder.group({
|
||||
simpleTriggerReactiveForm: UntypedFormGroup = this.formBuilder.group({
|
||||
triggerName: [this.trigger?.triggerKeyDTO.name, Validators.required],
|
||||
jobClass: [this.trigger?.jobDetailDTO.jobClassName, Validators.required],
|
||||
triggerPeriod: this.formBuilder.group({
|
||||
@@ -35,9 +34,8 @@ export class SimpleTriggerConfigComponent implements OnInit {
|
||||
|
||||
scheduler: Scheduler;
|
||||
|
||||
triggerLoading = true;
|
||||
triggerLoading = false;
|
||||
|
||||
private fetchedTriggers = false;
|
||||
private triggerInProgress = false;
|
||||
|
||||
private selectedTriggerKey: TriggerKey;
|
||||
@@ -49,8 +47,11 @@ export class SimpleTriggerConfigComponent implements OnInit {
|
||||
@Output()
|
||||
onNewTrigger = new EventEmitter<SimpleTrigger>();
|
||||
|
||||
@Output()
|
||||
triggerFormOpenChange = new EventEmitter<boolean>();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private schedulerService: SchedulerService,
|
||||
private jobService: JobService
|
||||
) {
|
||||
@@ -66,18 +67,37 @@ export class SimpleTriggerConfigComponent implements OnInit {
|
||||
|
||||
openTriggerForm() {
|
||||
this.enabledTriggerForm = true;
|
||||
this.triggerFormOpenChange.emit(this.enabledTriggerForm);
|
||||
}
|
||||
|
||||
private closeTriggerForm() {
|
||||
this.enabledTriggerForm = false;
|
||||
this.triggerFormOpenChange.emit(this.enabledTriggerForm);
|
||||
}
|
||||
|
||||
@Input()
|
||||
set triggerKey(triggerKey: TriggerKey) {
|
||||
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
||||
this.fetchSelectedTrigger();
|
||||
if (!triggerKey) {
|
||||
this.openNewTriggerForm();
|
||||
} else if (!this.selectedTriggerKey || this.selectedTriggerKey.name !== triggerKey.name) {
|
||||
this._resetTheTrigger();
|
||||
this.selectedTriggerKey = {...triggerKey} as TriggerKey;
|
||||
this.fetchSelectedTrigger();
|
||||
this.closeTriggerForm();
|
||||
}
|
||||
}
|
||||
|
||||
openNewTriggerForm() {
|
||||
this._resetTheTrigger();
|
||||
this.openTriggerForm();
|
||||
}
|
||||
|
||||
private _resetTheTrigger() {
|
||||
this.trigger = null;
|
||||
this.triggerInProgress = false;
|
||||
this.selectedTriggerKey = null;
|
||||
this.simpleTriggerReactiveForm.reset(new SimpleTriggerReactiveForm());
|
||||
}
|
||||
|
||||
fetchSelectedTrigger = () => {
|
||||
this.triggerLoading = true;
|
||||
@@ -95,7 +115,11 @@ export class SimpleTriggerConfigComponent implements OnInit {
|
||||
existsATriggerInProgress = (): boolean => this.trigger && this.triggerInProgress;
|
||||
|
||||
onResetReactiveForm = () => {
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
|
||||
if (this.trigger) {
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
|
||||
} else {
|
||||
this.simpleTriggerReactiveForm.reset(new SimpleTriggerReactiveForm());
|
||||
}
|
||||
this.closeTriggerForm();
|
||||
};
|
||||
|
||||
@@ -104,13 +128,13 @@ export class SimpleTriggerConfigComponent implements OnInit {
|
||||
this.schedulerService.updateSimpleTriggerConfig : this.schedulerService.saveSimpleTriggerConfig;
|
||||
|
||||
const simpleTriggerCommand = this._fromReactiveFormToCommand();
|
||||
this.triggerLoading = true;
|
||||
schedulerServiceCall(simpleTriggerCommand)
|
||||
.subscribe((retTrigger: SimpleTrigger) => {
|
||||
this.trigger = retTrigger;
|
||||
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(retTrigger));
|
||||
|
||||
this.fetchedTriggers = true;
|
||||
this.triggerInProgress = this.trigger.mayFireAgain;
|
||||
|
||||
if (schedulerServiceCall === this.schedulerService.saveSimpleTriggerConfig) {
|
||||
@@ -119,8 +143,13 @@ export class SimpleTriggerConfigComponent implements OnInit {
|
||||
|
||||
this.closeTriggerForm();
|
||||
}, error => {
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
|
||||
});
|
||||
if (this.trigger) {
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
|
||||
}
|
||||
this.triggerLoading = false;
|
||||
}, () => {
|
||||
this.triggerLoading = false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
<mat-card-content style="position: relative; height: 100%">
|
||||
<mat-nav-list style="overflow-y: auto; position: absolute; left: 0; right: 0; top: 0; bottom: 0; overflow: auto; height: calc(100% - 3em)">
|
||||
<mat-list-item *ngFor="let triggerKey of getTriggerKeyList()" class="triggerItemList"
|
||||
[ngClass]="{'selectedTrigger': selectedTrigger && selectedTrigger.name==triggerKey.name}">
|
||||
[ngClass]="{'selectedTrigger': selectedTrigger && selectedTrigger.name==triggerKey.name}"
|
||||
(click)="selectTrigger(triggerKey)"
|
||||
>
|
||||
<a matLine>{{ triggerKey.name }}</a>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
import {ApiService, ConfigService, CONTEXT_PATH, TriggerService} from '../../services';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||
@@ -21,7 +21,7 @@ describe('TriggerListComponent', () => {
|
||||
let httpClient: HttpClient;
|
||||
let httpTestingController: HttpTestingController;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatCardModule, MatDialogModule, MatDividerModule,
|
||||
MatIconModule, MatListModule, HttpClientTestingModule, RouterTestingModule],
|
||||
|
||||
@@ -2,13 +2,25 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {TriggerService} from '../../services/trigger.service';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {SimpleTrigger} from '../../model/simple-trigger.model';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
template: 'Multiple jobs not supported yet - Coming Soon...',
|
||||
template: `
|
||||
<h3 mat-dialog-title>Coming Soon</h3>
|
||||
<div mat-dialog-content>
|
||||
<p>This feature is in roadmap and it will come with the next releases</p>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="closeDialog()" style="padding: 0.5em;width: 5em;">Ok</button>
|
||||
</div>`,
|
||||
})
|
||||
// tslint:disable-next-line:component-class-suffix
|
||||
export class UnsupportedMultipleJobsDialog {
|
||||
constructor(public dialogRef: MatDialogRef<UnsupportedMultipleJobsDialog>) {
|
||||
}
|
||||
closeDialog(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -69,16 +81,17 @@ export class TriggerListComponent implements OnInit {
|
||||
}
|
||||
|
||||
onNewTriggerBtnClicked() {
|
||||
if (this.triggerKeys && this.triggerKeys.length > 0) {
|
||||
this.dialog.open(UnsupportedMultipleJobsDialog)
|
||||
} else {
|
||||
this.onNewTriggerClicked.emit();
|
||||
}
|
||||
this.onNewTriggerClicked.emit();
|
||||
// if (this.getTriggerKeyList() && this.getTriggerKeyList().length > 0) {
|
||||
// this.dialog.open(UnsupportedMultipleJobsDialog)
|
||||
// } else {
|
||||
// this.onNewTriggerClicked.emit();
|
||||
// }
|
||||
}
|
||||
|
||||
onNewTrigger(newTrigger: SimpleTrigger) {
|
||||
this.newTriggers = [newTrigger, ...this.newTriggers];
|
||||
this.selectedTrigger = newTrigger.triggerKeyDTO;
|
||||
this.selectTrigger(newTrigger.triggerKeyDTO);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
/** *************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
@@ -51,14 +51,14 @@ import 'core-js/es7/reflect';
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
/** *************************************************************************************************
|
||||
* Zone JS is required by Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
/** *************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
|
||||
@@ -68,7 +68,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
*/
|
||||
// import 'intl'; // Run `npm install --save intl`.
|
||||
|
||||
/***************************************************************************************************
|
||||
/** *************************************************************************************************
|
||||
* MATERIAL 2
|
||||
*/
|
||||
import 'hammerjs/hammer';
|
||||
|
||||
@@ -3,9 +3,8 @@ export * from './user.service';
|
||||
export * from './config.service';
|
||||
export * from './auth.service';
|
||||
export * from './scheduler.service';
|
||||
export * from './websocket.service';
|
||||
export * from './progress.websocket.service';
|
||||
export * from './logs.websocket.service';
|
||||
export * from './progress.rx-websocket.service';
|
||||
export * from './logs.rx-websocket.service';
|
||||
export * from './trigger.service'
|
||||
export * from './job.service'
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LogsRxWebsocketService } from './logs.rx-websocket.service';
|
||||
import {ApiService} from './api.service';
|
||||
import {RxStomp} from '@stomp/rx-stomp';
|
||||
import {jest} from '@jest/globals';
|
||||
|
||||
describe('LogsRxWebsocketService', () => {
|
||||
let service: LogsRxWebsocketService;
|
||||
let configureSpy;
|
||||
let activateSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
configureSpy = jest.spyOn(RxStomp.prototype, 'configure');
|
||||
activateSpy = jest.spyOn(RxStomp.prototype, 'activate').mockImplementation(() => undefined);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: ApiService, useValue: {getToken: () => 'test-token'}}
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(LogsRxWebsocketService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should configure rx-stomp with the logs websocket endpoint', () => {
|
||||
expect(configureSpy).toHaveBeenCalled();
|
||||
expect(activateSpy).toHaveBeenCalled();
|
||||
|
||||
const config = configureSpy.mock.calls[configureSpy.mock.calls.length - 1][0];
|
||||
expect(config.heartbeatIncoming).toEqual(0);
|
||||
expect(config.heartbeatOutgoing).toEqual(20000);
|
||||
expect(config.reconnectDelay).toEqual(200);
|
||||
expect(config.webSocketFactory.toString()).toContain('/logs?access_token=');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {RxStompService} from './rx-stomp.service';
|
||||
import {ApiService} from './api.service';
|
||||
import SockJS from 'sockjs-client';
|
||||
import {CONTEXT_PATH, getBaseUrl} from './config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LogsRxWebsocketService extends RxStompService {
|
||||
|
||||
constructor(private apiService: ApiService) {
|
||||
super({
|
||||
webSocketFactory: () => new SockJS(`${getBaseUrl()}${CONTEXT_PATH}/logs?access_token=${this.apiService.getToken()}`),
|
||||
heartbeatIncoming: 0,
|
||||
heartbeatOutgoing: 20000,
|
||||
reconnectDelay: 200,
|
||||
debug: (msg: string): void => {
|
||||
console.log(new Date(), msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {WebsocketService, ApiService, getBaseUrl, CONTEXT_PATH} from '.';
|
||||
import {SocketOption} from '../model/SocketOption.model';
|
||||
|
||||
@Injectable()
|
||||
export class LogsWebsocketService extends WebsocketService {
|
||||
|
||||
constructor(private apiService: ApiService) {
|
||||
super(new SocketOption(getBaseUrl() + `${CONTEXT_PATH}/logs`, '/topic/logs', apiService.getToken))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import {RxStomp} from '@stomp/rx-stomp';
|
||||
import {jest} from '@jest/globals';
|
||||
|
||||
import { ProgressRxWebsocketService } from './progress.rx-websocket.service';
|
||||
import {ApiService} from './api.service';
|
||||
|
||||
describe('ProgressRxWebsocketService', () => {
|
||||
let service: ProgressRxWebsocketService;
|
||||
let configureSpy;
|
||||
let activateSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
configureSpy = jest.spyOn(RxStomp.prototype, 'configure');
|
||||
activateSpy = jest.spyOn(RxStomp.prototype, 'activate').mockImplementation(() => undefined);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: ApiService, useValue: {getToken: () => 'test-token'}}
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(ProgressRxWebsocketService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should configure rx-stomp with the progress websocket endpoint', () => {
|
||||
expect(configureSpy).toHaveBeenCalled();
|
||||
expect(activateSpy).toHaveBeenCalled();
|
||||
|
||||
const config = configureSpy.mock.calls[configureSpy.mock.calls.length - 1][0];
|
||||
expect(config.heartbeatIncoming).toEqual(0);
|
||||
expect(config.heartbeatOutgoing).toEqual(20000);
|
||||
expect(config.reconnectDelay).toEqual(200);
|
||||
expect(config.webSocketFactory.toString()).toContain('/progress?access_token=');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {RxStompService} from './rx-stomp.service';
|
||||
import {ApiService} from './api.service';
|
||||
import SockJS from 'sockjs-client';
|
||||
import {CONTEXT_PATH, getBaseUrl} from './config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProgressRxWebsocketService extends RxStompService {
|
||||
|
||||
constructor(private apiService: ApiService) {
|
||||
super({
|
||||
webSocketFactory: () => new SockJS(`${getBaseUrl()}${CONTEXT_PATH}/progress?access_token=${this.apiService.getToken()}`),
|
||||
heartbeatIncoming: 0,
|
||||
heartbeatOutgoing: 20000,
|
||||
reconnectDelay: 200,
|
||||
debug: (msg: string): void => {
|
||||
console.log(new Date(), msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {WebsocketService, ApiService, getBaseUrl, CONTEXT_PATH} from '.';
|
||||
import {SocketOption} from '../model/SocketOption.model';
|
||||
|
||||
@Injectable()
|
||||
export class ProgressWebsocketService extends WebsocketService {
|
||||
|
||||
constructor(private apiService: ApiService) {
|
||||
super(new SocketOption(getBaseUrl() + `${CONTEXT_PATH}/progress`, '/topic/progress', apiService.getToken))
|
||||
}
|
||||
|
||||
}
|
||||
12
quartz-manager-frontend/src/app/services/rx-stomp.service.ts
Normal file
12
quartz-manager-frontend/src/app/services/rx-stomp.service.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {RxStomp} from '@stomp/rx-stomp';
|
||||
import {RxStompConfig} from '@stomp/rx-stomp/esm6/rx-stomp-config';
|
||||
|
||||
export class RxStompService extends RxStomp {
|
||||
|
||||
constructor(rxStompConfig: RxStompConfig) {
|
||||
super();
|
||||
super.configure(rxStompConfig);
|
||||
super.activate();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -35,15 +35,16 @@ export class UserService {
|
||||
this.currentUser = user;
|
||||
this.router.initialNavigation();
|
||||
}, err => {
|
||||
console.log(`error retrieving current user due to ` + JSON.stringify(err));
|
||||
console.log('error retrieving current user due to ' + JSON.stringify(err));
|
||||
const httpErrorResponse = err as HttpErrorResponse;
|
||||
if (httpErrorResponse.status === 404) {
|
||||
this.isAnAnonymousUser = true;
|
||||
this.router.initialNavigation();
|
||||
return;
|
||||
}
|
||||
if (httpErrorResponse.status < 200 || httpErrorResponse.status > 399)
|
||||
if (httpErrorResponse.status !== 401 && (httpErrorResponse.status < 200 || httpErrorResponse.status > 399)) {
|
||||
this.router.navigateByUrl('/error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import {Observable, Subscriber} from 'rxjs';
|
||||
import {SocketEndpoint} from '../model/SocketEndpoint.model'
|
||||
|
||||
|
||||
import Stomp from 'stompjs';
|
||||
import SockJS from 'sockjs-client';
|
||||
import {SocketOption} from '../model/SocketOption.model';
|
||||
|
||||
interface WebsocketSubscriber {
|
||||
index: number,
|
||||
observer: Subscriber<any>
|
||||
}
|
||||
|
||||
export interface QuartzManagerWebsocketMessage {
|
||||
type: string;
|
||||
message: any;
|
||||
headers: any;
|
||||
self: boolean;
|
||||
}
|
||||
|
||||
export class WebsocketService {
|
||||
|
||||
_options: SocketOption;
|
||||
|
||||
_socket: SocketEndpoint = new SocketEndpoint();
|
||||
|
||||
observableStompConnection: Observable<any>;
|
||||
subscribers: Array<WebsocketSubscriber> = [];
|
||||
subscriberIndex = 0;
|
||||
|
||||
_messageIds: Array<any> = [];
|
||||
|
||||
reconnectionPromise: any;
|
||||
|
||||
constructor(options: SocketOption) {
|
||||
this._options = options
|
||||
this.createObservableSocket();
|
||||
this.connect();
|
||||
}
|
||||
|
||||
getOptions = () => {
|
||||
}
|
||||
|
||||
private createObservableSocket = () => {
|
||||
this.observableStompConnection = new Observable((observer) => {
|
||||
const subscriberIndex = this.subscriberIndex++;
|
||||
this.addToSubscribers({index: subscriberIndex, observer});
|
||||
return () => this.removeFromSubscribers(subscriberIndex);
|
||||
});
|
||||
}
|
||||
|
||||
private addToSubscribers = (subscriber) => {
|
||||
this.subscribers.push(subscriber);
|
||||
}
|
||||
|
||||
private removeFromSubscribers = (index) => {
|
||||
this.subscribers = this.subscribers.filter(subscriber => subscriber.index !== index);
|
||||
}
|
||||
|
||||
getObservable = () => {
|
||||
return this.observableStompConnection;
|
||||
};
|
||||
|
||||
getMessage = function (data): QuartzManagerWebsocketMessage {
|
||||
const out: QuartzManagerWebsocketMessage = <QuartzManagerWebsocketMessage>{};
|
||||
out.type = 'SUCCESS';
|
||||
out.message = JSON.parse(data.body);
|
||||
out.headers = {};
|
||||
out.headers.messageId = data.headers['message-id'];
|
||||
|
||||
const messageIdIndex = this._messageIds.indexOf(out.headers.messageId);
|
||||
if (messageIdIndex > -1) {
|
||||
out.self = true;
|
||||
this._messageIds = this._messageIds.splice(messageIdIndex, 1);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
_socketListener = (frame) => {
|
||||
console.log('Connected: ' + frame);
|
||||
this._socket.stomp.subscribe(
|
||||
this._options.topicName,
|
||||
data => this.subscribers.forEach(subscriber => subscriber.observer.next(this.getMessage(data)))
|
||||
);
|
||||
}
|
||||
|
||||
_onSocketError = (errorMsg) => {
|
||||
const out: any = {};
|
||||
out.type = 'ERROR';
|
||||
out.message = errorMsg;
|
||||
this.subscribers.forEach(subscriber => subscriber.observer.error(out));
|
||||
this.scheduleReconnection();
|
||||
}
|
||||
|
||||
scheduleReconnection = () => {
|
||||
this.reconnectionPromise = setTimeout(() => {
|
||||
console.log('Socket reconnecting... (if it fails, next attempt in ' + this._options.reconnectionTimeout + ' msec)');
|
||||
this.connect();
|
||||
}, this._options.reconnectionTimeout);
|
||||
}
|
||||
|
||||
reconnectNow = function () {
|
||||
this._socket.stomp.disconnect();
|
||||
if (this.reconnectionPromise && this.reconnectionPromise.cancel) {
|
||||
this.reconnectionPromise.cancel();
|
||||
}
|
||||
this.connect();
|
||||
};
|
||||
|
||||
send = (message) => {
|
||||
const id = Math.floor(Math.random() * 1000000);
|
||||
this._socket.stomp.send(this._options.brokerName, {
|
||||
priority: 9
|
||||
}, JSON.stringify({
|
||||
message: message,
|
||||
id: id
|
||||
}));
|
||||
this._messageIds.push(id);
|
||||
};
|
||||
|
||||
connect = () => {
|
||||
const headers = {};
|
||||
|
||||
let socketUrl = this._options.socketUrl;
|
||||
if (this._options.getAccessToken()) {
|
||||
socketUrl += `?access_token=${this._options.getAccessToken()}`;
|
||||
}
|
||||
|
||||
this._socket.client = new SockJS(socketUrl);
|
||||
this._socket.stomp = Stomp.over(this._socket.client);
|
||||
this._socket.stomp.connect(headers, this._socketListener, this._onSocketError);
|
||||
this._socket.stomp.onclose = this.scheduleReconnection;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
|
||||
import {GenericErrorComponent} from './genericError.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('GenericComponent', () => {
|
||||
let component: GenericErrorComponent;
|
||||
let fixture: ComponentFixture<GenericErrorComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ GenericErrorComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
|
||||
import { ForbiddenComponent } from './forbidden.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('ForbiddenComponent', () => {
|
||||
let component: ForbiddenComponent;
|
||||
let fixture: ComponentFixture<ForbiddenComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ForbiddenComponent ]
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
import { LoginComponent } from './login.component';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
@@ -16,7 +16,7 @@ describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LoginComponent],
|
||||
imports: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
import {UntypedFormBuilder, UntypedFormGroup, Validators} from '@angular/forms';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {DisplayMessage} from '../../shared/models/display-message';
|
||||
import {Subject} from 'rxjs';
|
||||
@@ -15,7 +15,7 @@ import {AuthService, UserService} from '../../services';
|
||||
export class LoginComponent implements OnInit, OnDestroy {
|
||||
title = 'Login';
|
||||
githubLink = 'https://github.com/fabioformosa/quartz-manager';
|
||||
form: FormGroup;
|
||||
form: UntypedFormGroup;
|
||||
|
||||
submitted = false;
|
||||
|
||||
@@ -29,7 +29,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private formBuilder: FormBuilder
|
||||
private formBuilder: UntypedFormBuilder
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
@@ -19,18 +19,25 @@
|
||||
<div fxFlex="1 1 350px">
|
||||
<div fxLayout="row" fxFill>
|
||||
<div fxLayout="column" fxFill>
|
||||
<qrzmng-simple-trigger-config fxFill
|
||||
[triggerKey]="selectedTriggerKey"
|
||||
(onNewTrigger)="onNewTriggerCreated($event)">
|
||||
</qrzmng-simple-trigger-config>
|
||||
<qrzmng-simple-trigger-config fxFill
|
||||
[triggerKey]="selectedTriggerKey"
|
||||
(triggerFormOpenChange)="setNewTriggerFormOpened($event)"
|
||||
(onNewTrigger)="onNewTriggerCreated($event)">
|
||||
</qrzmng-simple-trigger-config>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div fxFlex="1 1 auto" style="margin-left: 20px;">
|
||||
<div fxFlex="1 1 auto" fxLayout="column" fxLayoutAlign="start stretch" fxLayoutGap="6px">
|
||||
<progress-panel></progress-panel>
|
||||
<logs-panel fxFlex="1 1 auto" fxFill></logs-panel>
|
||||
<progress-panel
|
||||
[triggerKey]=selectedTriggerKey
|
||||
>
|
||||
</progress-panel>
|
||||
<logs-panel fxFlex="1 1 auto" fxFill
|
||||
[triggerKey]=selectedTriggerKey
|
||||
>
|
||||
</logs-panel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {Component, OnInit, ViewChild} from '@angular/core';
|
||||
import {
|
||||
ConfigService,
|
||||
UserService
|
||||
} from '../../services';
|
||||
import {SimpleTrigger} from '../../model/simple-trigger.model';
|
||||
import {TriggerKey} from '../../model/triggerKey.model';
|
||||
import {SimpleTriggerConfigComponent} from '../../components/simple-trigger-config';
|
||||
@@ -32,15 +28,25 @@ export class ManagerComponent implements OnInit {
|
||||
}
|
||||
|
||||
onNewTriggerRequested() {
|
||||
this.triggerConfigComponent.openTriggerForm();
|
||||
this.selectedTriggerKey = null;
|
||||
this.newTriggerFormOpened = true;
|
||||
if (this.triggerConfigComponent) {
|
||||
this.triggerConfigComponent.openNewTriggerForm();
|
||||
}
|
||||
}
|
||||
|
||||
onNewTriggerCreated(newTrigger: SimpleTrigger) {
|
||||
this.triggerListComponent.onNewTrigger(newTrigger);
|
||||
this.newTriggerFormOpened = false;
|
||||
}
|
||||
|
||||
setSelectedTrigger(triggerKey: TriggerKey) {
|
||||
this.selectedTriggerKey = triggerKey;
|
||||
this.newTriggerFormOpened = false;
|
||||
}
|
||||
|
||||
setNewTriggerFormOpened(opened: boolean) {
|
||||
this.newTriggerFormOpened = opened;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
|
||||
import { NotFoundComponent } from './not-found.component';
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('NotFoundComponent', () => {
|
||||
let component: NotFoundComponent;
|
||||
let fixture: ComponentFixture<NotFoundComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [NotFoundComponent]
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
/** *************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
@@ -50,7 +50,7 @@ import 'core-js/es6/reflect';
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
/** *************************************************************************************************
|
||||
* Zone JS is required by Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
@@ -59,7 +59,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
/** *************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
|
||||
@@ -69,7 +69,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
*/
|
||||
// import 'intl'; // Run `npm install --save intl`.
|
||||
|
||||
/***************************************************************************************************
|
||||
/** *************************************************************************************************
|
||||
* MATERIAL 2
|
||||
*/
|
||||
import 'hammerjs/hammer';
|
||||
|
||||
2
quartz-manager-frontend/src/typings.d.ts
vendored
2
quartz-manager-frontend/src/typings.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
/* SystemJS module definition */
|
||||
declare var module: NodeModule;
|
||||
declare let module: NodeModule;
|
||||
interface NodeModule {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2015",
|
||||
"target": "es2020",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
@@ -18,6 +18,6 @@
|
||||
"es2016",
|
||||
"dom"
|
||||
],
|
||||
"module": "esnext"
|
||||
"module": "es2020"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"curly": true,
|
||||
"eofline": true,
|
||||
"forin": true,
|
||||
"deprecation": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"import-blacklist": [true],
|
||||
"import-spacing": true,
|
||||
"indent": [
|
||||
@@ -53,7 +56,6 @@
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-var-keyword": true,
|
||||
"object-literal-sort-keys": false,
|
||||
"one-line": [
|
||||
|
||||
1
quartz-manager-parent/.gitignore
vendored
1
quartz-manager-parent/.gitignore
vendored
@@ -4,3 +4,4 @@
|
||||
.classpath
|
||||
.project
|
||||
.idea
|
||||
/**/*.iml
|
||||
|
||||
@@ -58,13 +58,13 @@ Replace the dummy job (class: `it.fabioformosa.quartzmanager.jobs.SampleJob`) wi
|
||||
|
||||
**Backend Stack** Java 9, Spring Boot 2.5.6 (Spring MVC 5.3.12, Spring Security 5.5.3), Quartz Scheduler 2.3.2
|
||||
|
||||
**Frontend** Angular 9.1.4, Web-Socket (stompjs 2.3.3)
|
||||
**Frontend** Angular 14.2.12, Web-Socket (stompjs 2.3.3)
|
||||
|
||||
**Style** Angular Material 9, FontAwesome 5
|
||||
**Style** Angular Material 14, FontAwesome 5
|
||||
|
||||
Starting from Quartz Manager v2.x.x, the new structure of project is:
|
||||
* Multi-module maven project: REST API backend
|
||||
* Angular 9: Single Page Application frontend
|
||||
* Angular 14: Single Page Application frontend
|
||||
|
||||
(The first version of quartz manager was a monolithic backend that provided also frontend developed with angularjs 1.6.x. You can find it at the branch 1.x.x)
|
||||
|
||||
|
||||
2
quartz-manager-parent/lombok.config
Normal file
2
quartz-manager-parent/lombok.config
Normal file
@@ -0,0 +1,2 @@
|
||||
config.stopBubbling = true
|
||||
lombok.addLombokGeneratedAnnotation = true
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-parent</artifactId>
|
||||
<version>4.0.2</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
</developers>
|
||||
|
||||
<properties>
|
||||
<sonar.organization>fabioformosa</sonar.organization>
|
||||
<java.version>9</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<org.projectlombok.version>1.18.30</org.projectlombok.version>
|
||||
<maven-surefire-plugin.version>2.22.0</maven-surefire-plugin.version>
|
||||
<maven-failsafe-plugin.version>2.22.0</maven-failsafe-plugin.version>
|
||||
<jacoco-maven-plugin.version>0.8.8</jacoco-maven-plugin.version>
|
||||
@@ -51,8 +51,19 @@
|
||||
<nexus-staging-maven-plugin.version>1.6.7</nexus-staging-maven-plugin.version>
|
||||
<maven-release-plugin.version>2.5.3</maven-release-plugin.version>
|
||||
<maven-gpg-plugin.version>3.0.1</maven-gpg-plugin.version>
|
||||
<sonar-maven-plugin.version>3.11.0.3922</sonar-maven-plugin.version>
|
||||
<sonar.organization>fabioformosa</sonar.organization>
|
||||
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
|
||||
<sonar.exclusions>**/SpringApplicationTest.java, **/QuartManagerApplicationTests.java</sonar.exclusions>
|
||||
<sonar.exclusions>
|
||||
**/SpringApplicationTest.java, **/QuartManagerApplicationTests.java, **/MisfireTestJob.java
|
||||
</sonar.exclusions>
|
||||
<sonar.coverage.exclusions>
|
||||
OpenApiConfig.java,
|
||||
**/SecurityOpenApiConfig.java, **/QuartzModuleProperties.java, **/QuartzManagerDemoApplication.java,
|
||||
**/ServletInitializer.java, **/SessionController.java, **/HealthCheckController.java,
|
||||
**/WebShowcaseOpenApiConfig.java, **/MisfireTestJob.java, **/PersistenceConfig.java,
|
||||
**/QuartzManagerSecurityConfig.java
|
||||
</sonar.coverage.exclusions>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
@@ -69,27 +80,32 @@
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-common</artifactId>
|
||||
<version>4.0.2</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-api</artifactId>
|
||||
<version>4.0.2</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-security</artifactId>
|
||||
<version>4.0.2</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-persistence</artifactId>
|
||||
<version>4.0.2</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-ui</artifactId>
|
||||
<version>4.0.2</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${org.projectlombok.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
@@ -124,10 +140,28 @@
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>${maven-failsafe-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonarsource.scanner.maven</groupId>
|
||||
<artifactId>sonar-maven-plugin</artifactId>
|
||||
<version>${sonar-maven-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>${jacoco-maven-plugin.version}</version>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>**/OpenApiConfig.class</exclude>
|
||||
<exclude>**/SecurityOpenApiConfig.class</exclude>
|
||||
<exclude>**/QuartzModuleProperties.class</exclude>
|
||||
<exclude>**/QuartzManagerDemoApplication.class</exclude>
|
||||
<exclude>**/ServletInitializer.class</exclude>
|
||||
<exclude>**/SessionController.class</exclude>
|
||||
<exclude>**/HealthCheckController.class</exclude>
|
||||
<exclude>**/WebShowcaseOpenApiConfig.class</exclude>
|
||||
<exclude>**/MisfireTestJob.class</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -141,6 +175,13 @@
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report-aggregate</id>
|
||||
<goals>
|
||||
<goal>report-aggregate</goal>
|
||||
</goals>
|
||||
<phase>verify</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
@@ -179,17 +220,6 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>${nexus-staging-maven-plugin.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<serverId>ossrh</serverId>
|
||||
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
@@ -222,17 +252,27 @@
|
||||
</activation>
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>ossrh</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||
<id>maven-central-release</id>
|
||||
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
|
||||
</snapshotRepository>
|
||||
<repository>
|
||||
<id>ossrh</id>
|
||||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/
|
||||
</url>
|
||||
<id>maven-central-release</id>
|
||||
<url>https://central.sonatype.com</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<publishingServerId>maven-central-release</publishingServerId>
|
||||
<autoPublish>true</autoPublish>
|
||||
<waitUntil>published</waitUntil>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-parent</artifactId>
|
||||
<version>4.0.2</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>quartz-manager-common</artifactId>
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package it.fabioformosa.quartzmanager.api.common.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Generated;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
@Data
|
||||
@Generated
|
||||
public class QuartzModuleProperties {
|
||||
|
||||
private Properties properties = new Properties();
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package it.fabioformosa.quartzmanager.api.common.utils;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param <R> success type
|
||||
*/
|
||||
@Getter
|
||||
public class Try<R> {
|
||||
|
||||
private final Throwable failure;
|
||||
private final R success;
|
||||
|
||||
public Try(Throwable failure, R success) {
|
||||
private Try(Throwable failure, R success) {
|
||||
this.failure = failure;
|
||||
this.success = success;
|
||||
}
|
||||
@@ -16,11 +23,11 @@ public class Try<R> {
|
||||
return success;
|
||||
}
|
||||
|
||||
public static <R> Try<R> success(R r){
|
||||
private static <R> Try<R> success(R r){
|
||||
return new Try<>(null, r);
|
||||
}
|
||||
|
||||
public static <R> Try<R> failure(Throwable e){
|
||||
private static <R> Try<R> failure(Throwable e){
|
||||
return new Try<>(e, null);
|
||||
}
|
||||
|
||||
@@ -38,14 +45,6 @@ public class Try<R> {
|
||||
return t -> Try.with(checkedFunction).apply(t).getSuccess();
|
||||
}
|
||||
|
||||
public boolean isSuccess(){
|
||||
return this.failure == null;
|
||||
}
|
||||
|
||||
public boolean isFailure(){
|
||||
return this.failure != null;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public static interface CheckedFunction<T, R> {
|
||||
R apply(T t) throws java.lang.Exception;
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
class DateUtilsTest {
|
||||
@@ -17,4 +18,20 @@ class DateUtilsTest {
|
||||
Assertions.assertThat(convertedLocalDateTime).isEqualTo(originalLocalDateTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenALocalDatetime_whenTheAddHoursToNowIsCalled_shouldReturnAFutureDate(){
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
Date futureDate = DateUtils.addHoursToNow(1);
|
||||
|
||||
calendar.add(Calendar.HOUR_OF_DAY, 1);
|
||||
calendar.add(Calendar.MINUTE, -1);
|
||||
Date hourStartingAround = calendar.getTime();
|
||||
|
||||
calendar.add(Calendar.HOUR_OF_DAY, 1);
|
||||
calendar.add(Calendar.MINUTE, 2);
|
||||
Date hourEndingAround = calendar.getTime();
|
||||
|
||||
Assertions.assertThat(futureDate).isBetween(hourStartingAround, hourEndingAround);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package it.fabioformosa.quartzmanager.api.common.utils;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
class TryTest {
|
||||
|
||||
String raiseExceptionIfHello(String greetings) throws Exception {
|
||||
if("hello".equals(greetings))
|
||||
throw new Exception("hello");
|
||||
return greetings;
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenAFunctionWhichRaisesAnException_whenSneakyThrowIsCalled_thenItReturnsNull(){
|
||||
String hello = Optional.of("hello").map(Try.sneakyThrow(this::raiseExceptionIfHello)).orElse(null);
|
||||
Assertions.assertThat(hello).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenAFunctionWhichDoesntRaiseAnException_whenSneakyThrowIsCalled_thenItReturnsTheValue(){
|
||||
String hello = Optional.of("not hello").map(Try.sneakyThrow(this::raiseExceptionIfHello)).orElse(null);
|
||||
Assertions.assertThat(hello).isEqualTo("not hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenAFunctionWhichRaisesAnException_whenTryWithIsCalled_thenItReturnsAFailureObj(){
|
||||
Try<String> aTry = Optional.of("hello").map(greet -> Try.with(this::raiseExceptionIfHello).apply(greet)).get();
|
||||
Assertions.assertThat(aTry.getFailure()).isNotNull();
|
||||
Assertions.assertThat(aTry.getFailure().getMessage()).isEqualTo("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenAFunctionWhichDoesntRaiseAnException_whenTryWithIsCalled_thenItReturnsTheValue(){
|
||||
Try<String> aTry = Optional.of("not hello").map(greet -> Try.with(this::raiseExceptionIfHello).apply(greet)).get();
|
||||
Assertions.assertThat(aTry.getFailure()).isNull();
|
||||
Assertions.assertThat(aTry.getSuccess()).isEqualTo("not hello");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-parent</artifactId>
|
||||
<version>4.0.2</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>quartz-manager-starter-api</artifactId>
|
||||
@@ -20,7 +20,7 @@
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<springdoc-openapi.version>1.5.12</springdoc-openapi.version>
|
||||
<java.version>9</java.version>
|
||||
<sonar.exclusions>**/QuartManagerApplicationTests.java</sonar.exclusions>
|
||||
<sonar.exclusions>**/QuartManagerApplicationTests.java, **/OpenApiConfig.java</sonar.exclusions>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -106,6 +106,10 @@
|
||||
<artifactId>reflections</artifactId>
|
||||
<version>0.10.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- QUARTZ -->
|
||||
<dependency>
|
||||
@@ -118,34 +122,6 @@
|
||||
<version>1.3.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Reactor -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>io.projectreactor</groupId>-->
|
||||
<!-- <artifactId>reactor-core</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>io.projectreactor</groupId>-->
|
||||
<!-- <artifactId>reactor-net</artifactId>-->
|
||||
<!-- <version>2.0.8.RELEASE</version>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>io.projectreactor.spring</groupId>-->
|
||||
<!-- <artifactId>reactor-spring-context</artifactId>-->
|
||||
<!-- <version>2.0.7.RELEASE</version>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>io.netty</groupId>-->
|
||||
<!-- <artifactId>netty-all</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework.boot</groupId>-->
|
||||
<!-- <artifactId>spring-boot-starter-aop</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OAS -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths;
|
||||
import lombok.Generated;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.GroupedOpenApi;
|
||||
import org.springdoc.core.customizers.OpenApiCustomiser;
|
||||
@@ -19,6 +20,7 @@ import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@Generated
|
||||
public class OpenApiConfig {
|
||||
|
||||
@ConditionalOnProperty(name = "quartz-manager.oas.enabled")
|
||||
|
||||
@@ -55,7 +55,8 @@ public class SchedulerConfig {
|
||||
if (quartzProperties != null && quartzProperties.size() > 0)
|
||||
mergedProperties.putAll(quartzProperties);
|
||||
factory.setQuartzProperties(mergedProperties);
|
||||
factory.setAutoStartup(false);
|
||||
boolean isAutoStartup = mergedProperties.getProperty("org.quartz.scheduler.isAutoStartup") != null && mergedProperties.getProperty("org.quartz.scheduler.isAutoStartup").equals("true");
|
||||
factory.setAutoStartup(isAutoStartup);
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package it.fabioformosa.quartzmanager.api.configuration;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.common.config.QuartzManagerPaths;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
@@ -14,14 +15,16 @@ public class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
config.enableSimpleBroker("/topic");
|
||||
config.setApplicationDestinationPrefixes("/job");
|
||||
config.enableSimpleBroker("/topic"); //enable a simple memory-based message broker
|
||||
// on destinations prefixed with /topic
|
||||
config.setApplicationDestinationPrefixes("/job"); // it designates the prefix for messages
|
||||
// that are bound for methods annotated with @MessageMapping
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/quartz-manager/logs").setAllowedOrigins("/**").withSockJS();
|
||||
registry.addEndpoint("/quartz-manager/progress").setAllowedOrigins("/**").withSockJS();
|
||||
registry.addEndpoint(QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/logs").setAllowedOrigins("/**").withSockJS();
|
||||
registry.addEndpoint(QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/progress").setAllowedOrigins("/**").withSockJS();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RequestMapping(QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/jobs")
|
||||
@RequestMapping(JobController.JOB_CONTROLLER_BASE_URL)
|
||||
@SecurityRequirement(name = OpenAPIConfigConsts.QUARTZ_MANAGER_SEC_OAS_SCHEMA)
|
||||
@RestController
|
||||
public class JobController {
|
||||
public static final String JOB_CONTROLLER_BASE_URL = QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/jobs";
|
||||
private final JobService jobService;
|
||||
|
||||
public JobController(JobService jobService) {
|
||||
|
||||
@@ -8,7 +8,10 @@ import org.springframework.stereotype.Controller;
|
||||
@Controller
|
||||
public class WebsocketController {
|
||||
|
||||
@MessageMapping({ QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/logs", QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/progress" })
|
||||
@MessageMapping({
|
||||
QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/logs",
|
||||
QuartzManagerPaths.QUARTZ_MANAGER_BASE_CONTEXT_PATH + "/progress"
|
||||
})
|
||||
@SendTo("/topic/logs")
|
||||
public String subscribe() {
|
||||
return "subscribed";
|
||||
|
||||
@@ -23,6 +23,5 @@ public class JobKeyToJobDetailDTO extends AbstractBaseConverterToDTO<JobKey, Job
|
||||
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
|
||||
jobDetailDTO.setJobClassName(jobDetail.getJobClass().getName());
|
||||
jobDetailDTO.setDescription(jobDetail.getDescription());
|
||||
//jobDetail.getJobDataMap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ public class SchedulerToSchedulerDTO extends AbstractBaseConverterToDTO<Schedule
|
||||
protected void convert(Scheduler source, SchedulerDTO target) {
|
||||
target.setName(source.getSchedulerName());
|
||||
target.setInstanceId(source.getSchedulerInstanceId());
|
||||
target.setTriggerKeys(source.getTriggerKeys(GroupMatcher.anyTriggerGroup()));
|
||||
if(!source.isShutdown())
|
||||
target.setTriggerKeys(source.getTriggerKeys(GroupMatcher.anyTriggerGroup()));
|
||||
target.setStatus(buildTheSchedulerStatus(source));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package it.fabioformosa.quartzmanager.api.converters;
|
||||
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerCommandDTO;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.SimpleScheduleBuilder;
|
||||
import org.quartz.SimpleTrigger;
|
||||
import org.quartz.Trigger;
|
||||
@@ -19,6 +20,8 @@ public class SimpleTriggerCommandDTOToSimpleTrigger implements Converter<SimpleT
|
||||
if (triggerCommandDTO.getSimpleTriggerInputDTO().getEndDate() != null)
|
||||
triggerTriggerBuilder.endAt(triggerCommandDTO.getSimpleTriggerInputDTO().getEndDate());
|
||||
|
||||
if (triggerCommandDTO.getSimpleTriggerInputDTO().getJobDataMap() != null)
|
||||
triggerTriggerBuilder.usingJobData(new JobDataMap(triggerCommandDTO.getSimpleTriggerInputDTO().getJobDataMap()));
|
||||
|
||||
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
|
||||
if (triggerCommandDTO.getSimpleTriggerInputDTO().getRepeatInterval() != null)
|
||||
|
||||
@@ -14,6 +14,7 @@ public class SimpleTriggerToSimpleTriggerDTO extends TriggerToTriggerDTO<SimpleT
|
||||
target.setRepeatCount(source.getRepeatCount());
|
||||
target.setRepeatInterval(source.getRepeatInterval());
|
||||
target.setMisfireInstruction(source.getMisfireInstruction());
|
||||
target.setJobDataMap(source.getJobDataMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,6 +2,7 @@ package it.fabioformosa.quartzmanager.api.dto;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.enums.SchedulerStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.quartz.TriggerKey;
|
||||
@@ -11,6 +12,7 @@ import java.util.Set;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class SchedulerDTO {
|
||||
private String name;
|
||||
private String instanceId;
|
||||
|
||||
@@ -3,8 +3,9 @@ package it.fabioformosa.quartzmanager.api.dto;
|
||||
import it.fabioformosa.quartzmanager.api.validators.ValidTriggerRepetition;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.validation.constraints.Positive;
|
||||
import java.util.Map;
|
||||
|
||||
@ValidTriggerRepetition
|
||||
@SuperBuilder
|
||||
@@ -18,4 +19,7 @@ public class SimpleTriggerInputDTO extends TriggerCommandDTO implements TriggerR
|
||||
|
||||
@Positive
|
||||
private Long repeatInterval;
|
||||
|
||||
@Nullable
|
||||
private Map<String, ?> jobDataMap;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.quartz.JobDataMap;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@@ -23,4 +24,5 @@ public class TriggerDTO {
|
||||
private JobKeyDTO jobKeyDTO;
|
||||
private JobDetailDTO jobDetailDTO;
|
||||
private boolean mayFireAgain;
|
||||
private JobDataMap jobDataMap;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package it.fabioformosa.quartzmanager.api.dto;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TriggerFiredBundleDTO {
|
||||
|
||||
private int timesTriggered;
|
||||
@@ -18,22 +24,6 @@ public class TriggerFiredBundleDTO {
|
||||
|
||||
private String jobClass;
|
||||
|
||||
public Date getFinalFireTime() {
|
||||
return finalFireTime;
|
||||
}
|
||||
|
||||
public String getJobClass() {
|
||||
return jobClass;
|
||||
}
|
||||
|
||||
public String getJobKey() {
|
||||
return jobKey;
|
||||
}
|
||||
|
||||
public Date getNextFireTime() {
|
||||
return nextFireTime;
|
||||
}
|
||||
|
||||
public int getPercentage() {
|
||||
if (this.repeatCount <= 0)
|
||||
return -1;
|
||||
@@ -41,44 +31,4 @@ public class TriggerFiredBundleDTO {
|
||||
* 100);
|
||||
}
|
||||
|
||||
public Date getPreviousFireTime() {
|
||||
return previousFireTime;
|
||||
}
|
||||
|
||||
public int getRepeatCount() {
|
||||
return repeatCount;
|
||||
}
|
||||
|
||||
public int getTimesTriggered() {
|
||||
return timesTriggered;
|
||||
}
|
||||
|
||||
public void setFinalFireTime(Date finalFireTime) {
|
||||
this.finalFireTime = finalFireTime;
|
||||
}
|
||||
|
||||
public void setJobClass(String jobClass) {
|
||||
this.jobClass = jobClass;
|
||||
}
|
||||
|
||||
public void setJobKey(String jobKey) {
|
||||
this.jobKey = jobKey;
|
||||
}
|
||||
|
||||
public void setNextFireTime(Date nextFireTime) {
|
||||
this.nextFireTime = nextFireTime;
|
||||
}
|
||||
|
||||
public void setPreviousFireTime(Date previousFireTime) {
|
||||
this.previousFireTime = previousFireTime;
|
||||
}
|
||||
|
||||
public void setRepeatCount(int repeatCount) {
|
||||
this.repeatCount = repeatCount;
|
||||
}
|
||||
|
||||
public void setTimesTriggered(int timesTriggered) {
|
||||
this.timesTriggered = timesTriggered;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,11 +8,8 @@ public class ResourceConflictException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1791564636123821405L;
|
||||
|
||||
private final Long resourceId;
|
||||
|
||||
public ResourceConflictException(Long resourceId, String message) {
|
||||
super(message);
|
||||
this.resourceId = resourceId;
|
||||
super("Conflict on resourceID " + resourceId + " " + message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
package it.fabioformosa.quartzmanager.api.exceptions;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
@Getter
|
||||
public class TriggerNotFoundException extends Exception {
|
||||
|
||||
private final String name;
|
||||
|
||||
public TriggerNotFoundException(String name) {
|
||||
super("Trigger with name " + name + " not found!");
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,13 @@ public abstract class AbstractQuartzManagerJob implements Job {
|
||||
LogRecord logMsg = doIt(jobExecutionContext);
|
||||
log.info(logMsg.getMessage());
|
||||
|
||||
String triggerName = jobExecutionContext.getTrigger().getKey().getName();
|
||||
|
||||
logMsg.setThreadName(Thread.currentThread().getName());
|
||||
webSocketLogsNotifier.send(logMsg);
|
||||
webSocketLogsNotifier.send(triggerName, logMsg);
|
||||
|
||||
TriggerFiredBundleDTO triggerFiredBundleDTO = WebSocketProgressNotifier.buildTriggerFiredBundle(jobExecutionContext);
|
||||
webSocketProgressNotifier.send(triggerFiredBundleDTO);
|
||||
webSocketProgressNotifier.send(triggerName, triggerFiredBundleDTO);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package it.fabioformosa.quartzmanager.api.jobs.entities;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
@@ -8,6 +11,8 @@ import java.util.Date;
|
||||
* @author Fabio.Formosa
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@ToString
|
||||
public class LogRecord {
|
||||
|
||||
public enum LogType {
|
||||
@@ -27,41 +32,4 @@ public class LogRecord {
|
||||
date = new Date();
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getThreadName() {
|
||||
return threadName;
|
||||
}
|
||||
|
||||
public LogType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setDate(Date date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public void setMessage(String msg) {
|
||||
message = msg;
|
||||
}
|
||||
|
||||
public void setThreadName(String threadName) {
|
||||
this.threadName = threadName;
|
||||
}
|
||||
|
||||
public void setType(LogType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LogRecord [date=" + date + ", type=" + type + ", message=" + message + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package it.fabioformosa.quartzmanager.api.services;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerCommandDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.TriggerDTO;
|
||||
import it.fabioformosa.quartzmanager.api.dto.SimpleTriggerDTO;
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.TriggerNotFoundException;
|
||||
import org.quartz.*;
|
||||
@@ -34,7 +33,7 @@ public class SimpleTriggerService extends AbstractSchedulerService {
|
||||
return conversionService.convert(newSimpleTrigger, SimpleTriggerDTO.class);
|
||||
}
|
||||
|
||||
public TriggerDTO rescheduleSimpleTrigger(SimpleTriggerCommandDTO triggerCommandDTO) throws SchedulerException {
|
||||
public SimpleTriggerDTO rescheduleSimpleTrigger(SimpleTriggerCommandDTO triggerCommandDTO) throws SchedulerException {
|
||||
SimpleTrigger newSimpleTrigger = conversionService.convert(triggerCommandDTO, SimpleTrigger.class);
|
||||
|
||||
TriggerKey triggerKey = TriggerKey.triggerKey(triggerCommandDTO.getTriggerName());
|
||||
|
||||
@@ -14,7 +14,7 @@ public class WebSocketLogsNotifier implements WebhookSender<LogRecord> {
|
||||
private SimpMessageSendingOperations messagingTemplate;
|
||||
|
||||
@Override
|
||||
public void send(LogRecord logRecord) {
|
||||
messagingTemplate.convertAndSend(TOPIC_LOGS, logRecord);
|
||||
public void send(String triggerName, LogRecord logRecord) {
|
||||
messagingTemplate.convertAndSend(TOPIC_LOGS + "/" + triggerName, logRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ public class WebSocketProgressNotifier implements WebhookSender<TriggerFiredBund
|
||||
private SimpMessageSendingOperations messagingTemplate;
|
||||
|
||||
@Override
|
||||
public void send(TriggerFiredBundleDTO triggerFiredBundleDTO) {
|
||||
messagingTemplate.convertAndSend(TOPIC_PROGRESS, triggerFiredBundleDTO);
|
||||
public void send(String triggerName, TriggerFiredBundleDTO triggerFiredBundleDTO) {
|
||||
messagingTemplate.convertAndSend(TOPIC_PROGRESS + "/" + triggerName, triggerFiredBundleDTO);
|
||||
}
|
||||
|
||||
public static TriggerFiredBundleDTO buildTriggerFiredBundle(JobExecutionContext jobExecutionContext) {
|
||||
@@ -43,7 +43,7 @@ public class WebSocketProgressNotifier implements WebhookSender<TriggerFiredBund
|
||||
|
||||
JobDetail jobDetail = jobExecutionContext.getJobDetail();
|
||||
triggerFiredBundleDTO.setJobKey(jobDetail.getKey().getName());
|
||||
triggerFiredBundleDTO.setJobClass(trigger.getClass().getSimpleName());
|
||||
triggerFiredBundleDTO.setJobClass(jobDetail.getJobClass().getName());
|
||||
return triggerFiredBundleDTO;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,6 @@ package it.fabioformosa.quartzmanager.api.websockets;
|
||||
*/
|
||||
public interface WebhookSender<T> {
|
||||
|
||||
void send(T message);
|
||||
void send(String triggerName, T message);
|
||||
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import org.quartz.Scheduler;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
class SchedulerConfigTest {
|
||||
|
||||
@@ -16,11 +18,8 @@ class SchedulerConfigTest {
|
||||
public static final String QUARTZ_SCHEDULER_DEFAULT_NAME = "QuartzScheduler";
|
||||
|
||||
@Test
|
||||
void givenASchedulerName_whenTheSchedulerIsInstatiated_thenTheSchedulerHasThatName() throws Exception {
|
||||
QuartzModuleProperties quartzModuleProperties = new QuartzModuleProperties();
|
||||
quartzModuleProperties.getProperties().put("org.quartz.scheduler.instanceName", TEST_SCHEDULER_NAME);
|
||||
List<QuartzModuleProperties> quartzModulePropertiesList = new ArrayList<>();
|
||||
quartzModulePropertiesList.add(quartzModuleProperties);
|
||||
void givenASchedulerName_whenTheSchedulerIsInstantiated_thenTheSchedulerHasThatName() throws Exception {
|
||||
List<QuartzModuleProperties> quartzModulePropertiesList = getQuartzModulePropertiesWithASchedulerName(TEST_SCHEDULER_NAME);
|
||||
SchedulerConfig schedulerConfig = new SchedulerConfig(quartzModulePropertiesList);
|
||||
GenericApplicationContext applicationContext = new GenericApplicationContext();
|
||||
applicationContext.refresh();
|
||||
@@ -31,8 +30,16 @@ class SchedulerConfigTest {
|
||||
Assertions.assertThat(scheduler.getSchedulerName()).isEqualTo(TEST_SCHEDULER_NAME);
|
||||
}
|
||||
|
||||
private static List<QuartzModuleProperties> getQuartzModulePropertiesWithASchedulerName(String schedulerName) {
|
||||
QuartzModuleProperties quartzModuleProperties = new QuartzModuleProperties();
|
||||
quartzModuleProperties.getProperties().put("org.quartz.scheduler.instanceName", schedulerName);
|
||||
List<QuartzModuleProperties> quartzModulePropertiesList = new ArrayList<>();
|
||||
quartzModulePropertiesList.add(quartzModuleProperties);
|
||||
return quartzModulePropertiesList;
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenNoSchedulerName_whenTheSchedulerIsInstatiated_thenTheSchedulerHasTheDefaultName() throws Exception {
|
||||
void givenNoSchedulerName_whenTheSchedulerIsInstantiated_thenTheSchedulerHasTheDefaultName() throws Exception {
|
||||
QuartzModuleProperties quartzModuleProperties = new QuartzModuleProperties();
|
||||
List<QuartzModuleProperties> quartzModulePropertiesList = new ArrayList<>();
|
||||
quartzModulePropertiesList.add(quartzModuleProperties);
|
||||
@@ -46,4 +53,32 @@ class SchedulerConfigTest {
|
||||
Assertions.assertThat(scheduler.getSchedulerName()).isEqualTo(QUARTZ_SCHEDULER_DEFAULT_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenAManagedProperties_whenTheSchedulerIsInstantiated_thenTheManagedPropsHavePriority() throws Exception {
|
||||
List<QuartzModuleProperties> quartzModulePropertiesList = getQuartzModulePropertiesWithASchedulerName(TEST_SCHEDULER_NAME);
|
||||
SchedulerConfig schedulerConfig = new SchedulerConfig(quartzModulePropertiesList);
|
||||
GenericApplicationContext applicationContext = new GenericApplicationContext();
|
||||
applicationContext.refresh();
|
||||
|
||||
Properties managedProps = new Properties();
|
||||
String overridden_scheduler_name = "OVERRIDDEN_SCHEDULER_NAME";
|
||||
managedProps.put("org.quartz.scheduler.instanceName", overridden_scheduler_name);
|
||||
SchedulerFactoryBean schedulerFactoryBean = schedulerConfig.schedulerFactoryBean(schedulerConfig.jobFactory(applicationContext), managedProps);
|
||||
|
||||
schedulerFactoryBean.afterPropertiesSet();
|
||||
Scheduler scheduler = schedulerFactoryBean.getScheduler();
|
||||
Assertions.assertThat(scheduler.getSchedulerName()).isEqualTo(overridden_scheduler_name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenAnEmptyManagedPropFile_whenSchedulerConfigRuns_thenItReturnsAnEmptyPropList() throws IOException {
|
||||
List<QuartzModuleProperties> quartzModulePropertiesList = getQuartzModulePropertiesWithASchedulerName(TEST_SCHEDULER_NAME);
|
||||
SchedulerConfig schedulerConfig = new SchedulerConfig(quartzModulePropertiesList);
|
||||
GenericApplicationContext applicationContext = new GenericApplicationContext();
|
||||
applicationContext.refresh();
|
||||
|
||||
Properties properties = schedulerConfig.quartzProperties();
|
||||
Assertions.assertThat(properties).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package it.fabioformosa.quartzmanager.api.controllers;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests;
|
||||
import it.fabioformosa.quartzmanager.api.controllers.utils.TestUtils;
|
||||
import it.fabioformosa.quartzmanager.api.jobs.SampleJob;
|
||||
import it.fabioformosa.quartzmanager.api.services.JobService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
|
||||
@WebMvcTest(controllers = SimpleTriggerController.class, properties = {
|
||||
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
|
||||
})
|
||||
class JobControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean
|
||||
private JobService jobService;
|
||||
|
||||
@Test
|
||||
void whenGetListIsCalled_thenTheSimpleJobIsReturned() throws Exception {
|
||||
Mockito.when(jobService.getJobClasses()).thenReturn(List.of(SampleJob.class));
|
||||
|
||||
List<String> expectedJobs = List.of(SampleJob.class.getName());
|
||||
mockMvc.perform(get(JobController.JOB_CONTROLLER_BASE_URL)
|
||||
.contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(expectedJobs)));
|
||||
|
||||
Mockito.verify(jobService, Mockito.times(1)).getJobClasses();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package it.fabioformosa.quartzmanager.api.controllers;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||
|
||||
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
|
||||
@WebMvcTest(controllers = SimpleTriggerController.class, properties = {
|
||||
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
|
||||
})
|
||||
class ResourceConflictControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void whenAResourceConflictExceptionIsRaised_thenTheExceptionHandlerReturns409() throws Exception {
|
||||
mockMvc.perform(
|
||||
MockMvcRequestBuilders.post(TestController.TEST_CONTROLLER_BASE_URL + "/test-conflict")
|
||||
.contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isConflict());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package it.fabioformosa.quartzmanager.api.controllers;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.QuartManagerApplicationTests;
|
||||
import it.fabioformosa.quartzmanager.api.controllers.utils.TestUtils;
|
||||
import it.fabioformosa.quartzmanager.api.dto.SchedulerDTO;
|
||||
import it.fabioformosa.quartzmanager.api.enums.SchedulerStatus;
|
||||
import it.fabioformosa.quartzmanager.api.services.SchedulerService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
@ContextConfiguration(classes = {QuartManagerApplicationTests.class})
|
||||
@WebMvcTest(controllers = SimpleTriggerController.class, properties = {
|
||||
"quartz-manager.jobClassPackages=it.fabioformosa.quartzmanager.jobs"
|
||||
})
|
||||
class SchedulerControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean
|
||||
private SchedulerService schedulerService;
|
||||
|
||||
@Test
|
||||
void whenTheGetIsCalled_thenTheSchedulerServiceIsReturned() throws Exception {
|
||||
SchedulerDTO schedulerDTO = SchedulerDTO.builder()
|
||||
.name("TEST_SCHEDULER")
|
||||
.instanceId("testSchedulerId")
|
||||
.status(SchedulerStatus.STOPPED)
|
||||
.build();
|
||||
Mockito.when(schedulerService.getScheduler()).thenReturn(schedulerDTO);
|
||||
|
||||
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(MockMvcResultMatchers.content().json(TestUtils.toJson(schedulerDTO)));
|
||||
|
||||
Mockito.verify(schedulerService).getScheduler();
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenAScheduler_whenTheGetPausedIsCalled_then2xxReturned() throws Exception {
|
||||
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/pause")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(MockMvcResultMatchers.status().isNoContent())
|
||||
.andExpect(MockMvcResultMatchers.content().string(""));
|
||||
|
||||
Mockito.verify(schedulerService).standby();
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenAScheduler_whenTheGetResumedIsCalled_then2xxReturned() throws Exception {
|
||||
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/resume")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(MockMvcResultMatchers.status().isNoContent())
|
||||
.andExpect(MockMvcResultMatchers.content().string(""));
|
||||
|
||||
Mockito.verify(schedulerService).start();
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenAScheduler_whenTheGetRunIsCalled_then2xxReturned() throws Exception {
|
||||
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/run")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(MockMvcResultMatchers.status().isNoContent())
|
||||
.andExpect(MockMvcResultMatchers.content().string(""));
|
||||
|
||||
Mockito.verify(schedulerService).start();
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenAScheduler_whenTheGetStoppedIsCalled_then2xxReturned() throws Exception {
|
||||
mockMvc.perform(get(SchedulerController.SCHEDULER_CONTROLLER_BASE_URL + "/stop")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(MockMvcResultMatchers.status().isNoContent())
|
||||
.andExpect(MockMvcResultMatchers.content().string(""));
|
||||
|
||||
Mockito.verify(schedulerService).shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,8 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import java.util.Map;
|
||||
import static java.util.Map.entry;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
|
||||
@@ -68,6 +69,10 @@ class SimpleTriggerControllerTest {
|
||||
}
|
||||
|
||||
private SimpleTriggerInputDTO buildACompleteSimpleTriggerCommandDTO() {
|
||||
Map<String, ?> triggerJobDataMap = Map.ofEntries(
|
||||
entry("customTriggerData1", "value1"),
|
||||
entry("customTriggerData2", "value2")
|
||||
);
|
||||
return SimpleTriggerInputDTO.builder()
|
||||
.jobClass("it.fabioformosa.quartzmanager.api.jobs.SampleJob")
|
||||
.startDate(new Date())
|
||||
@@ -75,6 +80,7 @@ class SimpleTriggerControllerTest {
|
||||
.misfireInstruction(MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW)
|
||||
.repeatCount(5)
|
||||
.repeatInterval(1000L * 60 * 60)
|
||||
.jobDataMap(triggerJobDataMap)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package it.fabioformosa.quartzmanager.api.controllers;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.exceptions.ResourceConflictException;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RequestMapping(TestController.TEST_CONTROLLER_BASE_URL)
|
||||
@RestController
|
||||
public class TestController {
|
||||
|
||||
public static final String TEST_CONTROLLER_BASE_URL = "/test-controller";
|
||||
|
||||
@PostMapping("/test-conflict")
|
||||
public void raiseConflictException(){
|
||||
throw new ResourceConflictException(1000L, "another entity has found with the same ID");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,12 +2,23 @@ package it.fabioformosa.quartzmanager.api.controllers.utils;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.common.utils.DateUtils;
|
||||
import it.fabioformosa.quartzmanager.api.dto.*;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.SimpleScheduleBuilder;
|
||||
import org.quartz.SimpleTrigger;
|
||||
import org.quartz.Trigger;
|
||||
import org.quartz.TriggerBuilder;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.util.Map.entry;
|
||||
|
||||
public class TriggerUtils {
|
||||
|
||||
static public TriggerDTO getTriggerInstance(String triggerName){
|
||||
static public TriggerDTO getTriggerInstance(String triggerName) {
|
||||
return TriggerDTO.builder()
|
||||
.description("sample trigger")
|
||||
.endTime(DateUtils.addHoursToNow(2L))
|
||||
@@ -28,7 +39,7 @@ public class TriggerUtils {
|
||||
.build();
|
||||
}
|
||||
|
||||
static public SimpleTriggerDTO getSimpleTriggerInstance(String triggerName, SimpleTriggerInputDTO simpleTriggerInputDTO){
|
||||
static public SimpleTriggerDTO getSimpleTriggerInstance(String triggerName, SimpleTriggerInputDTO simpleTriggerInputDTO) {
|
||||
return SimpleTriggerDTO.builder()
|
||||
.description("simple trigger")
|
||||
.repeatCount(simpleTriggerInputDTO.getRepeatCount())
|
||||
@@ -48,10 +59,11 @@ public class TriggerUtils {
|
||||
.nextFireTime(DateUtils.addHoursToNow(1L))
|
||||
.priority(1)
|
||||
.startTime(DateUtils.fromLocalDateTimeToDate(LocalDateTime.now()))
|
||||
.jobDataMap(new JobDataMap(simpleTriggerInputDTO.getJobDataMap()))
|
||||
.build();
|
||||
}
|
||||
|
||||
static public SimpleTriggerDTO getSimpleTriggerInstance(String triggerName){
|
||||
static public SimpleTriggerDTO getSimpleTriggerInstance(String triggerName) {
|
||||
return SimpleTriggerDTO.builder()
|
||||
.description("simple trigger")
|
||||
.repeatCount(2)
|
||||
@@ -71,6 +83,44 @@ public class TriggerUtils {
|
||||
.nextFireTime(DateUtils.addHoursToNow(1L))
|
||||
.priority(1)
|
||||
.startTime(DateUtils.fromLocalDateTimeToDate(LocalDateTime.now()))
|
||||
.jobDataMap(new JobDataMap(Map.ofEntries(entry("customTriggerData1", "value1"))))
|
||||
.build();
|
||||
}
|
||||
|
||||
static public SimpleTrigger buildSimpleTrigger() {
|
||||
TriggerBuilder<Trigger> triggerTriggerBuilder = TriggerBuilder.newTrigger();
|
||||
triggerTriggerBuilder.startAt(new Date());
|
||||
triggerTriggerBuilder.endAt(DateUtils.addHoursToNow(1));
|
||||
triggerTriggerBuilder.usingJobData(new JobDataMap(Map.ofEntries(entry("data", "value"))));
|
||||
|
||||
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
|
||||
scheduleBuilder.withIntervalInMilliseconds(1000);
|
||||
scheduleBuilder.withRepeatCount(1);
|
||||
scheduleBuilder.withMisfireHandlingInstructionFireNow();
|
||||
|
||||
return triggerTriggerBuilder.withSchedule(
|
||||
scheduleBuilder
|
||||
)
|
||||
.withIdentity("simpleTrigger").build();
|
||||
}
|
||||
|
||||
static public SimpleTriggerCommandDTO buildSimpleTriggerCommandDTO(String triggerName) throws ParseException {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Date startDate = dateFormat.parse("2024-02-02");
|
||||
Date endDate = dateFormat.parse("2024-03-02");
|
||||
|
||||
SimpleTriggerInputDTO triggerInputDTO = SimpleTriggerInputDTO.builder()
|
||||
.misfireInstruction(MisfireInstruction.MISFIRE_INSTRUCTION_FIRE_NOW)
|
||||
.jobClass("sample.jobClass")
|
||||
.repeatCount(1)
|
||||
.repeatInterval(1000L)
|
||||
.startDate(startDate)
|
||||
.endDate(endDate)
|
||||
.jobDataMap(Map.ofEntries(entry("data", "value")))
|
||||
.build();
|
||||
return SimpleTriggerCommandDTO.builder()
|
||||
.triggerName(triggerName)
|
||||
.simpleTriggerInputDTO(triggerInputDTO)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package it.fabioformosa.quartzmanager.api.converters;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.dto.SchedulerDTO;
|
||||
import it.fabioformosa.quartzmanager.api.enums.SchedulerStatus;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.quartz.Scheduler;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
@SpringBootTest
|
||||
class SchedulerToSchedulerDTOTest {
|
||||
|
||||
@Autowired
|
||||
private Scheduler scheduler;
|
||||
|
||||
@Autowired
|
||||
private ConversionService conversionService;
|
||||
|
||||
|
||||
|
||||
@Order(1)
|
||||
@Test
|
||||
void givenAnActiveScheduler_whenItIsConverted_thenADtoIsReturned () throws SchedulerException {
|
||||
Assertions.assertThat(scheduler.isShutdown()).isFalse();
|
||||
SchedulerDTO schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
|
||||
Assertions.assertThat(schedulerDTO).isNotNull();
|
||||
Assertions.assertThat(schedulerDTO.getName()).isEqualTo(scheduler.getSchedulerName());
|
||||
Assertions.assertThat(schedulerDTO.getInstanceId()).isEqualTo(scheduler.getSchedulerInstanceId());
|
||||
}
|
||||
|
||||
@Order(2)
|
||||
@Test
|
||||
void givenAnActiveScheduler_whenItIsConverted_thenADtoHasAStatus () throws SchedulerException {
|
||||
scheduler.start();
|
||||
Assertions.assertThat(scheduler.isStarted()).isTrue();
|
||||
SchedulerDTO schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
|
||||
Assertions.assertThat(schedulerDTO.getStatus()).isEqualTo(SchedulerStatus.RUNNING);
|
||||
|
||||
scheduler.standby();
|
||||
Assertions.assertThat(scheduler.isInStandbyMode()).isTrue();
|
||||
schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
|
||||
Assertions.assertThat(schedulerDTO.getStatus()).isEqualTo(SchedulerStatus.PAUSED);
|
||||
|
||||
}
|
||||
|
||||
@DirtiesContext
|
||||
@Order(3)
|
||||
@Test
|
||||
void givenASchedulerInShutdown_whenItIsConverted_thenADtoIsReturnedWithNoTriggers () throws SchedulerException {
|
||||
Assertions.assertThat(scheduler.isShutdown()).isFalse();
|
||||
scheduler.shutdown(false);
|
||||
Assertions.assertThat(scheduler.isShutdown()).isTrue();
|
||||
|
||||
SchedulerDTO schedulerDTO = conversionService.convert(scheduler, SchedulerDTO.class);
|
||||
Assertions.assertThat(schedulerDTO).isNotNull();
|
||||
Assertions.assertThat(schedulerDTO.getName()).isEqualTo(scheduler.getSchedulerName());
|
||||
Assertions.assertThat(schedulerDTO.getInstanceId()).isEqualTo(scheduler.getSchedulerInstanceId());
|
||||
Assertions.assertThat(schedulerDTO.getTriggerKeys()).isNull();
|
||||
Assertions.assertThat(schedulerDTO.getStatus()).isEqualTo(SchedulerStatus.STOPPED);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user