diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03e9a08 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +# .dockerignore +quartz-manager-frontend/node_modules diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml index cdf26e4..33d4a9a 100644 --- a/.github/workflows/maven-release.yml +++ b/.github/workflows/maven-release.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2093f93..832fcc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..64c6968 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..bfa16d0 --- /dev/null +++ b/cloudbuild.yaml @@ -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 diff --git a/quartz-manager-frontend/.eslintrc.sonar.json b/quartz-manager-frontend/.eslintrc.sonar.json new file mode 100644 index 0000000..d98850c --- /dev/null +++ b/quartz-manager-frontend/.eslintrc.sonar.json @@ -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 +} diff --git a/quartz-manager-frontend/angular.json b/quartz-manager-frontend/angular.json index 86c6522..21d6d19 100644 --- a/quartz-manager-frontend/angular.json +++ b/quartz-manager-frontend/angular.json @@ -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", @@ -31,6 +31,14 @@ "scripts": [] }, "configurations": { + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + }, "production": { "budgets": [ { @@ -58,18 +66,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": { @@ -81,7 +89,7 @@ } } }, - "angular-spring-starter-e2e": { + "quartz-manager-ui-e2e": { "root": "e2e", "sourceRoot": "e2e", "projectType": "application", @@ -90,7 +98,7 @@ "builder": "@angular-devkit/build-angular:protractor", "options": { "protractorConfig": "./protractor.conf.js", - "devServerTarget": "angular-spring-starter:serve" + "devServerTarget": "quartz-manager-ui:serve" } }, "lint": { diff --git a/quartz-manager-frontend/package-lock.json b/quartz-manager-frontend/package-lock.json index f3bbc29..90398ad 100644 --- a/quartz-manager-frontend/package-lock.json +++ b/quartz-manager-frontend/package-lock.json @@ -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", @@ -62,6 +62,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", @@ -4229,19 +4230,19 @@ "dev": true, "license": "MIT" }, - "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", @@ -5419,6 +5420,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", @@ -8599,6 +8605,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, @@ -22763,16 +22781,20 @@ "version": "3.1.0", "dev": true }, - "@stomp/ng2-stompjs": { - "version": "0.6.4", + "@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==", "requires": { - "@stomp/stompjs": "^4.0.0 >=4.0.2" - } - }, - "@stomp/stompjs": { - "version": "4.0.8", - "requires": { - "websocket": "^1.0.24" + "@stomp/stompjs": "^6.0.0 >=6.1.1", + "angular2-uuid": "^1.1.1" + }, + "dependencies": { + "@stomp/stompjs": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-6.1.2.tgz", + "integrity": "sha512-FHDTrIFM5Ospi4L3Xhj6v2+NzCVAeNDcBe95YjUWhWiRMrBF6uN3I7AUOlRgT6jU/2WQvvYK8ZaIxFfxFp+uHQ==" + } } }, "@tootallnate/once": { @@ -23670,6 +23692,11 @@ "fast-deep-equal": "^3.1.3" } }, + "angular2-uuid": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/angular2-uuid/-/angular2-uuid-1.1.1.tgz", + "integrity": "sha512-6AXPyii9q8KBFGagybLNVmdGJLPcVZAhmv3odNGSJIA18LuJ3xOe6uN9GvjlQsGfdmYeuxlsGnFEUu7gPhkc+g==" + }, "ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -26034,6 +26061,13 @@ "prettier-linter-helpers": "^1.0.0" } }, + "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, + "requires": {} + }, "eslint-scope": { "version": "5.1.1", "dev": true, diff --git a/quartz-manager-frontend/package.json b/quartz-manager-frontend/package.json index e6d640d..77c9d3b 100644 --- a/quartz-manager-frontend/package.json +++ b/quartz-manager-frontend/package.json @@ -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", @@ -65,6 +67,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", diff --git a/quartz-manager-frontend/src/_test.ts b/quartz-manager-frontend/src/_test.ts index 9bf7226..1cfaeb5 100644 --- a/quartz-manager-frontend/src/_test.ts +++ b/quartz-manager-frontend/src/_test.ts @@ -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 () {}; diff --git a/quartz-manager-frontend/src/app/app.module.ts b/quartz-manager-frontend/src/app/app.module.ts index f521893..54fbc83 100644 --- a/quartz-manager-frontend/src/app/app.module.ts +++ b/quartz-manager-frontend/src/app/app.module.ts @@ -43,7 +43,8 @@ import { SchedulerControlComponent, LogsPanelComponent, ProgressPanelComponent, - TriggerListComponent + TriggerListComponent, + SimpleTriggerConfigComponent } from './components'; import { @@ -52,14 +53,13 @@ import { UserService, SchedulerService, ConfigService, - ProgressWebsocketService, - LogsWebsocketService, getHtmlBaseUrl, + LogsRxWebsocketService, + ProgressRxWebsocketService, TriggerService } from './services'; import { ForbiddenComponent } from './views/forbidden/forbidden.component'; import { APP_BASE_HREF } from '@angular/common'; -import {SimpleTriggerConfigComponent} from './components/simple-trigger-config'; import JobService from './services/job.service'; import {GenericErrorComponent} from './views/error/genericError.component'; @@ -67,31 +67,6 @@ export function initUserFactory(userService: UserService) { return () => userService.fetchLoggedUser(); } - -// const stompConfig: StompConfig = { -// // Which server? -// url: 'ws://localhost:8080/quartz-manager/progress', - -// // Headers -// // Typical keys: login, passcode, host -// headers: { -// login: 'admin', -// passcode: 'admin' -// }, - -// // How often to heartbeat? -// // Interval in milliseconds, set to 0 to disable -// heartbeat_in: 0, // Typical value 0 - disabled -// heartbeat_out: 20000, // Typical value 20000 - every 20 seconds -// // Wait in milliseconds before attempting auto reconnect -// // Set to 0 to disable -// // Typical value 5000 (5 seconds) -// reconnect_delay: 5000, - -// // Will log diagnostics on console -// debug: true -// }; - export function jwtOptionsFactory(apiService: ApiService) { return { tokenGetter: () => { @@ -168,19 +143,13 @@ export function jwtOptionsFactory(apiService: ApiService) { SchedulerService, JobService, TriggerService, - ProgressWebsocketService, - LogsWebsocketService, + ProgressRxWebsocketService, + LogsRxWebsocketService, AuthService, ApiService, UserService, ConfigService, MatIconRegistry - // StompService, - // ServerSocket - // { - // provide: StompConfig, - // useValue: stompConfig - // } ], bootstrap: [AppComponent] }) diff --git a/quartz-manager-frontend/src/app/components/index.ts b/quartz-manager-frontend/src/app/components/index.ts index 2a77c4a..dd226d3 100644 --- a/quartz-manager-frontend/src/app/components/index.ts +++ b/quartz-manager-frontend/src/app/components/index.ts @@ -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'; diff --git a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.html b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.html index eccd084..7d8d78c 100644 --- a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.html +++ b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.html @@ -3,12 +3,16 @@ JOB LOGS -
-
- no logs -
-
-
+
+
+ no logs +
+
+
+ +
Waiting for logs from {{selectedTriggerName}}...
+
+
diff --git a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.scss b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.scss index 9529fd0..104f119 100644 --- a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.scss +++ b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.scss @@ -9,11 +9,18 @@ color: gold; } -#logs{ - font-size: 1em; -} - -/* ===== Scrollbar CSS ===== */ +#logs{ + font-size: 1em; +} + +.waitingLogs { + color: #6b7280; + height: 100%; + min-height: 180px; + text-align: center; +} + +/* ===== Scrollbar CSS ===== */ /* Firefox */ * { scrollbar-width: auto; diff --git a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.spec.ts b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.spec.ts new file mode 100644 index 0000000..414e6c3 --- /dev/null +++ b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.spec.ts @@ -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(); + 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(); + const secondMessages = new Subject(); + 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(); + const secondMessages = new Subject(); + 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(); + 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(); + }); + +}); diff --git a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.ts b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.ts index a9dc720..6b4403d 100644 --- a/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.ts +++ b/quartz-manager-frontend/src/app/components/logs-panel/logs-panel.component.ts @@ -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) { diff --git a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.html b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.html index 6816bcf..9e2c21e 100644 --- a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.html +++ b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.html @@ -6,12 +6,12 @@
--> - + JOB PROGRESS -
+
{{percentageStr}}
diff --git a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.scss b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.scss index 7aa05ec..51c57ca 100644 --- a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.scss +++ b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.scss @@ -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); + } +} diff --git a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.spec.ts b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.spec.ts new file mode 100644 index 0000000..05b3f86 --- /dev/null +++ b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.spec.ts @@ -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(); + 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(); + const secondMessages = new Subject(); + 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(); + const secondMessages = new Subject(); + 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(); + 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(); + }); + +}); diff --git a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.ts b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.ts index 0d50777..550305a 100644 --- a/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.ts +++ b/quartz-manager-frontend/src/app/components/progress-panel/progress-panel.component.ts @@ -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; - // // Subscription status - // public subscribed: boolean; - // // Array of historic message (bodies) - // public mq: Array = []; +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; + } + +} diff --git a/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.spec.ts b/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.spec.ts index 266fe11..2245bb0 100644 --- a/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.spec.ts +++ b/quartz-manager-frontend/src/app/components/scheduler-control/scheduler-control.component.spec.ts @@ -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; @@ -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(); diff --git a/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.spec.ts b/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.spec.ts index 1f66f48..422752f 100644 --- a/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.spec.ts +++ b/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.spec.ts @@ -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; @@ -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 = {jobClassName: 'TestJob', description: null}; + mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null); + mockTrigger.jobDetailDTO = {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 = {jobClassName: 'TestJob', description: null}; + mockTrigger.triggerKeyDTO = new TriggerKey(testTriggerName, null); + mockTrigger.jobDetailDTO = {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 = {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', () => { diff --git a/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.ts b/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.ts index 1b643ae..99ae80f 100644 --- a/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.ts +++ b/quartz-manager-frontend/src/app/components/simple-trigger-config/simple-trigger-config.component.ts @@ -34,9 +34,8 @@ export class SimpleTriggerConfigComponent implements OnInit { scheduler: Scheduler; - triggerLoading = true; + triggerLoading = false; - private fetchedTriggers = false; private triggerInProgress = false; private selectedTriggerKey: TriggerKey; @@ -48,6 +47,9 @@ export class SimpleTriggerConfigComponent implements OnInit { @Output() onNewTrigger = new EventEmitter(); + @Output() + triggerFormOpenChange = new EventEmitter(); + constructor( private formBuilder: UntypedFormBuilder, private schedulerService: SchedulerService, @@ -65,18 +67,37 @@ export class SimpleTriggerConfigComponent implements OnInit { openTriggerForm() { this.enabledTriggerForm = true; + this.triggerFormOpenChange.emit(this.enabledTriggerForm); } private closeTriggerForm() { this.enabledTriggerForm = false; + this.triggerFormOpenChange.emit(this.enabledTriggerForm); } @Input() set triggerKey(triggerKey: TriggerKey) { - this.selectedTriggerKey = {...triggerKey} as TriggerKey; - this.fetchSelectedTrigger(); + if (!triggerKey) { + this.openNewTriggerForm(); + } else if (!this.selectedTriggerKey || this.selectedTriggerKey.name !== triggerKey.name) { + this._resetTheTrigger(); + this.selectedTriggerKey = {...triggerKey} as TriggerKey; + this.fetchSelectedTrigger(); + this.closeTriggerForm(); + } } + openNewTriggerForm() { + this._resetTheTrigger(); + this.openTriggerForm(); + } + + private _resetTheTrigger() { + this.trigger = null; + this.triggerInProgress = false; + this.selectedTriggerKey = null; + this.simpleTriggerReactiveForm.reset(new SimpleTriggerReactiveForm()); + } fetchSelectedTrigger = () => { this.triggerLoading = true; @@ -94,7 +115,11 @@ export class SimpleTriggerConfigComponent implements OnInit { existsATriggerInProgress = (): boolean => this.trigger && this.triggerInProgress; onResetReactiveForm = () => { - this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger)); + if (this.trigger) { + this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger)); + } else { + this.simpleTriggerReactiveForm.reset(new SimpleTriggerReactiveForm()); + } this.closeTriggerForm(); }; @@ -103,13 +128,13 @@ export class SimpleTriggerConfigComponent implements OnInit { this.schedulerService.updateSimpleTriggerConfig : this.schedulerService.saveSimpleTriggerConfig; const simpleTriggerCommand = this._fromReactiveFormToCommand(); + this.triggerLoading = true; schedulerServiceCall(simpleTriggerCommand) .subscribe((retTrigger: SimpleTrigger) => { this.trigger = retTrigger; this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(retTrigger)); - this.fetchedTriggers = true; this.triggerInProgress = this.trigger.mayFireAgain; if (schedulerServiceCall === this.schedulerService.saveSimpleTriggerConfig) { @@ -118,8 +143,13 @@ export class SimpleTriggerConfigComponent implements OnInit { this.closeTriggerForm(); }, error => { - this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger)); - }); + if (this.trigger) { + this.simpleTriggerReactiveForm.setValue(this._fromTriggerToReactiveForm(this.trigger)); + } + this.triggerLoading = false; + }, () => { +this.triggerLoading = false +}); } diff --git a/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.html b/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.html index 88677dd..7718dfb 100644 --- a/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.html +++ b/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.html @@ -9,7 +9,9 @@ + [ngClass]="{'selectedTrigger': selectedTrigger && selectedTrigger.name==triggerKey.name}" + (click)="selectTrigger(triggerKey)" + > {{ triggerKey.name }} diff --git a/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.ts b/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.ts index 9e98f91..3a41163 100644 --- a/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.ts +++ b/quartz-manager-frontend/src/app/components/trigger-list/trigger-list.component.ts @@ -81,16 +81,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); } } diff --git a/quartz-manager-frontend/src/app/polyfills.ts b/quartz-manager-frontend/src/app/polyfills.ts index 622efa0..766305d 100644 --- a/quartz-manager-frontend/src/app/polyfills.ts +++ b/quartz-manager-frontend/src/app/polyfills.ts @@ -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'; diff --git a/quartz-manager-frontend/src/app/services/index.ts b/quartz-manager-frontend/src/app/services/index.ts index a3e5802..81435d0 100644 --- a/quartz-manager-frontend/src/app/services/index.ts +++ b/quartz-manager-frontend/src/app/services/index.ts @@ -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' diff --git a/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.spec.ts b/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.spec.ts new file mode 100644 index 0000000..64fa05e --- /dev/null +++ b/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.spec.ts @@ -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='); + }); +}); diff --git a/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.ts b/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.ts new file mode 100644 index 0000000..76a07b4 --- /dev/null +++ b/quartz-manager-frontend/src/app/services/logs.rx-websocket.service.ts @@ -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); + } + }); + } +} diff --git a/quartz-manager-frontend/src/app/services/logs.websocket.service.ts b/quartz-manager-frontend/src/app/services/logs.websocket.service.ts deleted file mode 100644 index 97e77dd..0000000 --- a/quartz-manager-frontend/src/app/services/logs.websocket.service.ts +++ /dev/null @@ -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)) - } - -} diff --git a/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.spec.ts b/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.spec.ts new file mode 100644 index 0000000..cf32c29 --- /dev/null +++ b/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.spec.ts @@ -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='); + }); +}); diff --git a/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.ts b/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.ts new file mode 100644 index 0000000..647f668 --- /dev/null +++ b/quartz-manager-frontend/src/app/services/progress.rx-websocket.service.ts @@ -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); + } + }); + } +} diff --git a/quartz-manager-frontend/src/app/services/progress.websocket.service.ts b/quartz-manager-frontend/src/app/services/progress.websocket.service.ts deleted file mode 100644 index 7322e6f..0000000 --- a/quartz-manager-frontend/src/app/services/progress.websocket.service.ts +++ /dev/null @@ -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)) - } - -} diff --git a/quartz-manager-frontend/src/app/services/rx-stomp.service.ts b/quartz-manager-frontend/src/app/services/rx-stomp.service.ts new file mode 100644 index 0000000..dfdd16a --- /dev/null +++ b/quartz-manager-frontend/src/app/services/rx-stomp.service.ts @@ -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(); + } + +} diff --git a/quartz-manager-frontend/src/app/services/user.service.ts b/quartz-manager-frontend/src/app/services/user.service.ts index ae0bf61..224953f 100644 --- a/quartz-manager-frontend/src/app/services/user.service.ts +++ b/quartz-manager-frontend/src/app/services/user.service.ts @@ -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; diff --git a/quartz-manager-frontend/src/app/services/websocket.service.ts b/quartz-manager-frontend/src/app/services/websocket.service.ts deleted file mode 100644 index 599f936..0000000 --- a/quartz-manager-frontend/src/app/services/websocket.service.ts +++ /dev/null @@ -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 -} - -export interface QuartzManagerWebsocketMessage { - type: string; - message: any; - headers: any; - self: boolean; -} - -export class WebsocketService { - - _options: SocketOption; - - _socket: SocketEndpoint = new SocketEndpoint(); - - observableStompConnection: Observable; - subscribers: Array = []; - subscriberIndex = 0; - - _messageIds: Array = []; - - 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 = {}; - 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; - } - - -} diff --git a/quartz-manager-frontend/src/app/views/manager/manager.component.html b/quartz-manager-frontend/src/app/views/manager/manager.component.html index 05b11c9..1428332 100644 --- a/quartz-manager-frontend/src/app/views/manager/manager.component.html +++ b/quartz-manager-frontend/src/app/views/manager/manager.component.html @@ -19,18 +19,25 @@
- - + +
- - + + + +
diff --git a/quartz-manager-frontend/src/app/views/manager/manager.component.ts b/quartz-manager-frontend/src/app/views/manager/manager.component.ts index 01ce9f1..20a9a7d 100644 --- a/quartz-manager-frontend/src/app/views/manager/manager.component.ts +++ b/quartz-manager-frontend/src/app/views/manager/manager.component.ts @@ -1,8 +1,4 @@ import {Component, OnInit, ViewChild} from '@angular/core'; -import { - ConfigService, - UserService -} from '../../services'; import {SimpleTrigger} from '../../model/simple-trigger.model'; import {TriggerKey} from '../../model/triggerKey.model'; import {SimpleTriggerConfigComponent} from '../../components/simple-trigger-config'; @@ -32,15 +28,25 @@ export class ManagerComponent implements OnInit { } onNewTriggerRequested() { - this.triggerConfigComponent.openTriggerForm(); + this.selectedTriggerKey = null; + this.newTriggerFormOpened = true; + if (this.triggerConfigComponent) { + this.triggerConfigComponent.openNewTriggerForm(); + } } onNewTriggerCreated(newTrigger: SimpleTrigger) { this.triggerListComponent.onNewTrigger(newTrigger); + this.newTriggerFormOpened = false; } setSelectedTrigger(triggerKey: TriggerKey) { this.selectedTriggerKey = triggerKey; + this.newTriggerFormOpened = false; + } + + setNewTriggerFormOpened(opened: boolean) { + this.newTriggerFormOpened = opened; } } diff --git a/quartz-manager-frontend/src/polyfills.ts b/quartz-manager-frontend/src/polyfills.ts index ecc7cec..95ab387 100644 --- a/quartz-manager-frontend/src/polyfills.ts +++ b/quartz-manager-frontend/src/polyfills.ts @@ -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'; diff --git a/quartz-manager-frontend/src/typings.d.ts b/quartz-manager-frontend/src/typings.d.ts index ef5c7bd..388fb92 100644 --- a/quartz-manager-frontend/src/typings.d.ts +++ b/quartz-manager-frontend/src/typings.d.ts @@ -1,5 +1,5 @@ /* SystemJS module definition */ -declare var module: NodeModule; +declare let module: NodeModule; interface NodeModule { id: string; } diff --git a/quartz-manager-parent/.gitignore b/quartz-manager-parent/.gitignore index 29e1e7f..a3232ff 100644 --- a/quartz-manager-parent/.gitignore +++ b/quartz-manager-parent/.gitignore @@ -4,4 +4,4 @@ .classpath .project .idea -*.iml +/**/*.iml diff --git a/quartz-manager-parent/pom.xml b/quartz-manager-parent/pom.xml index b23625b..15f0ba1 100644 --- a/quartz-manager-parent/pom.xml +++ b/quartz-manager-parent/pom.xml @@ -10,7 +10,7 @@ it.fabioformosa.quartz-manager quartz-manager-parent - 4.0.9 + 4.0.11-SNAPSHOT pom @@ -43,6 +43,7 @@ 9 UTF-8 + 1.18.30 2.22.0 2.22.0 0.8.8 @@ -50,6 +51,7 @@ 1.6.7 2.5.3 3.0.1 + 3.11.0.3922 fabioformosa https://sonarcloud.io @@ -78,27 +80,32 @@ it.fabioformosa.quartz-manager quartz-manager-common - 4.0.9 + 4.0.11-SNAPSHOT it.fabioformosa.quartz-manager quartz-manager-starter-api - 4.0.9 + 4.0.11-SNAPSHOT it.fabioformosa.quartz-manager quartz-manager-starter-security - 4.0.9 + 4.0.11-SNAPSHOT it.fabioformosa.quartz-manager quartz-manager-starter-persistence - 4.0.9 + 4.0.11-SNAPSHOT it.fabioformosa.quartz-manager quartz-manager-starter-ui - 4.0.9 + 4.0.11-SNAPSHOT + + + org.projectlombok + lombok + ${org.projectlombok.version} @@ -133,6 +140,11 @@ maven-failsafe-plugin ${maven-failsafe-plugin.version} + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar-maven-plugin.version} + org.jacoco jacoco-maven-plugin @@ -240,26 +252,25 @@ - ossrh - https://oss.sonatype.org/content/repositories/snapshots + maven-central-release + https://central.sonatype.com/repository/maven-snapshots/ - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - + maven-central-release + https://central.sonatype.com - org.sonatype.plugins - nexus-staging-maven-plugin - ${nexus-staging-maven-plugin.version} + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://oss.sonatype.org/ - true + maven-central-release + true + published diff --git a/quartz-manager-parent/quartz-manager-common/pom.xml b/quartz-manager-parent/quartz-manager-common/pom.xml index 8e18ae9..2eec18c 100644 --- a/quartz-manager-parent/quartz-manager-common/pom.xml +++ b/quartz-manager-parent/quartz-manager-common/pom.xml @@ -3,7 +3,7 @@ it.fabioformosa.quartz-manager quartz-manager-parent - 4.0.9 + 4.0.11-SNAPSHOT quartz-manager-common diff --git a/quartz-manager-parent/quartz-manager-starter-api/pom.xml b/quartz-manager-parent/quartz-manager-starter-api/pom.xml index 053f50b..557bc03 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/pom.xml +++ b/quartz-manager-parent/quartz-manager-starter-api/pom.xml @@ -5,7 +5,7 @@ it.fabioformosa.quartz-manager quartz-manager-parent - 4.0.9 + 4.0.11-SNAPSHOT quartz-manager-starter-api diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/jobs/AbstractQuartzManagerJob.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/jobs/AbstractQuartzManagerJob.java index 609ee8d..20d6c90 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/jobs/AbstractQuartzManagerJob.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/jobs/AbstractQuartzManagerJob.java @@ -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); } } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketLogsNotifier.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketLogsNotifier.java index 9ced563..9cdbfb7 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketLogsNotifier.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketLogsNotifier.java @@ -14,7 +14,7 @@ public class WebSocketLogsNotifier implements WebhookSender { 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); } } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketProgressNotifier.java b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketProgressNotifier.java index 7eeba6d..d616bf9 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketProgressNotifier.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/main/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketProgressNotifier.java @@ -20,8 +20,8 @@ public class WebSocketProgressNotifier implements WebhookSender { - void send(T message); + void send(String triggerName, T message); } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/jobs/SampleJobTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/jobs/SampleJobTest.java index f56bd0d..2ffde76 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/jobs/SampleJobTest.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/jobs/SampleJobTest.java @@ -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 webSocketLogsNotifier; - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } + @Captor + private ArgumentCaptor logRecordCaptor; + + @Captor + private ArgumentCaptor 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 diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceIntegrationTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceIntegrationTest.java index f26e829..74c633d 100644 --- a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceIntegrationTest.java +++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/services/SimpleTriggerServiceIntegrationTest.java @@ -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(); + } + } diff --git a/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketNotifierTest.java b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketNotifierTest.java new file mode 100644 index 0000000..94b38ad --- /dev/null +++ b/quartz-manager-parent/quartz-manager-starter-api/src/test/java/it/fabioformosa/quartzmanager/api/websockets/WebSocketNotifierTest.java @@ -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); + } + +} diff --git a/quartz-manager-parent/quartz-manager-starter-persistence/pom.xml b/quartz-manager-parent/quartz-manager-starter-persistence/pom.xml index 52f3e03..20811ef 100644 --- a/quartz-manager-parent/quartz-manager-starter-persistence/pom.xml +++ b/quartz-manager-parent/quartz-manager-starter-persistence/pom.xml @@ -3,7 +3,7 @@ it.fabioformosa.quartz-manager quartz-manager-parent - 4.0.9 + 4.0.11-SNAPSHOT quartz-manager-starter-persistence diff --git a/quartz-manager-parent/quartz-manager-starter-security/pom.xml b/quartz-manager-parent/quartz-manager-starter-security/pom.xml index 53d5f71..f69ee60 100644 --- a/quartz-manager-parent/quartz-manager-starter-security/pom.xml +++ b/quartz-manager-parent/quartz-manager-starter-security/pom.xml @@ -4,7 +4,7 @@ it.fabioformosa.quartz-manager quartz-manager-parent - 4.0.9 + 4.0.11-SNAPSHOT quartz-manager-starter-security diff --git a/quartz-manager-parent/quartz-manager-starter-ui/pom.xml b/quartz-manager-parent/quartz-manager-starter-ui/pom.xml index b4c0f64..f72cd7f 100644 --- a/quartz-manager-parent/quartz-manager-starter-ui/pom.xml +++ b/quartz-manager-parent/quartz-manager-starter-ui/pom.xml @@ -4,7 +4,7 @@ it.fabioformosa.quartz-manager quartz-manager-parent - 4.0.9 + 4.0.11-SNAPSHOT quartz-manager-starter-ui diff --git a/quartz-manager-parent/quartz-manager-web-showcase/deploy/dev.yaml b/quartz-manager-parent/quartz-manager-web-showcase/deploy/dev.yaml new file mode 100644 index 0000000..1883818 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/deploy/dev.yaml @@ -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 diff --git a/quartz-manager-parent/quartz-manager-web-showcase/deploy/pipeline.yaml b/quartz-manager-parent/quartz-manager-web-showcase/deploy/pipeline.yaml new file mode 100644 index 0000000..3d3c7fa --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/deploy/pipeline.yaml @@ -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 diff --git a/quartz-manager-parent/quartz-manager-web-showcase/deploy/prod.yaml b/quartz-manager-parent/quartz-manager-web-showcase/deploy/prod.yaml new file mode 100644 index 0000000..ea621a6 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/deploy/prod.yaml @@ -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 diff --git a/quartz-manager-parent/quartz-manager-web-showcase/deploy/staging.yaml b/quartz-manager-parent/quartz-manager-web-showcase/deploy/staging.yaml new file mode 100644 index 0000000..ecaf3a7 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/deploy/staging.yaml @@ -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 diff --git a/quartz-manager-parent/quartz-manager-web-showcase/helm/Chart.yaml b/quartz-manager-parent/quartz-manager-web-showcase/helm/Chart.yaml new file mode 100644 index 0000000..5a63105 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/helm/Chart.yaml @@ -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" diff --git a/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/NOTES.txt b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/NOTES.txt new file mode 100644 index 0000000..d432c5c --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/NOTES.txt @@ -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 }} diff --git a/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/_helpers.yaml b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/_helpers.yaml new file mode 100644 index 0000000..59d7d95 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/_helpers.yaml @@ -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 }} diff --git a/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/configmap.yaml b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/configmap.yaml new file mode 100644 index 0000000..aa87e0a --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/configmap.yaml @@ -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 }} diff --git a/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/deployment.yaml b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/deployment.yaml new file mode 100644 index 0000000..760aeea --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/deployment.yaml @@ -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 }} diff --git a/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/hpa.yaml b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/hpa.yaml new file mode 100644 index 0000000..67973b7 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/hpa.yaml @@ -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 }} diff --git a/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/ingress.yaml b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/ingress.yaml new file mode 100644 index 0000000..46de18b --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/ingress.yaml @@ -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 }} diff --git a/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/service.yaml b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/service.yaml new file mode 100644 index 0000000..96bcb76 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/service.yaml @@ -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 }} diff --git a/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/serviceaccount.yaml b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/serviceaccount.yaml new file mode 100644 index 0000000..6e11e89 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/helm/templates/serviceaccount.yaml @@ -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 }} diff --git a/quartz-manager-parent/quartz-manager-web-showcase/helm/values.yaml b/quartz-manager-parent/quartz-manager-web-showcase/helm/values.yaml new file mode 100644 index 0000000..dab99e5 --- /dev/null +++ b/quartz-manager-parent/quartz-manager-web-showcase/helm/values.yaml @@ -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 diff --git a/quartz-manager-parent/quartz-manager-web-showcase/pom.xml b/quartz-manager-parent/quartz-manager-web-showcase/pom.xml index 6e51702..0d80c5a 100644 --- a/quartz-manager-parent/quartz-manager-web-showcase/pom.xml +++ b/quartz-manager-parent/quartz-manager-web-showcase/pom.xml @@ -5,12 +5,12 @@ it.fabioformosa.quartz-manager quartz-manager-parent - 4.0.9 + 4.0.11-SNAPSHOT - quartz-manager-web-showcase + jar - war + quartz-manager-web-showcase Quartz Manager Web Showcase A webapp that imports Quartz Manager API lib and the frontend webjar @@ -49,15 +49,10 @@ org.springframework.boot spring-boot-devtools - - org.springframework.boot - spring-boot-configuration-processor - true - - + org.springframework.boot - spring-boot-starter-tomcat - provided + spring-boot-configuration-processor + true org.springframework.boot @@ -123,30 +118,28 @@ - - - - org.springframework.boot - spring-boot-maven-plugin - - - - repackage - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.0 - - 9 - 9 - - - - + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 9 + 9 + + + diff --git a/quartz-manager-parent/quartz-manager-web-showcase/src/main/java/it/fabioformosa/ServletInitializer.java b/quartz-manager-parent/quartz-manager-web-showcase/src/main/java/it/fabioformosa/ServletInitializer.java deleted file mode 100644 index dcc3414..0000000 --- a/quartz-manager-parent/quartz-manager-web-showcase/src/main/java/it/fabioformosa/ServletInitializer.java +++ /dev/null @@ -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); - } - -} diff --git a/skaffold.yaml b/skaffold.yaml new file mode 100644 index 0000000..035e751 --- /dev/null +++ b/skaffold.yaml @@ -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"