mirror of
https://github.com/fabioformosa/quartz-manager.git
synced 2026-05-14 22:00:30 +09:00
Merge branch 'develop' into feature/#101_angular15_update
# Conflicts: # quartz-manager-frontend/package-lock.json # quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.html # quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.html # quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.ts # quartz-manager-frontend/src/app/views/manager/manager.component.html # quartz-manager-frontend/src/app/views/manager/manager.component.ts
This commit is contained in:
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
# .dockerignore
|
||||
quartz-manager-frontend/node_modules
|
||||
6
.github/workflows/maven-release.yml
vendored
6
.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 }}
|
||||
@@ -31,8 +31,8 @@ jobs:
|
||||
- name: Publish to maven central
|
||||
run: mvn deploy --file quartz-manager-parent/pom.xml --batch-mode -P "release-maven-central,build-webjar"
|
||||
env:
|
||||
MAVEN_USERNAME: ${{ secrets.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
|
||||
|
||||
1
.github/workflows/sonar-java.yml
vendored
1
.github/workflows/sonar-java.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
# paths: [ 'quartz-manager-parent/**' ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build:
|
||||
name: Build and analyze
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
## **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
|
||||
|
||||
|
||||
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"]
|
||||
@@ -3,6 +3,8 @@
|
||||
[](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)
|
||||
@@ -269,6 +271,7 @@ You can find some examples of different scenarios of projects which import Quart
|
||||
* *existing-security-header-based* - It demonstrates how Quartz Manager Security can coexists with another Spring Security Config present in your project. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Security.
|
||||
* *existing-quartz* - It demonstrates how to Quartz Manager can coexist with a Quartz instance already present in your project Imported libraries: Quartz Manager API, Quartz Manager UI.
|
||||
* *existing-quartz-and-storage* - It demonstrates how to Quartz Manager Persistence can coexist with a Quartz instance already present in your project. Imported libraries: Quartz Manager API, Quartz Manager UI and Quartz Manager Persistence.
|
||||
* *with-persistence* - It demonstrates how to import the Quartz Manager Persistence and get created the quartz tables automatically at the bootstrap
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-spring-starter": {
|
||||
"quartz-manager-ui": {
|
||||
"root": "",
|
||||
"prefix": "qrzmng",
|
||||
"sourceRoot": "src",
|
||||
@@ -19,7 +19,7 @@
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"allowedCommonJsDependencies": [
|
||||
"stompjs", "sockjs-client", "moment"
|
||||
"stompjs", "sockjs-client", "moment", "angular2-uuid"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
@@ -32,6 +32,14 @@
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
@@ -59,18 +67,18 @@
|
||||
"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": {
|
||||
@@ -82,7 +90,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"angular-spring-starter-e2e": {
|
||||
"quartz-manager-ui-e2e": {
|
||||
"root": "e2e",
|
||||
"sourceRoot": "e2e",
|
||||
"projectType": "application",
|
||||
@@ -91,7 +99,7 @@
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "./protractor.conf.js",
|
||||
"devServerTarget": "angular-spring-starter:serve"
|
||||
"devServerTarget": "quartz-manager-ui:serve"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
||||
40
quartz-manager-frontend/package-lock.json
generated
40
quartz-manager-frontend/package-lock.json
generated
@@ -27,7 +27,7 @@
|
||||
"@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",
|
||||
"@stomp/rx-stomp": "1.2.0",
|
||||
"core-js": "2.5.1",
|
||||
"hammerjs": "2.0.8",
|
||||
"moment": "^2.29.1",
|
||||
@@ -63,6 +63,7 @@
|
||||
"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",
|
||||
@@ -5615,19 +5616,19 @@
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@stomp/ng2-stompjs": {
|
||||
"version": "0.6.4",
|
||||
"license": "MIT",
|
||||
"node_modules/@stomp/rx-stomp": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@stomp/rx-stomp/-/rx-stomp-1.2.0.tgz",
|
||||
"integrity": "sha512-QLzPe3q0EwLB+cVWdUFEO4z5tyR+kPnXJANKN2UvB7Spz/oViHF959cydmXdQWaK7NHp86VO54TgFfXbHVnSLg==",
|
||||
"dependencies": {
|
||||
"@stomp/stompjs": "^4.0.0 >=4.0.2"
|
||||
"@stomp/stompjs": "^6.0.0 >=6.1.1",
|
||||
"angular2-uuid": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@stomp/stompjs": {
|
||||
"version": "4.0.8",
|
||||
"license": "Apache-2.0",
|
||||
"optionalDependencies": {
|
||||
"websocket": "^1.0.24"
|
||||
}
|
||||
"node_modules/@stomp/rx-stomp/node_modules/@stomp/stompjs": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-6.1.2.tgz",
|
||||
"integrity": "sha512-FHDTrIFM5Ospi4L3Xhj6v2+NzCVAeNDcBe95YjUWhWiRMrBF6uN3I7AUOlRgT6jU/2WQvvYK8ZaIxFfxFp+uHQ=="
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
@@ -6873,6 +6874,11 @@
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/angular2-uuid": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/angular2-uuid/-/angular2-uuid-1.1.1.tgz",
|
||||
"integrity": "sha512-6AXPyii9q8KBFGagybLNVmdGJLPcVZAhmv3odNGSJIA18LuJ3xOe6uN9GvjlQsGfdmYeuxlsGnFEUu7gPhkc+g=="
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
@@ -9734,6 +9740,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-sonarjs": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.16.0.tgz",
|
||||
"integrity": "sha512-al8ojAzcQW8Eu0tWn841ldhPpPcjrJ59TzzTfAVWR45bWvdAASCmrGl8vK0MWHyKVDdC0i17IGbtQQ1KgxLlVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"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,
|
||||
@@ -30,7 +32,7 @@
|
||||
"@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",
|
||||
"@stomp/rx-stomp": "1.2.0",
|
||||
"core-js": "2.5.1",
|
||||
"hammerjs": "2.0.8",
|
||||
"moment": "^2.29.1",
|
||||
@@ -66,6 +68,7 @@
|
||||
"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",
|
||||
|
||||
@@ -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 () {};
|
||||
|
||||
@@ -44,7 +44,8 @@ import {
|
||||
SchedulerControlComponent,
|
||||
LogsPanelComponent,
|
||||
ProgressPanelComponent,
|
||||
TriggerListComponent
|
||||
TriggerListComponent,
|
||||
SimpleTriggerConfigComponent
|
||||
} from './components';
|
||||
|
||||
import {
|
||||
@@ -53,14 +54,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';
|
||||
|
||||
@@ -68,31 +68,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: () => {
|
||||
@@ -170,19 +145,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]
|
||||
})
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4,13 +4,17 @@
|
||||
</mat-card-header>
|
||||
<mat-card-content class="flex flex-1 overflow-y-auto">
|
||||
<div class="flex-1">
|
||||
<div *ngIf="!logs || logs.length == 0" fxFill class="h-100" style="text-align: center;">
|
||||
<div *ngIf="!selectedTriggerName && (!logs || logs.length == 0)" fxFill class="h-100" style="text-align: center;">
|
||||
<img
|
||||
src="assets/image/logs.svg"
|
||||
alt="no logs"
|
||||
width="320"
|
||||
style="margin-top: 6em" />
|
||||
</div>
|
||||
<div *ngIf="isWaitingForLogs()" class="waitingLogs" fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="12px">
|
||||
<mat-spinner diameter="36"></mat-spinner>
|
||||
<div>Waiting for logs from {{selectedTriggerName}}...</div>
|
||||
</div>
|
||||
|
||||
<div id="logs" fxFill style="height: 100%">
|
||||
<div
|
||||
|
||||
@@ -15,11 +15,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,7 +6,7 @@
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<mat-card style="padding-bottom: 0">
|
||||
<mat-card style="padding-bottom: 0" [ngClass]="{'progress-updated': progressUpdated}">
|
||||
<mat-card-header style="padding-bottom: 16px;">
|
||||
<mat-card-subtitle><b>JOB PROGRESS</b></mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ 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>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ 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>;
|
||||
|
||||
@@ -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();
|
||||
@@ -220,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', () => {
|
||||
|
||||
@@ -34,18 +34,22 @@ export class SimpleTriggerConfigComponent implements OnInit {
|
||||
|
||||
scheduler: Scheduler;
|
||||
|
||||
triggerLoading = true;
|
||||
triggerLoading = false;
|
||||
|
||||
private fetchedTriggers = false;
|
||||
private triggerInProgress = false;
|
||||
|
||||
private selectedTriggerKey: TriggerKey;
|
||||
|
||||
private jobs: Array<String>;
|
||||
|
||||
enabledTriggerForm = false;
|
||||
|
||||
@Output()
|
||||
onNewTrigger = new EventEmitter<SimpleTrigger>();
|
||||
|
||||
@Output()
|
||||
triggerFormOpenChange = new EventEmitter<boolean>();
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private schedulerService: SchedulerService,
|
||||
@@ -57,33 +61,6 @@ export class SimpleTriggerConfigComponent implements OnInit {
|
||||
this.fetchJobs();
|
||||
}
|
||||
|
||||
onSubmitTriggerConfig = () => {
|
||||
console.log(this.existsATriggerInProgress());
|
||||
const schedulerServiceCall = this.existsATriggerInProgress() ?
|
||||
this.schedulerService.updateSimpleTriggerConfig : this.schedulerService.saveSimpleTriggerConfig;
|
||||
|
||||
const simpleTriggerCommand = this._fromReactiveFormToCommand();
|
||||
schedulerServiceCall(simpleTriggerCommand)
|
||||
.subscribe((retTrigger: SimpleTrigger) => {
|
||||
console.log(retTrigger);
|
||||
this.trigger = retTrigger;
|
||||
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(retTrigger));
|
||||
|
||||
this.fetchedTriggers = true;
|
||||
this.triggerInProgress = this.trigger.mayFireAgain;
|
||||
|
||||
if (schedulerServiceCall === this.schedulerService.saveSimpleTriggerConfig) {
|
||||
this.onNewTrigger.emit(retTrigger);
|
||||
}
|
||||
|
||||
this.closeTriggerForm();
|
||||
}, error => {
|
||||
console.error(error);
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
|
||||
});
|
||||
}
|
||||
|
||||
openTriggerForm() {
|
||||
this.simpleTriggerReactiveForm.enable();
|
||||
}
|
||||
@@ -92,18 +69,39 @@ export class SimpleTriggerConfigComponent implements OnInit {
|
||||
this.jobService.fetchJobs().subscribe(jobs => this.jobs = jobs);
|
||||
}
|
||||
|
||||
|
||||
openTriggerForm() {
|
||||
this.enabledTriggerForm = true;
|
||||
this.triggerFormOpenChange.emit(this.enabledTriggerForm);
|
||||
}
|
||||
|
||||
private closeTriggerForm() {
|
||||
this.simpleTriggerReactiveForm.disable();
|
||||
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;
|
||||
@@ -122,10 +120,44 @@ 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();
|
||||
};
|
||||
|
||||
onSubmitTriggerConfig = () => {
|
||||
const schedulerServiceCall = this.existsATriggerInProgress() ?
|
||||
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.triggerInProgress = this.trigger.mayFireAgain;
|
||||
|
||||
if (schedulerServiceCall === this.schedulerService.saveSimpleTriggerConfig) {
|
||||
this.onNewTrigger.emit(retTrigger);
|
||||
}
|
||||
|
||||
this.closeTriggerForm();
|
||||
}, error => {
|
||||
if (this.trigger) {
|
||||
this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger));
|
||||
}
|
||||
this.triggerLoading = false;
|
||||
}, () => {
|
||||
this.triggerLoading = false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private _triggerPeriodValidator(control: AbstractControl): ValidationErrors | null {
|
||||
const startDate = control.get('startDate');
|
||||
const endDate = control.get('endDate');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -83,16 +83,17 @@ export class TriggerListComponent implements OnInit {
|
||||
}
|
||||
|
||||
onNewTriggerBtnClicked() {
|
||||
if (this.getTriggerKeyList() && this.getTriggerKeyList().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,7 +35,7 @@ export class UserService {
|
||||
this.currentUser = user;
|
||||
this.router.initialNavigation();
|
||||
}, err => {
|
||||
console.log(`error retrieving current user due to ` + JSON.stringify(err));
|
||||
console.log('error retrieving current user due to ' + JSON.stringify(err));
|
||||
const httpErrorResponse = err as HttpErrorResponse;
|
||||
if (httpErrorResponse.status === 404) {
|
||||
this.isAnAnonymousUser = true;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
<qrzmng-simple-trigger-config
|
||||
fxFill
|
||||
[triggerKey]="selectedTriggerKey"
|
||||
(triggerFormOpenChange)="setNewTriggerFormOpened($event)"
|
||||
(onNewTrigger)="onNewTriggerCreated($event)">
|
||||
</qrzmng-simple-trigger-config>
|
||||
</div>
|
||||
@@ -27,14 +28,20 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="h-100 min-h-100 flex flex-column gap-6">
|
||||
<div class="flex flex-column" >
|
||||
<progress-panel class="flex-1"></progress-panel>
|
||||
</div>
|
||||
<div class="flex flex-column flex-1" style="max-height: calc(100% - 136px); min-height: calc(100% - 210px);">
|
||||
<logs-panel class="flex flex-1 h-100 max-h-100"></logs-panel>
|
||||
<div class="h-100 min-h-100 flex flex-column gap-6">
|
||||
<div class="flex flex-column" >
|
||||
<progress-panel class="flex-1"
|
||||
[triggerKey]=selectedTriggerKey
|
||||
>
|
||||
</progress-panel>
|
||||
</div>
|
||||
<div class="flex flex-column flex-1" style="max-height: calc(100% - 136px); min-height: calc(100% - 210px);">
|
||||
<logs-panel class="flex flex-1 h-100 max-h-100"
|
||||
[triggerKey]=selectedTriggerKey
|
||||
>
|
||||
</logs-panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { ConfigService, UserService } from '../../services';
|
||||
import { SimpleTrigger } from '../../model/simple-trigger.model';
|
||||
import { SimpleTrigger } from '../../model/simple-trigger.model';
|
||||
import { TriggerKey } from '../../model/triggerKey.model';
|
||||
import { SimpleTriggerConfigComponent } from '../../components/simple-trigger-config';
|
||||
import { TriggerListComponent } from '../../components';
|
||||
@@ -26,14 +25,24 @@ export class ManagerComponent implements OnInit {
|
||||
ngOnInit() {}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
2
quartz-manager-parent/.gitignore
vendored
2
quartz-manager-parent/.gitignore
vendored
@@ -4,4 +4,4 @@
|
||||
.classpath
|
||||
.project
|
||||
.idea
|
||||
*.iml
|
||||
/**/*.iml
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-parent</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<properties>
|
||||
<java.version>9</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<org.projectlombok.version>1.18.30</org.projectlombok.version>
|
||||
<maven-surefire-plugin.version>2.22.0</maven-surefire-plugin.version>
|
||||
<maven-failsafe-plugin.version>2.22.0</maven-failsafe-plugin.version>
|
||||
<jacoco-maven-plugin.version>0.8.8</jacoco-maven-plugin.version>
|
||||
@@ -50,6 +51,7 @@
|
||||
<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>
|
||||
@@ -78,27 +80,32 @@
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-common</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-api</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-security</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-persistence</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-starter-ui</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${org.projectlombok.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
@@ -133,6 +140,11 @@
|
||||
<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>
|
||||
@@ -240,26 +252,25 @@
|
||||
</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.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>${nexus-staging-maven-plugin.version}</version>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<serverId>ossrh</serverId>
|
||||
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
<publishingServerId>maven-central-release</publishingServerId>
|
||||
<autoPublish>true</autoPublish>
|
||||
<waitUntil>published</waitUntil>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-parent</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>quartz-manager-common</artifactId>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-parent</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>quartz-manager-starter-api</artifactId>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,6 +9,6 @@ package it.fabioformosa.quartzmanager.api.websockets;
|
||||
*/
|
||||
public interface WebhookSender<T> {
|
||||
|
||||
void send(T message);
|
||||
void send(String triggerName, T message);
|
||||
|
||||
}
|
||||
|
||||
@@ -4,16 +4,26 @@ import it.fabioformosa.quartzmanager.api.dto.TriggerFiredBundleDTO;
|
||||
import it.fabioformosa.quartzmanager.api.jobs.entities.LogRecord;
|
||||
import it.fabioformosa.quartzmanager.api.websockets.WebhookSender;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.quartz.*;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.quartz.JobBuilder;
|
||||
import org.quartz.JobDetail;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobKey;
|
||||
import org.quartz.ScheduleBuilder;
|
||||
import org.quartz.SimpleScheduleBuilder;
|
||||
import org.quartz.Trigger;
|
||||
import org.quartz.TriggerBuilder;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SampleJobTest {
|
||||
|
||||
@InjectMocks
|
||||
@@ -24,14 +34,16 @@ class SampleJobTest {
|
||||
@Mock
|
||||
private WebhookSender<LogRecord> webSocketLogsNotifier;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
@Captor
|
||||
private ArgumentCaptor<LogRecord> logRecordCaptor;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<TriggerFiredBundleDTO> triggerFiredBundleDTOCaptor;
|
||||
|
||||
@Test
|
||||
void givenASampleJob_whenTheJobIsExecuted_thenTheWebhookSendersAreCalled() {
|
||||
JobExecutionContext jobExecutionContext = Mockito.mock(JobExecutionContext.class);
|
||||
String triggerName = "test-trigger";
|
||||
|
||||
ScheduleBuilder schedulerBuilder = SimpleScheduleBuilder.simpleSchedule()
|
||||
.withRepeatCount(5)
|
||||
@@ -40,6 +52,7 @@ class SampleJobTest {
|
||||
.newJob(SampleJob.class).withIdentity(JobKey.jobKey("test-job"))
|
||||
.build();
|
||||
Trigger trigger = TriggerBuilder.newTrigger()
|
||||
.withIdentity(triggerName)
|
||||
.forJob(jobDetail)
|
||||
.withSchedule(schedulerBuilder)
|
||||
.build();
|
||||
@@ -47,24 +60,23 @@ class SampleJobTest {
|
||||
Mockito.when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail);
|
||||
|
||||
sampleJob.execute(jobExecutionContext);
|
||||
Mockito.verify(webSocketLogsNotifier).send(argThat(actualLogRecord -> {
|
||||
Assertions.assertThat(actualLogRecord.getMessage()).isEqualTo("Hello!");
|
||||
Assertions.assertThat(actualLogRecord.getType()).isEqualTo(LogRecord.LogType.INFO);
|
||||
Assertions.assertThat(actualLogRecord.getDate()).isNotNull();
|
||||
Assertions.assertThat(actualLogRecord.getThreadName()).isNotNull();
|
||||
return true;
|
||||
}));
|
||||
Mockito.verify(webSocketProgressNotifier).send(argThat(triggerFiredBundleDTO -> {
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getJobKey()).isEqualTo("test-job");
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getRepeatCount()).isEqualTo(6);
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getJobClass()).isEqualTo(SampleJob.class.getName());
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getTimesTriggered()).isZero();
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getNextFireTime()).isNull();
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getPercentage()).isZero();
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getFinalFireTime()).isNotNull();
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getPreviousFireTime()).isNull();
|
||||
return true;
|
||||
}));
|
||||
Mockito.verify(webSocketLogsNotifier).send(eq(triggerName), logRecordCaptor.capture());
|
||||
LogRecord actualLogRecord = logRecordCaptor.getValue();
|
||||
Assertions.assertThat(actualLogRecord.getMessage()).isEqualTo("Hello!");
|
||||
Assertions.assertThat(actualLogRecord.getType()).isEqualTo(LogRecord.LogType.INFO);
|
||||
Assertions.assertThat(actualLogRecord.getDate()).isNotNull();
|
||||
Assertions.assertThat(actualLogRecord.getThreadName()).isNotNull();
|
||||
|
||||
Mockito.verify(webSocketProgressNotifier).send(eq(triggerName), triggerFiredBundleDTOCaptor.capture());
|
||||
TriggerFiredBundleDTO triggerFiredBundleDTO = triggerFiredBundleDTOCaptor.getValue();
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getJobKey()).isEqualTo("test-job");
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getRepeatCount()).isEqualTo(6);
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getJobClass()).isEqualTo(SampleJob.class.getName());
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getTimesTriggered()).isZero();
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getNextFireTime()).isNull();
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getPercentage()).isZero();
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getFinalFireTime()).isNotNull();
|
||||
Assertions.assertThat(triggerFiredBundleDTO.getPreviousFireTime()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,13 +18,17 @@ import java.util.Date;
|
||||
@SpringBootTest
|
||||
class SimpleTriggerServiceIntegrationTest {
|
||||
|
||||
private static final String SAMPLE_JOB_CLASS = "it.fabioformosa.quartzmanager.api.jobs.SampleJob";
|
||||
private static final String SAMPLE_EXTRA_JOB_CLASS = "it.fabioformosa.samplepackage.SampleExtraJob";
|
||||
private static final String FIRST_TRIGGER_SUFFIX = "A";
|
||||
private static final String SECOND_TRIGGER_SUFFIX = "B";
|
||||
|
||||
@Autowired
|
||||
private SimpleTriggerService simpleTriggerService;
|
||||
|
||||
@Test
|
||||
void givenASimpleTriggerCommandDTOWithAllData_whenANewSimpleTriggerIsScheduled_thenShouldGetATriggertDTO() throws SchedulerException, ClassNotFoundException {
|
||||
String simpleTriggerTestName = "simpleTriggerWithAllData";
|
||||
String jobClass = "it.fabioformosa.quartzmanager.api.jobs.SampleJob";
|
||||
Date startDate = new Date();
|
||||
Date endDate = DateUtils.addHoursToNow(5);
|
||||
int repeatCount = 3;
|
||||
@@ -41,7 +45,7 @@ class SimpleTriggerServiceIntegrationTest {
|
||||
.repeatCount(repeatCount)
|
||||
.repeatInterval(repeatInterval)
|
||||
.misfireInstruction(misfireInstructionFireNow)
|
||||
.jobClass(jobClass)
|
||||
.jobClass(SAMPLE_JOB_CLASS)
|
||||
.build())
|
||||
.build();
|
||||
SimpleTriggerDTO simpleTriggerDTO = simpleTriggerService.scheduleSimpleTrigger(simpleTriggerCommand);
|
||||
@@ -61,12 +65,11 @@ class SimpleTriggerServiceIntegrationTest {
|
||||
@Test
|
||||
void givenASimpleTriggerCommandDTOWithMissingOptionalField_whenANewSimpleTriggerIsScheduled_thenShouldGetATriggertDTO() throws SchedulerException, ClassNotFoundException {
|
||||
String simpleTriggerTestName = "simpleTriggerWithoutOptionalData";
|
||||
String jobClass = "it.fabioformosa.quartzmanager.api.jobs.SampleJob";
|
||||
|
||||
SimpleTriggerCommandDTO simpleTriggerCommand = SimpleTriggerCommandDTO.builder()
|
||||
.triggerName(simpleTriggerTestName)
|
||||
.simpleTriggerInputDTO(SimpleTriggerInputDTO.builder()
|
||||
.jobClass(jobClass)
|
||||
.jobClass(SAMPLE_JOB_CLASS)
|
||||
.build())
|
||||
.build();
|
||||
SimpleTriggerDTO simpleTriggerDTO = simpleTriggerService.scheduleSimpleTrigger(simpleTriggerCommand);
|
||||
@@ -81,4 +84,49 @@ class SimpleTriggerServiceIntegrationTest {
|
||||
Assertions.assertThat(simpleTriggerDTO.getRepeatInterval()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenTwoSimpleTriggerCommandDTOsForTheSameJob_whenScheduled_thenShouldCreateTwoTriggers() throws SchedulerException, ClassNotFoundException {
|
||||
String triggerNamePrefix = "sameJobTrigger" + System.nanoTime();
|
||||
|
||||
SimpleTriggerDTO firstTrigger = simpleTriggerService.scheduleSimpleTrigger(
|
||||
buildSimpleTriggerCommand(triggerNamePrefix + FIRST_TRIGGER_SUFFIX, SAMPLE_JOB_CLASS)
|
||||
);
|
||||
SimpleTriggerDTO secondTrigger = simpleTriggerService.scheduleSimpleTrigger(
|
||||
buildSimpleTriggerCommand(triggerNamePrefix + SECOND_TRIGGER_SUFFIX, SAMPLE_JOB_CLASS)
|
||||
);
|
||||
|
||||
Assertions.assertThat(firstTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + FIRST_TRIGGER_SUFFIX);
|
||||
Assertions.assertThat(secondTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + SECOND_TRIGGER_SUFFIX);
|
||||
Assertions.assertThat(firstTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_JOB_CLASS);
|
||||
Assertions.assertThat(secondTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_JOB_CLASS);
|
||||
Assertions.assertThat(firstTrigger.getJobKeyDTO().getName()).isNotEqualTo(secondTrigger.getJobKeyDTO().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenTwoSimpleTriggerCommandDTOsForDifferentJobs_whenScheduled_thenShouldCreateTwoTriggers() throws SchedulerException, ClassNotFoundException {
|
||||
String triggerNamePrefix = "differentJobTrigger" + System.nanoTime();
|
||||
|
||||
SimpleTriggerDTO firstTrigger = simpleTriggerService.scheduleSimpleTrigger(
|
||||
buildSimpleTriggerCommand(triggerNamePrefix + FIRST_TRIGGER_SUFFIX, SAMPLE_JOB_CLASS)
|
||||
);
|
||||
SimpleTriggerDTO secondTrigger = simpleTriggerService.scheduleSimpleTrigger(
|
||||
buildSimpleTriggerCommand(triggerNamePrefix + SECOND_TRIGGER_SUFFIX, SAMPLE_EXTRA_JOB_CLASS)
|
||||
);
|
||||
|
||||
Assertions.assertThat(firstTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + FIRST_TRIGGER_SUFFIX);
|
||||
Assertions.assertThat(secondTrigger.getTriggerKeyDTO().getName()).isEqualTo(triggerNamePrefix + SECOND_TRIGGER_SUFFIX);
|
||||
Assertions.assertThat(firstTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_JOB_CLASS);
|
||||
Assertions.assertThat(secondTrigger.getJobDetailDTO().getJobClassName()).isEqualTo(SAMPLE_EXTRA_JOB_CLASS);
|
||||
}
|
||||
|
||||
private static SimpleTriggerCommandDTO buildSimpleTriggerCommand(String triggerName, String jobClass) {
|
||||
return SimpleTriggerCommandDTO.builder()
|
||||
.triggerName(triggerName)
|
||||
.simpleTriggerInputDTO(SimpleTriggerInputDTO.builder()
|
||||
.jobClass(jobClass)
|
||||
.startDate(DateUtils.addHoursToNow(1))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package it.fabioformosa.quartzmanager.api.websockets;
|
||||
|
||||
import it.fabioformosa.quartzmanager.api.dto.TriggerFiredBundleDTO;
|
||||
import it.fabioformosa.quartzmanager.api.jobs.entities.LogRecord;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||
|
||||
import static org.mockito.MockitoAnnotations.openMocks;
|
||||
|
||||
class WebSocketNotifierTest {
|
||||
|
||||
@InjectMocks
|
||||
private WebSocketLogsNotifier webSocketLogsNotifier;
|
||||
|
||||
@InjectMocks
|
||||
private WebSocketProgressNotifier webSocketProgressNotifier;
|
||||
|
||||
@Mock
|
||||
private SimpMessageSendingOperations messagingTemplate;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenATriggerName_whenALogIsSent_thenShouldSendItToTheTriggerLogsTopic() {
|
||||
LogRecord logRecord = new LogRecord(LogRecord.LogType.INFO, "Hello!");
|
||||
|
||||
webSocketLogsNotifier.send("trigger-1", logRecord);
|
||||
|
||||
Mockito.verify(messagingTemplate).convertAndSend("/topic/logs/trigger-1", logRecord);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenATriggerName_whenProgressIsSent_thenShouldSendItToTheTriggerProgressTopic() {
|
||||
TriggerFiredBundleDTO triggerFiredBundleDTO = new TriggerFiredBundleDTO();
|
||||
|
||||
webSocketProgressNotifier.send("trigger-1", triggerFiredBundleDTO);
|
||||
|
||||
Mockito.verify(messagingTemplate).convertAndSend("/topic/progress/trigger-1", triggerFiredBundleDTO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-parent</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>quartz-manager-starter-persistence</artifactId>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-parent</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>quartz-manager-starter-security</artifactId>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-parent</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>quartz-manager-starter-ui</artifactId>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: deploy.cloud.google.com/v1
|
||||
kind: Target
|
||||
metadata:
|
||||
name: dev
|
||||
annotations: {}
|
||||
labels: {}
|
||||
description: dev
|
||||
gke:
|
||||
cluster: projects/quartz-manager-test/locations/europe-west8-a/clusters/gke-cluster
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: deploy.cloud.google.com/v1
|
||||
kind: DeliveryPipeline
|
||||
metadata:
|
||||
name: quartz-manager-pipeline
|
||||
labels:
|
||||
app: quartz-manager-standalone
|
||||
description: quartz-manager-standalone delivery pipeline
|
||||
serialPipeline:
|
||||
stages:
|
||||
- targetId: dev
|
||||
profiles:
|
||||
- dev
|
||||
- targetId: staging
|
||||
profiles:
|
||||
- staging
|
||||
- targetId: prod
|
||||
profiles:
|
||||
- prod
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: deploy.cloud.google.com/v1
|
||||
kind: Target
|
||||
metadata:
|
||||
name: prod
|
||||
annotations: {}
|
||||
labels: {}
|
||||
description: prod
|
||||
requireApproval: true
|
||||
gke:
|
||||
cluster: projects/quartz-manager-test/locations/europe-west8-a/clusters/gke-cluster
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: deploy.cloud.google.com/v1
|
||||
kind: Target
|
||||
metadata:
|
||||
name: staging
|
||||
annotations: {}
|
||||
labels: {}
|
||||
description: staging
|
||||
requireApproval: true
|
||||
gke:
|
||||
cluster: projects/quartz-manager-test/locations/europe-west8-a/clusters/gke-cluster
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: quartz-manager-standalone
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.0.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "_HELM_APP_VERSION"
|
||||
@@ -0,0 +1,22 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "quartzmanager-standalone.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "quartzmanager-standalone.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "quartzmanager-standalone.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "quartzmanager-standalone.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "quartzmanager-standalone.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "quartzmanager-standalone.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "quartzmanager-standalone.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "quartzmanager-standalone.labels" -}}
|
||||
helm.sh/chart: {{ include "quartzmanager-standalone.chart" . }}
|
||||
{{ include "quartzmanager-standalone.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "quartzmanager-standalone.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "quartzmanager-standalone.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "quartzmanager-standalone.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "quartzmanager-standalone.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: "v1"
|
||||
kind: "ConfigMap"
|
||||
metadata:
|
||||
name: {{ include "quartzmanager-standalone.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ .Chart.Name }}
|
||||
data:
|
||||
envName: {{ .Values.config.envName | quote }}
|
||||
@@ -0,0 +1,68 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "quartzmanager-standalone.fullname" . }}
|
||||
labels:
|
||||
{{- include "quartzmanager-standalone.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "quartzmanager-standalone.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "quartzmanager-standalone.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "quartzmanager-standalone.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
{{/* livenessProbe:*/}}
|
||||
{{/* initialDelaySeconds: 30*/}}
|
||||
{{/* timeoutSeconds: 10*/}}
|
||||
{{/* httpGet:*/}}
|
||||
{{/* path: /*/}}
|
||||
{{/* port: 8080*/}}
|
||||
{{/* readinessProbe:*/}}
|
||||
{{/* initialDelaySeconds: 30*/}}
|
||||
{{/* timeoutSeconds: 10*/}}
|
||||
{{/* httpGet:*/}}
|
||||
{{/* path: /*/}}
|
||||
{{/* port: 8080*/}}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "quartzmanager-standalone.fullname" . }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,28 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "quartzmanager-standalone.fullname" . }}
|
||||
labels:
|
||||
{{- include "quartzmanager-standalone.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "quartzmanager-standalone.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,61 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "quartzmanager-standalone.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "quartzmanager-standalone.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "quartzmanager-standalone.fullname" . }}
|
||||
labels:
|
||||
{{- include "quartzmanager-standalone.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "quartzmanager-standalone.selectorLabels" . | nindent 4 }}
|
||||
@@ -0,0 +1,12 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "quartzmanager-standalone.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "quartzmanager-standalone.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,83 @@
|
||||
# Default values for hello-world.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: "europe-west8-docker.pkg.dev/quartz-manager-test/quartz-manager/quartz-manager-standalone/quartz-manager-standalone"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: false
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
config:
|
||||
envName: NA
|
||||
@@ -5,12 +5,12 @@
|
||||
<parent>
|
||||
<groupId>it.fabioformosa.quartz-manager</groupId>
|
||||
<artifactId>quartz-manager-parent</artifactId>
|
||||
<version>4.0.10-SNAPSHOT</version>
|
||||
<version>4.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>quartz-manager-web-showcase</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<packaging>war</packaging>
|
||||
<artifactId>quartz-manager-web-showcase</artifactId>
|
||||
|
||||
<name>Quartz Manager Web Showcase</name>
|
||||
<description>A webapp that imports Quartz Manager API lib and the frontend webjar</description>
|
||||
@@ -49,15 +49,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
<scope>provided</scope>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -123,30 +118,28 @@
|
||||
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<configuration>
|
||||
<source>9</source>
|
||||
<target>9</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<configuration>
|
||||
<source>9</source>
|
||||
<target>9</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package it.fabioformosa;
|
||||
|
||||
import lombok.Generated;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
|
||||
/**
|
||||
* ServletInitializer needs to deploy quartz-manager into a servlet container as a war file
|
||||
*
|
||||
* @author Fabio Formosa
|
||||
*
|
||||
*/
|
||||
@Generated
|
||||
public class ServletInitializer extends SpringBootServletInitializer {
|
||||
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
|
||||
return application.sources(QuartzManagerDemoApplication.class);
|
||||
}
|
||||
|
||||
}
|
||||
46
skaffold.yaml
Normal file
46
skaffold.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
apiVersion: skaffold/v4beta7
|
||||
kind: Config
|
||||
build:
|
||||
tagPolicy:
|
||||
envTemplate:
|
||||
template: "_IMAGE_TAG_POLICY"
|
||||
artifacts:
|
||||
- image: quartz-manager-standalone
|
||||
context: ./
|
||||
profiles:
|
||||
- name: dev
|
||||
deploy:
|
||||
helm:
|
||||
releases:
|
||||
- name: quartzmanager-standalone
|
||||
createNamespace: true
|
||||
namespace: quartzmanager-dev
|
||||
chartPath: quartz-manager-parent/quartz-manager-web-showcase/helm
|
||||
# valuesFiles:
|
||||
# - helm/envs/dev/values.yaml
|
||||
setValueTemplates:
|
||||
image.tag: "_IMAGE_TAG_POLICY"
|
||||
- name: staging
|
||||
deploy:
|
||||
helm:
|
||||
releases:
|
||||
- name: quartzmanager-standalone
|
||||
createNamespace: true
|
||||
namespace: quartzmanager-staging
|
||||
chartPath: quartz-manager-parent/quartz-manager-web-showcase/helm
|
||||
# valuesFiles:
|
||||
# - helm/envs/dev/values.yaml
|
||||
setValueTemplates:
|
||||
image.tag: "_IMAGE_TAG_POLICY"
|
||||
- name: prod
|
||||
deploy:
|
||||
helm:
|
||||
releases:
|
||||
- name: quartzmanager-standalone
|
||||
createNamespace: true
|
||||
namespace: quartzmanager-prod
|
||||
chartPath: quartz-manager-parent/quartz-manager-web-showcase/helm
|
||||
# valuesFiles:
|
||||
# - helm/envs/dev/values.yaml
|
||||
setValueTemplates:
|
||||
image.tag: "_IMAGE_TAG_POLICY"
|
||||
Reference in New Issue
Block a user