From abca9cf59e533e0233aa968382dfaab29827c719 Mon Sep 17 00:00:00 2001 From: mariateresa Date: Wed, 2 Aug 2023 11:18:57 +0200 Subject: [PATCH] Initial version of isdashboard generated by generator-jhipster@8.0.0-beta.2 --- .devcontainer/Dockerfile | 25 + .devcontainer/devcontainer.json | 53 + .editorconfig | 23 + .eslintignore | 9 + .eslintrc.json | 99 + .gitattributes | 150 + .gitignore | 151 + .husky/pre-commit | 5 + .lintstagedrc.js | 3 + .mvn/jvm.config | 1 + .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .mvn/wrapper/maven-wrapper.properties | 18 + .prettierignore | 8 + .prettierrc | 18 + .yo-rc.json | 46 + README.md | 246 ++ angular.json | 109 + checkstyle.xml | 20 + jest.conf.js | 29 + mvnw | 308 ++ mvnw.cmd | 205 + ngsw-config.json | 21 + npmw | 42 + npmw.cmd | 31 + package.json | 138 + pom.xml | 1033 +++++ sonar-project.properties | 31 + src/main/docker/app.yml | 18 + .../grafana/provisioning/dashboards/JVM.json | 3778 +++++++++++++++++ .../provisioning/dashboards/dashboard.yml | 11 + .../provisioning/datasources/datasource.yml | 50 + src/main/docker/jhipster-control-center.yml | 48 + src/main/docker/jib/entrypoint.sh | 39 + src/main/docker/monitoring.yml | 31 + src/main/docker/prometheus/prometheus.yml | 31 + src/main/docker/sonar.yml | 15 + .../gcube/isdashboard/ApplicationWebXml.java | 19 + .../isdashboard/GeneratedByJHipster.java | 13 + .../org/gcube/isdashboard/IsdashboardApp.java | 107 + .../aop/logging/LoggingAspect.java | 115 + .../isdashboard/aop/logging/package-info.java | 4 + .../config/ApplicationProperties.java | 16 + .../config/AsyncConfiguration.java | 48 + .../isdashboard/config/CRLFLogConverter.java | 67 + .../config/CacheConfiguration.java | 66 + .../config/DateTimeFormatConfiguration.java | 20 + .../config/JacksonConfiguration.java | 24 + .../config/LocaleConfiguration.java | 26 + .../config/LoggingAspectConfiguration.java | 17 + .../config/LoggingConfiguration.java | 47 + .../config/SecurityConfiguration.java | 95 + .../StaticResourcesWebConfiguration.java | 59 + .../isdashboard/config/WebConfigurer.java | 98 + .../isdashboard/config/package-info.java | 4 + .../org/gcube/isdashboard/package-info.java | 4 + .../security/AuthoritiesConstants.java | 15 + .../isdashboard/security/SecurityUtils.java | 86 + .../isdashboard/security/package-info.java | 4 + .../isdashboard/web/filter/SpaWebFilter.java | 32 + .../isdashboard/web/filter/package-info.java | 4 + .../isdashboard/web/rest/AccountResource.java | 78 + .../rest/errors/BadRequestAlertException.java | 50 + .../web/rest/errors/ErrorConstants.java | 13 + .../web/rest/errors/ExceptionTranslator.java | 253 ++ .../web/rest/errors/FieldErrorVM.java | 32 + .../web/rest/errors/package-info.java | 4 + .../isdashboard/web/rest/package-info.java | 4 + src/main/resources/banner.txt | 10 + src/main/resources/config/application-dev.yml | 83 + .../resources/config/application-prod.yml | 98 + src/main/resources/config/application-tls.yml | 19 + src/main/resources/config/application.yml | 192 + src/main/resources/config/tls/keystore.p12 | Bin 0 -> 2768 bytes src/main/resources/i18n/messages.properties | 6 + src/main/resources/logback-spring.xml | 71 + src/main/resources/templates/error.html | 92 + src/main/webapp/404.html | 58 + src/main/webapp/WEB-INF/web.xml | 13 + .../webapp/app/admin/admin-routing.module.ts | 18 + .../webapp/app/admin/docs/docs.component.html | 10 + .../webapp/app/admin/docs/docs.component.scss | 6 + .../webapp/app/admin/docs/docs.component.ts | 9 + .../webapp/app/app-page-title-strategy.ts | 17 + src/main/webapp/app/app-routing.module.ts | 52 + src/main/webapp/app/app.constants.ts | 9 + src/main/webapp/app/app.module.ts | 49 + .../webapp/app/config/authority.constants.ts | 4 + .../webapp/app/config/datepicker-adapter.ts | 20 + src/main/webapp/app/config/dayjs.ts | 11 + src/main/webapp/app/config/error.constants.ts | 3 + .../webapp/app/config/font-awesome-icons.ts | 83 + src/main/webapp/app/config/input.constants.ts | 2 + .../webapp/app/config/navigation.constants.ts | 5 + .../webapp/app/config/pagination.constants.ts | 3 + .../app/config/uib-pagination.config.ts | 14 + .../webapp/app/core/auth/account.model.ts | 12 + .../app/core/auth/account.service.spec.ts | 198 + .../webapp/app/core/auth/account.service.ts | 77 + .../app/core/auth/auth-session.service.ts | 34 + .../app/core/auth/state-storage.service.ts | 40 + .../core/auth/user-route-access.service.ts | 33 + .../config/application-config.service.spec.ts | 40 + .../core/config/application-config.service.ts | 28 + .../interceptor/auth-expired.interceptor.ts | 37 + .../interceptor/error-handler.interceptor.ts | 23 + src/main/webapp/app/core/interceptor/index.ts | 23 + .../interceptor/notification.interceptor.ts | 34 + .../webapp/app/core/request/request-util.ts | 23 + .../webapp/app/core/request/request.model.ts | 11 + .../app/core/util/alert.service.spec.ts | 233 + .../webapp/app/core/util/alert.service.ts | 73 + .../app/core/util/data-util.service.spec.ts | 34 + .../webapp/app/core/util/data-util.service.ts | 130 + .../core/util/event-manager.service.spec.ts | 84 + .../app/core/util/event-manager.service.ts | 63 + .../webapp/app/core/util/operators.spec.ts | 18 + src/main/webapp/app/core/util/operators.ts | 9 + .../app/core/util/parse-links.service.spec.ts | 36 + .../app/core/util/parse-links.service.ts | 47 + .../app/entities/entity-navbar-items.ts | 3 + .../app/entities/entity-routing.module.ts | 11 + src/main/webapp/app/home/home.component.html | 49 + src/main/webapp/app/home/home.component.scss | 23 + .../webapp/app/home/home.component.spec.ts | 111 + src/main/webapp/app/home/home.component.ts | 39 + .../app/layouts/error/error.component.html | 15 + .../app/layouts/error/error.component.ts | 23 + .../webapp/app/layouts/error/error.route.ts | 31 + .../app/layouts/footer/footer.component.html | 3 + .../app/layouts/footer/footer.component.ts | 8 + .../app/layouts/main/main.component.html | 13 + .../app/layouts/main/main.component.spec.ts | 118 + .../webapp/app/layouts/main/main.component.ts | 19 + .../webapp/app/layouts/main/main.module.ts | 13 + .../app/layouts/navbar/navbar-item.model.d.ts | 6 + .../app/layouts/navbar/navbar.component.html | 107 + .../app/layouts/navbar/navbar.component.scss | 36 + .../layouts/navbar/navbar.component.spec.ts | 95 + .../app/layouts/navbar/navbar.component.ts | 70 + .../profiles/page-ribbon.component.scss | 25 + .../profiles/page-ribbon.component.spec.ts | 39 + .../layouts/profiles/page-ribbon.component.ts | 27 + .../layouts/profiles/profile-info.model.ts | 15 + .../app/layouts/profiles/profile.service.ts | 41 + .../webapp/app/login/login.component.html | 47 + .../webapp/app/login/login.component.spec.ts | 152 + src/main/webapp/app/login/login.component.ts | 54 + src/main/webapp/app/login/login.model.ts | 3 + src/main/webapp/app/login/login.service.ts | 34 + .../shared/alert/alert-error.component.html | 7 + .../alert/alert-error.component.spec.ts | 158 + .../app/shared/alert/alert-error.component.ts | 99 + .../app/shared/alert/alert-error.model.ts | 3 + .../app/shared/alert/alert.component.html | 7 + .../app/shared/alert/alert.component.spec.ts | 44 + .../app/shared/alert/alert.component.ts | 37 + .../auth/has-any-authority.directive.spec.ts | 131 + .../auth/has-any-authority.directive.ts | 54 + .../webapp/app/shared/date/duration.pipe.ts | 16 + .../date/format-medium-date.pipe.spec.ts | 19 + .../shared/date/format-medium-date.pipe.ts | 13 + .../date/format-medium-datetime.pipe.spec.ts | 19 + .../date/format-medium-datetime.pipe.ts | 13 + src/main/webapp/app/shared/date/index.ts | 3 + .../app/shared/filter/filter.component.html | 12 + .../app/shared/filter/filter.component.ts | 21 + .../app/shared/filter/filter.model.spec.ts | 242 ++ .../webapp/app/shared/filter/filter.model.ts | 156 + src/main/webapp/app/shared/filter/index.ts | 2 + .../webapp/app/shared/pagination/index.ts | 1 + .../pagination/item-count.component.spec.ts | 64 + .../shared/pagination/item-count.component.ts | 32 + src/main/webapp/app/shared/shared.module.ts | 15 + src/main/webapp/app/shared/sort/index.ts | 2 + .../app/shared/sort/sort-by.directive.spec.ts | 140 + .../app/shared/sort/sort-by.directive.ts | 56 + .../app/shared/sort/sort.directive.spec.ts | 87 + .../webapp/app/shared/sort/sort.directive.ts | 40 + .../webapp/app/shared/sort/sort.service.ts | 13 + src/main/webapp/bootstrap.ts | 16 + src/main/webapp/content/css/loading.css | 152 + .../images/jhipster_family_member_0.svg | 1 + .../jhipster_family_member_0_head-192.png | Bin 0 -> 13439 bytes .../jhipster_family_member_0_head-256.png | Bin 0 -> 7037 bytes .../jhipster_family_member_0_head-384.png | Bin 0 -> 10350 bytes .../jhipster_family_member_0_head-512.png | Bin 0 -> 11431 bytes .../images/jhipster_family_member_1.svg | 1 + .../jhipster_family_member_1_head-192.png | Bin 0 -> 7046 bytes .../jhipster_family_member_1_head-256.png | Bin 0 -> 9505 bytes .../jhipster_family_member_1_head-384.png | Bin 0 -> 15054 bytes .../jhipster_family_member_1_head-512.png | Bin 0 -> 16456 bytes .../images/jhipster_family_member_2.svg | 1 + .../jhipster_family_member_2_head-192.png | Bin 0 -> 5423 bytes .../jhipster_family_member_2_head-256.png | Bin 0 -> 6687 bytes .../jhipster_family_member_2_head-384.png | Bin 0 -> 9682 bytes .../jhipster_family_member_2_head-512.png | Bin 0 -> 10514 bytes .../images/jhipster_family_member_3.svg | 1 + .../jhipster_family_member_3_head-192.png | Bin 0 -> 6148 bytes .../jhipster_family_member_3_head-256.png | Bin 0 -> 8028 bytes .../jhipster_family_member_3_head-384.png | Bin 0 -> 11998 bytes .../jhipster_family_member_3_head-512.png | Bin 0 -> 13555 bytes .../webapp/content/images/logo-jhipster.png | Bin 0 -> 605 bytes .../content/scss/_bootstrap-variables.scss | 45 + src/main/webapp/content/scss/global.scss | 239 ++ src/main/webapp/content/scss/vendor.scss | 12 + src/main/webapp/declarations.d.ts | 1 + src/main/webapp/favicon.ico | Bin 0 -> 1574 bytes src/main/webapp/index.html | 126 + src/main/webapp/main.ts | 1 + src/main/webapp/manifest.webapp | 31 + src/main/webapp/robots.txt | 8 + .../swagger-ui/dist/images/throbber.gif | Bin 0 -> 9257 bytes src/main/webapp/swagger-ui/index.html | 92 + .../gcube/isdashboard/IntegrationTest.java | 20 + .../isdashboard/TechnicalStructureTest.java | 33 + .../config/AsyncSyncConfiguration.java | 16 + .../config/SpringBootTestClassOrderer.java | 22 + .../StaticResourcesWebConfigurerTest.java | 76 + .../isdashboard/config/WebConfigurerTest.java | 133 + .../config/WebConfigurerTestController.java | 14 + .../security/SecurityUtilsUnitTest.java | 92 + .../web/filter/SpaWebFilterIT.java | 83 + .../web/rest/AccountResourceIT.java | 63 + .../gcube/isdashboard/web/rest/TestUtil.java | 182 + .../web/rest/WithUnauthenticatedMockUser.java | 23 + .../rest/errors/ExceptionTranslatorIT.java | 112 + .../ExceptionTranslatorTestController.java | 60 + src/test/resources/config/application.yml | 89 + src/test/resources/junit-platform.properties | 4 + src/test/resources/logback.xml | 42 + tsconfig.app.json | 9 + tsconfig.json | 35 + tsconfig.spec.json | 9 + webpack/environment.js | 6 + webpack/logo-jhipster.png | Bin 0 -> 3326 bytes webpack/proxy.conf.js | 13 + webpack/webpack.custom.js | 122 + 237 files changed, 15285 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .husky/pre-commit create mode 100644 .lintstagedrc.js create mode 100644 .mvn/jvm.config create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .yo-rc.json create mode 100644 README.md create mode 100644 angular.json create mode 100644 checkstyle.xml create mode 100644 jest.conf.js create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 ngsw-config.json create mode 100644 npmw create mode 100644 npmw.cmd create mode 100644 package.json create mode 100644 pom.xml create mode 100644 sonar-project.properties create mode 100644 src/main/docker/app.yml create mode 100644 src/main/docker/grafana/provisioning/dashboards/JVM.json create mode 100644 src/main/docker/grafana/provisioning/dashboards/dashboard.yml create mode 100644 src/main/docker/grafana/provisioning/datasources/datasource.yml create mode 100644 src/main/docker/jhipster-control-center.yml create mode 100644 src/main/docker/jib/entrypoint.sh create mode 100644 src/main/docker/monitoring.yml create mode 100644 src/main/docker/prometheus/prometheus.yml create mode 100644 src/main/docker/sonar.yml create mode 100644 src/main/java/org/gcube/isdashboard/ApplicationWebXml.java create mode 100644 src/main/java/org/gcube/isdashboard/GeneratedByJHipster.java create mode 100644 src/main/java/org/gcube/isdashboard/IsdashboardApp.java create mode 100644 src/main/java/org/gcube/isdashboard/aop/logging/LoggingAspect.java create mode 100644 src/main/java/org/gcube/isdashboard/aop/logging/package-info.java create mode 100644 src/main/java/org/gcube/isdashboard/config/ApplicationProperties.java create mode 100644 src/main/java/org/gcube/isdashboard/config/AsyncConfiguration.java create mode 100644 src/main/java/org/gcube/isdashboard/config/CRLFLogConverter.java create mode 100644 src/main/java/org/gcube/isdashboard/config/CacheConfiguration.java create mode 100644 src/main/java/org/gcube/isdashboard/config/DateTimeFormatConfiguration.java create mode 100644 src/main/java/org/gcube/isdashboard/config/JacksonConfiguration.java create mode 100644 src/main/java/org/gcube/isdashboard/config/LocaleConfiguration.java create mode 100644 src/main/java/org/gcube/isdashboard/config/LoggingAspectConfiguration.java create mode 100644 src/main/java/org/gcube/isdashboard/config/LoggingConfiguration.java create mode 100644 src/main/java/org/gcube/isdashboard/config/SecurityConfiguration.java create mode 100644 src/main/java/org/gcube/isdashboard/config/StaticResourcesWebConfiguration.java create mode 100644 src/main/java/org/gcube/isdashboard/config/WebConfigurer.java create mode 100644 src/main/java/org/gcube/isdashboard/config/package-info.java create mode 100644 src/main/java/org/gcube/isdashboard/package-info.java create mode 100644 src/main/java/org/gcube/isdashboard/security/AuthoritiesConstants.java create mode 100644 src/main/java/org/gcube/isdashboard/security/SecurityUtils.java create mode 100644 src/main/java/org/gcube/isdashboard/security/package-info.java create mode 100644 src/main/java/org/gcube/isdashboard/web/filter/SpaWebFilter.java create mode 100644 src/main/java/org/gcube/isdashboard/web/filter/package-info.java create mode 100644 src/main/java/org/gcube/isdashboard/web/rest/AccountResource.java create mode 100644 src/main/java/org/gcube/isdashboard/web/rest/errors/BadRequestAlertException.java create mode 100644 src/main/java/org/gcube/isdashboard/web/rest/errors/ErrorConstants.java create mode 100644 src/main/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslator.java create mode 100644 src/main/java/org/gcube/isdashboard/web/rest/errors/FieldErrorVM.java create mode 100644 src/main/java/org/gcube/isdashboard/web/rest/errors/package-info.java create mode 100644 src/main/java/org/gcube/isdashboard/web/rest/package-info.java create mode 100644 src/main/resources/banner.txt create mode 100644 src/main/resources/config/application-dev.yml create mode 100644 src/main/resources/config/application-prod.yml create mode 100644 src/main/resources/config/application-tls.yml create mode 100644 src/main/resources/config/application.yml create mode 100644 src/main/resources/config/tls/keystore.p12 create mode 100644 src/main/resources/i18n/messages.properties create mode 100644 src/main/resources/logback-spring.xml create mode 100644 src/main/resources/templates/error.html create mode 100644 src/main/webapp/404.html create mode 100644 src/main/webapp/WEB-INF/web.xml create mode 100644 src/main/webapp/app/admin/admin-routing.module.ts create mode 100644 src/main/webapp/app/admin/docs/docs.component.html create mode 100644 src/main/webapp/app/admin/docs/docs.component.scss create mode 100644 src/main/webapp/app/admin/docs/docs.component.ts create mode 100644 src/main/webapp/app/app-page-title-strategy.ts create mode 100644 src/main/webapp/app/app-routing.module.ts create mode 100644 src/main/webapp/app/app.constants.ts create mode 100644 src/main/webapp/app/app.module.ts create mode 100644 src/main/webapp/app/config/authority.constants.ts create mode 100644 src/main/webapp/app/config/datepicker-adapter.ts create mode 100644 src/main/webapp/app/config/dayjs.ts create mode 100644 src/main/webapp/app/config/error.constants.ts create mode 100644 src/main/webapp/app/config/font-awesome-icons.ts create mode 100644 src/main/webapp/app/config/input.constants.ts create mode 100644 src/main/webapp/app/config/navigation.constants.ts create mode 100644 src/main/webapp/app/config/pagination.constants.ts create mode 100644 src/main/webapp/app/config/uib-pagination.config.ts create mode 100644 src/main/webapp/app/core/auth/account.model.ts create mode 100644 src/main/webapp/app/core/auth/account.service.spec.ts create mode 100644 src/main/webapp/app/core/auth/account.service.ts create mode 100644 src/main/webapp/app/core/auth/auth-session.service.ts create mode 100644 src/main/webapp/app/core/auth/state-storage.service.ts create mode 100644 src/main/webapp/app/core/auth/user-route-access.service.ts create mode 100644 src/main/webapp/app/core/config/application-config.service.spec.ts create mode 100644 src/main/webapp/app/core/config/application-config.service.ts create mode 100644 src/main/webapp/app/core/interceptor/auth-expired.interceptor.ts create mode 100644 src/main/webapp/app/core/interceptor/error-handler.interceptor.ts create mode 100644 src/main/webapp/app/core/interceptor/index.ts create mode 100644 src/main/webapp/app/core/interceptor/notification.interceptor.ts create mode 100644 src/main/webapp/app/core/request/request-util.ts create mode 100644 src/main/webapp/app/core/request/request.model.ts create mode 100644 src/main/webapp/app/core/util/alert.service.spec.ts create mode 100644 src/main/webapp/app/core/util/alert.service.ts create mode 100644 src/main/webapp/app/core/util/data-util.service.spec.ts create mode 100644 src/main/webapp/app/core/util/data-util.service.ts create mode 100644 src/main/webapp/app/core/util/event-manager.service.spec.ts create mode 100644 src/main/webapp/app/core/util/event-manager.service.ts create mode 100644 src/main/webapp/app/core/util/operators.spec.ts create mode 100644 src/main/webapp/app/core/util/operators.ts create mode 100644 src/main/webapp/app/core/util/parse-links.service.spec.ts create mode 100644 src/main/webapp/app/core/util/parse-links.service.ts create mode 100644 src/main/webapp/app/entities/entity-navbar-items.ts create mode 100644 src/main/webapp/app/entities/entity-routing.module.ts create mode 100644 src/main/webapp/app/home/home.component.html create mode 100644 src/main/webapp/app/home/home.component.scss create mode 100644 src/main/webapp/app/home/home.component.spec.ts create mode 100644 src/main/webapp/app/home/home.component.ts create mode 100644 src/main/webapp/app/layouts/error/error.component.html create mode 100644 src/main/webapp/app/layouts/error/error.component.ts create mode 100644 src/main/webapp/app/layouts/error/error.route.ts create mode 100644 src/main/webapp/app/layouts/footer/footer.component.html create mode 100644 src/main/webapp/app/layouts/footer/footer.component.ts create mode 100644 src/main/webapp/app/layouts/main/main.component.html create mode 100644 src/main/webapp/app/layouts/main/main.component.spec.ts create mode 100644 src/main/webapp/app/layouts/main/main.component.ts create mode 100644 src/main/webapp/app/layouts/main/main.module.ts create mode 100644 src/main/webapp/app/layouts/navbar/navbar-item.model.d.ts create mode 100644 src/main/webapp/app/layouts/navbar/navbar.component.html create mode 100644 src/main/webapp/app/layouts/navbar/navbar.component.scss create mode 100644 src/main/webapp/app/layouts/navbar/navbar.component.spec.ts create mode 100644 src/main/webapp/app/layouts/navbar/navbar.component.ts create mode 100644 src/main/webapp/app/layouts/profiles/page-ribbon.component.scss create mode 100644 src/main/webapp/app/layouts/profiles/page-ribbon.component.spec.ts create mode 100644 src/main/webapp/app/layouts/profiles/page-ribbon.component.ts create mode 100644 src/main/webapp/app/layouts/profiles/profile-info.model.ts create mode 100644 src/main/webapp/app/layouts/profiles/profile.service.ts create mode 100644 src/main/webapp/app/login/login.component.html create mode 100644 src/main/webapp/app/login/login.component.spec.ts create mode 100644 src/main/webapp/app/login/login.component.ts create mode 100644 src/main/webapp/app/login/login.model.ts create mode 100644 src/main/webapp/app/login/login.service.ts create mode 100644 src/main/webapp/app/shared/alert/alert-error.component.html create mode 100644 src/main/webapp/app/shared/alert/alert-error.component.spec.ts create mode 100644 src/main/webapp/app/shared/alert/alert-error.component.ts create mode 100644 src/main/webapp/app/shared/alert/alert-error.model.ts create mode 100644 src/main/webapp/app/shared/alert/alert.component.html create mode 100644 src/main/webapp/app/shared/alert/alert.component.spec.ts create mode 100644 src/main/webapp/app/shared/alert/alert.component.ts create mode 100644 src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts create mode 100644 src/main/webapp/app/shared/auth/has-any-authority.directive.ts create mode 100644 src/main/webapp/app/shared/date/duration.pipe.ts create mode 100644 src/main/webapp/app/shared/date/format-medium-date.pipe.spec.ts create mode 100644 src/main/webapp/app/shared/date/format-medium-date.pipe.ts create mode 100644 src/main/webapp/app/shared/date/format-medium-datetime.pipe.spec.ts create mode 100644 src/main/webapp/app/shared/date/format-medium-datetime.pipe.ts create mode 100644 src/main/webapp/app/shared/date/index.ts create mode 100644 src/main/webapp/app/shared/filter/filter.component.html create mode 100644 src/main/webapp/app/shared/filter/filter.component.ts create mode 100644 src/main/webapp/app/shared/filter/filter.model.spec.ts create mode 100644 src/main/webapp/app/shared/filter/filter.model.ts create mode 100644 src/main/webapp/app/shared/filter/index.ts create mode 100644 src/main/webapp/app/shared/pagination/index.ts create mode 100644 src/main/webapp/app/shared/pagination/item-count.component.spec.ts create mode 100644 src/main/webapp/app/shared/pagination/item-count.component.ts create mode 100644 src/main/webapp/app/shared/shared.module.ts create mode 100644 src/main/webapp/app/shared/sort/index.ts create mode 100644 src/main/webapp/app/shared/sort/sort-by.directive.spec.ts create mode 100644 src/main/webapp/app/shared/sort/sort-by.directive.ts create mode 100644 src/main/webapp/app/shared/sort/sort.directive.spec.ts create mode 100644 src/main/webapp/app/shared/sort/sort.directive.ts create mode 100644 src/main/webapp/app/shared/sort/sort.service.ts create mode 100644 src/main/webapp/bootstrap.ts create mode 100644 src/main/webapp/content/css/loading.css create mode 100644 src/main/webapp/content/images/jhipster_family_member_0.svg create mode 100644 src/main/webapp/content/images/jhipster_family_member_0_head-192.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_0_head-256.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_0_head-384.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_0_head-512.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_1.svg create mode 100644 src/main/webapp/content/images/jhipster_family_member_1_head-192.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_1_head-256.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_1_head-384.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_1_head-512.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_2.svg create mode 100644 src/main/webapp/content/images/jhipster_family_member_2_head-192.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_2_head-256.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_2_head-384.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_2_head-512.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_3.svg create mode 100644 src/main/webapp/content/images/jhipster_family_member_3_head-192.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_3_head-256.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_3_head-384.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_3_head-512.png create mode 100644 src/main/webapp/content/images/logo-jhipster.png create mode 100644 src/main/webapp/content/scss/_bootstrap-variables.scss create mode 100644 src/main/webapp/content/scss/global.scss create mode 100644 src/main/webapp/content/scss/vendor.scss create mode 100644 src/main/webapp/declarations.d.ts create mode 100644 src/main/webapp/favicon.ico create mode 100644 src/main/webapp/index.html create mode 100644 src/main/webapp/main.ts create mode 100644 src/main/webapp/manifest.webapp create mode 100644 src/main/webapp/robots.txt create mode 100644 src/main/webapp/swagger-ui/dist/images/throbber.gif create mode 100644 src/main/webapp/swagger-ui/index.html create mode 100644 src/test/java/org/gcube/isdashboard/IntegrationTest.java create mode 100644 src/test/java/org/gcube/isdashboard/TechnicalStructureTest.java create mode 100644 src/test/java/org/gcube/isdashboard/config/AsyncSyncConfiguration.java create mode 100644 src/test/java/org/gcube/isdashboard/config/SpringBootTestClassOrderer.java create mode 100644 src/test/java/org/gcube/isdashboard/config/StaticResourcesWebConfigurerTest.java create mode 100644 src/test/java/org/gcube/isdashboard/config/WebConfigurerTest.java create mode 100644 src/test/java/org/gcube/isdashboard/config/WebConfigurerTestController.java create mode 100644 src/test/java/org/gcube/isdashboard/security/SecurityUtilsUnitTest.java create mode 100644 src/test/java/org/gcube/isdashboard/web/filter/SpaWebFilterIT.java create mode 100644 src/test/java/org/gcube/isdashboard/web/rest/AccountResourceIT.java create mode 100644 src/test/java/org/gcube/isdashboard/web/rest/TestUtil.java create mode 100644 src/test/java/org/gcube/isdashboard/web/rest/WithUnauthenticatedMockUser.java create mode 100644 src/test/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslatorIT.java create mode 100644 src/test/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslatorTestController.java create mode 100644 src/test/resources/config/application.yml create mode 100644 src/test/resources/junit-platform.properties create mode 100644 src/test/resources/logback.xml create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.spec.json create mode 100644 webpack/environment.js create mode 100644 webpack/logo-jhipster.png create mode 100644 webpack/proxy.conf.js create mode 100644 webpack/webpack.custom.js diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..a914fd2 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/java/.devcontainer/base.Dockerfile + +# [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 17, 17-bullseye, 17-buster +ARG VARIANT="17" +FROM mcr.microsoft.com/vscode/devcontainers/java:0-${VARIANT} + +# [Option] Install Maven +ARG INSTALL_MAVEN="false" +ARG MAVEN_VERSION="" +# [Option] Install Gradle +ARG INSTALL_GRADLE="false" +ARG GRADLE_VERSION="" +RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \ + && if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..321a789 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,53 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/java +{ + "name": "Isdashboard", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Update the VARIANT arg to pick a Java version: 17, 19 + // Append -bullseye or -buster to pin to an OS version. + // Use the -bullseye variants on local arm64/Apple Silicon. + "VARIANT": "17-bullseye", + // Options + // maven and gradle wrappers are used by default, we don't need them installed globally + // "INSTALL_MAVEN": "true", + // "INSTALL_GRADLE": "false", + "NODE_VERSION": "18.16.1" + } + }, + + "customizations": { + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "java.jdt.ls.java.home": "/docker-java-home" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "angular.ng-template", + "christian-kohler.npm-intellisense", + "firsttris.vscode-jest-runner", + "ms-vscode.vscode-typescript-tslint-plugin", + "dbaeumer.vscode-eslint", + "vscjava.vscode-java-pack", + "pivotal.vscode-boot-dev-pack", + "esbenp.prettier-vscode" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [4200, 3001, 9000, 8080], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "java -version", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "docker-in-docker": "latest", + "docker-from-docker": "latest" + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c2fa6a2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +[*.{ts,tsx,js,jsx,json,css,scss,yml,html,vue}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..d2162ab --- /dev/null +++ b/.eslintignore @@ -0,0 +1,9 @@ +node_modules/ +src/main/docker/ +jest.conf.js +webpack/ +target/ +build/ +node/ +coverage/ +postcss.config.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..d56923d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,99 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@angular-eslint/eslint-plugin", "@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:@angular-eslint/recommended", + "prettier", + "eslint-config-prettier" + ], + "env": { + "browser": true, + "es6": true, + "commonjs": true + }, + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "project": ["./tsconfig.app.json", "./tsconfig.spec.json"] + }, + "rules": { + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "jhi", + "style": "kebab-case" + } + ], + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "jhi", + "style": "camelCase" + } + ], + "@angular-eslint/relative-url-prefix": "error", + "@typescript-eslint/ban-types": [ + "error", + { + "extendDefaults": true, + "types": { + "{}": false + } + } + ], + "@typescript-eslint/explicit-function-return-type": ["error", { "allowExpressions": true }], + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/member-ordering": [ + "error", + { + "default": [ + "public-static-field", + "protected-static-field", + "private-static-field", + "public-instance-field", + "protected-instance-field", + "private-instance-field", + "constructor", + "public-static-method", + "protected-static-method", + "private-static-method", + "public-instance-method", + "protected-instance-method", + "private-instance-method" + ] + } + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-parameter-properties": ["warn", { "allows": ["public", "private", "protected"] }], + "@typescript-eslint/no-shadow": ["error"], + "@typescript-eslint/no-unnecessary-condition": "error", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/prefer-nullish-coalescing": "error", + "@typescript-eslint/prefer-optional-chain": "error", + "@typescript-eslint/unbound-method": "off", + "arrow-body-style": "error", + "curly": "error", + "eqeqeq": ["error", "always", { "null": "ignore" }], + "guard-for-in": "error", + "no-bitwise": "error", + "no-caller": "error", + "no-console": ["error", { "allow": ["warn", "error"] }], + "no-eval": "error", + "no-labels": "error", + "no-new": "error", + "no-new-wrappers": "error", + "object-shorthand": ["error", "always", { "avoidExplicitReturnArrows": true }], + "radix": "error", + "spaced-comment": ["warn", "always"] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ca61722 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,150 @@ +# This file is inspired by https://github.com/alexkaratarakis/gitattributes +# +# Auto detect text files and perform LF normalization +# http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ +* text=auto + +# The above will handle all files NOT found below +# These files are text and should be normalized (Convert crlf => lf) + +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf +*.coffee text +*.css text +*.cql text +*.df text +*.ejs text +*.html text +*.java text +*.js text +*.json text +*.less text +*.properties text +*.sass text +*.scss text +*.sh text eol=lf +*.sql text +*.txt text +*.ts text +*.xml text +*.yaml text +*.yml text + +# Documents +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain +*.markdown text +*.md text +*.adoc text +*.textile text +*.mustache text +*.csv text +*.tab text +*.tsv text +*.txt text +AUTHORS text +CHANGELOG text +CHANGES text +CONTRIBUTING text +COPYING text +copyright text +*COPYRIGHT* text +INSTALL text +license text +LICENSE text +NEWS text +readme text +*README* text +TODO text + +# Graphics +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.tif binary +*.tiff binary +*.ico binary +# SVG treated as an asset (binary) by default. If you want to treat it as text, +# comment-out the following line and uncomment the line after. +*.svg binary +#*.svg text +*.eps binary + +# These files are binary and should be left untouched +# (binary is a macro for -text -diff) +*.class binary +*.jar binary +*.war binary + +## LINTERS +.csslintrc text +.eslintrc text +.jscsrc text +.jshintrc text +.jshintignore text +.stylelintrc text + +## CONFIGS +*.conf text +*.config text +.editorconfig text +.gitattributes text +.gitconfig text +.gitignore text +.htaccess text +*.npmignore text + +## HEROKU +Procfile text +.slugignore text + +## AUDIO +*.kar binary +*.m4a binary +*.mid binary +*.midi binary +*.mp3 binary +*.ogg binary +*.ra binary + +## VIDEO +*.3gpp binary +*.3gp binary +*.as binary +*.asf binary +*.asx binary +*.fla binary +*.flv binary +*.m4v binary +*.mng binary +*.mov binary +*.mp4 binary +*.mpeg binary +*.mpg binary +*.swc binary +*.swf binary +*.webm binary + +## ARCHIVES +*.7z binary +*.gz binary +*.rar binary +*.tar binary +*.zip binary + +## FONTS +*.ttf binary +*.eot binary +*.otf binary +*.woff binary +*.woff2 binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d543a07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,151 @@ +###################### +# Node +###################### +/node/ +node_tmp/ +node_modules/ +npm-debug.log.* +/.awcache/* +/.cache-loader/* + +###################### +# SASS +###################### +.sass-cache/ + +###################### +# Eclipse +###################### +*.pydevproject +.project +.metadata +tmp/ +tmp/**/* +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath +.factorypath + +# External tool builders +.externalToolBuilders/** + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + +# STS-specific +/.sts4-cache/* + +###################### +# IntelliJ +###################### +.idea/ +*.iml +*.iws +*.ipr +*.ids +*.orig +classes/ +out/ + +###################### +# Visual Studio Code +###################### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +###################### +# Maven +###################### +/log/ +/target/ + +###################### +# Gradle +###################### +.gradle/ +/build/ + +###################### +# Package Files +###################### +*.jar +*.war +*.ear +*.db + +###################### +# Windows +###################### +# Windows image file caches +Thumbs.db + +# Folder config file +Desktop.ini + +###################### +# Mac OSX +###################### +.DS_Store +.svn + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +###################### +# Directories +###################### +/bin/ +/deploy/ + +###################### +# Logs +###################### +*.log* + +###################### +# Others +###################### +*.class +*.*~ +*~ +.merge_file* + +###################### +# Gradle Wrapper +###################### +!gradle/wrapper/gradle-wrapper.jar + +###################### +# Maven Wrapper +###################### +!.mvn/wrapper/maven-wrapper.jar + +###################### +# ESLint +###################### +.eslintcache + +###################### +# Code coverage +###################### +/coverage/ +/.nyc_output/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..adefefb --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + + +"$(dirname "$0")/../npmw" exec --no-install lint-staged diff --git a/.lintstagedrc.js b/.lintstagedrc.js new file mode 100644 index 0000000..6876878 --- /dev/null +++ b/.lintstagedrc.js @@ -0,0 +1,3 @@ +module.exports = { + '{,src/**/,webpack/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}': ['prettier --write'], +}; diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1 @@ + diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..3c6fda8 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.2/apache-maven-3.9.2-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ab0567f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules +target +build +package-lock.json +.git +.mvn +gradle +.gradle diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6d25fba --- /dev/null +++ b/.prettierrc @@ -0,0 +1,18 @@ +# Prettier configuration + +printWidth: 140 +singleQuote: true +tabWidth: 2 +useTabs: false + +# js and ts rules: +arrowParens: avoid + +# jsx and tsx rules: +bracketSameLine: false + +# java rules: +overrides: + - files: "*.java" + options: + tabWidth: 4 diff --git a/.yo-rc.json b/.yo-rc.json new file mode 100644 index 0000000..17684b2 --- /dev/null +++ b/.yo-rc.json @@ -0,0 +1,46 @@ +{ + "generator-jhipster": { + "applicationType": "monolith", + "authenticationType": "session", + "baseName": "isdashboard", + "buildTool": "maven", + "cacheProvider": "ehcache", + "clientFramework": "angular", + "clientTheme": "none", + "creationTimestamp": 1690967932750, + "databaseType": "no", + "devDatabaseType": "no", + "devServerPort": 4200, + "dtoSuffix": "DTO", + "enableGradleEnterprise": null, + "enableHibernateCache": false, + "enableSwaggerCodegen": false, + "enableTranslation": false, + "entities": [], + "entitySuffix": "", + "gradleEnterpriseHost": null, + "jhiPrefix": "jhi", + "jhipsterVersion": "8.0.0-beta.2", + "messageBroker": false, + "microfrontend": false, + "microfrontends": [], + "nativeLanguage": "en", + "packageFolder": "org/gcube/isdashboard", + "packageName": "org.gcube.isdashboard", + "pages": [], + "prodDatabaseType": "no", + "reactive": false, + "rememberMeKey": "6038bcb93c3f8cc61c163e71aa55573477e46c3535cf0624212567f396b89e7be83d90b03885a818d14b1b7f4928ab548561", + "searchEngine": false, + "serverPort": null, + "serverSideOptions": [], + "serviceDiscoveryType": false, + "skipCheckLengthOfIdentifier": false, + "skipClient": false, + "skipFakeData": false, + "skipUserManagement": true, + "testFrameworks": [], + "websocket": false, + "withAdminUi": false + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c96c06 --- /dev/null +++ b/README.md @@ -0,0 +1,246 @@ +# isdashboard + +This application was generated using JHipster 8.0.0-beta.2, you can find documentation and help at [https://www.jhipster.tech/documentation-archive/v8.0.0-beta.2](https://www.jhipster.tech/documentation-archive/v8.0.0-beta.2). + +## Project Structure + +Node is required for generation and recommended for development. `package.json` is always generated for a better development experience with prettier, commit hooks, scripts and so on. + +In the project root, JHipster generates configuration files for tools like git, prettier, eslint, husky, and others that are well known and you can find references in the web. + +`/src/*` structure follows default Java structure. + +- `.yo-rc.json` - Yeoman configuration file + JHipster configuration is stored in this file at `generator-jhipster` key. You may find `generator-jhipster-*` for specific blueprints configuration. +- `.yo-resolve` (optional) - Yeoman conflict resolver + Allows to use a specific action when conflicts are found skipping prompts for files that matches a pattern. Each line should match `[pattern] [action]` with pattern been a [Minimatch](https://github.com/isaacs/minimatch#minimatch) pattern and action been one of skip (default if ommited) or force. Lines starting with `#` are considered comments and are ignored. +- `.jhipster/*.json` - JHipster entity configuration files + +- `npmw` - wrapper to use locally installed npm. + JHipster installs Node and npm locally using the build tool by default. This wrapper makes sure npm is installed locally and uses it avoiding some differences different versions can cause. By using `./npmw` instead of the traditional `npm` you can configure a Node-less environment to develop or test your application. +- `/src/main/docker` - Docker configurations for the application and services that the application depends on + +## Development + +Before you can build this project, you must install and configure the following dependencies on your machine: + +1. [Node.js][]: We use Node to run a development web server and build the project. + Depending on your system, you can install Node either from source or as a pre-packaged bundle. + +After installing Node, you should be able to run the following command to install development tools. +You will only need to run this command when dependencies change in [package.json](package.json). + +``` +npm install +``` + +We use npm scripts and [Angular CLI][] with [Webpack][] as our build system. + +Run the following commands in two separate terminals to create a blissful development experience where your browser +auto-refreshes when files change on your hard drive. + +``` +./mvnw +npm start +``` + +Npm is also used to manage CSS and JavaScript dependencies used in this application. You can upgrade dependencies by +specifying a newer version in [package.json](package.json). You can also run `npm update` and `npm install` to manage dependencies. +Add the `help` flag on any command to see how you can use it. For example, `npm help update`. + +The `npm run` command will list all of the scripts available to run for this project. + +### PWA Support + +JHipster ships with PWA (Progressive Web App) support, and it's turned off by default. One of the main components of a PWA is a service worker. + +The service worker initialization code is disabled by default. To enable it, uncomment the following code in `src/main/webapp/app/app.module.ts`: + +```typescript +ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }), +``` + +### Managing dependencies + +For example, to add [Leaflet][] library as a runtime dependency of your application, you would run following command: + +``` +npm install --save --save-exact leaflet +``` + +To benefit from TypeScript type definitions from [DefinitelyTyped][] repository in development, you would run following command: + +``` +npm install --save-dev --save-exact @types/leaflet +``` + +Then you would import the JS and CSS files specified in library's installation instructions so that [Webpack][] knows about them: +Edit [src/main/webapp/app/app.module.ts](src/main/webapp/app/app.module.ts) file: + +``` +import 'leaflet/dist/leaflet.js'; +``` + +Edit [src/main/webapp/content/scss/vendor.scss](src/main/webapp/content/scss/vendor.scss) file: + +``` +@import 'leaflet/dist/leaflet.css'; +``` + +Note: There are still a few other things remaining to do for Leaflet that we won't detail here. + +For further instructions on how to develop with JHipster, have a look at [Using JHipster in development][]. + +### Using Angular CLI + +You can also use [Angular CLI][] to generate some custom client code. + +For example, the following command: + +``` +ng generate component my-component +``` + +will generate few files: + +``` +create src/main/webapp/app/my-component/my-component.component.html +create src/main/webapp/app/my-component/my-component.component.ts +update src/main/webapp/app/app.module.ts +``` + +### JHipster Control Center + +JHipster Control Center can help you manage and control your application(s). You can start a local control center server (accessible on http://localhost:7419) with: + +``` +docker compose -f src/main/docker/jhipster-control-center.yml up +``` + +## Building for production + +### Packaging as jar + +To build the final jar and optimize the isdashboard application for production, run: + +``` +./mvnw -Pprod clean verify +``` + +This will concatenate and minify the client CSS and JavaScript files. It will also modify `index.html` so it references these new files. +To ensure everything worked, run: + +``` +java -jar target/*.jar +``` + +Then navigate to [http://localhost:8080](http://localhost:8080) in your browser. + +Refer to [Using JHipster in production][] for more details. + +### Packaging as war + +To package your application as a war in order to deploy it to an application server, run: + +``` +./mvnw -Pprod,war clean verify +``` + +## Testing + +To launch your application's tests, run: + +``` +./mvnw verify +``` + +### Client tests + +Unit tests are run by [Jest][]. They're located in [src/test/javascript/](src/test/javascript/) and can be run with: + +``` +npm test +``` + +For more information, refer to the [Running tests page][]. + +### Code quality + +Sonar is used to analyse code quality. You can start a local Sonar server (accessible on http://localhost:9001) with: + +``` +docker compose -f src/main/docker/sonar.yml up -d +``` + +Note: we have turned off forced authentication redirect for UI in [src/main/docker/sonar.yml](src/main/docker/sonar.yml) for out of the box experience while trying out SonarQube, for real use cases turn it back on. + +You can run a Sonar analysis with using the [sonar-scanner](https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner) or by using the maven plugin. + +Then, run a Sonar analysis: + +``` +./mvnw -Pprod clean verify sonar:sonar -Dsonar.login=admin -Dsonar.password=admin +``` + +If you need to re-run the Sonar phase, please be sure to specify at least the `initialize` phase since Sonar properties are loaded from the sonar-project.properties file. + +``` +./mvnw initialize sonar:sonar -Dsonar.login=admin -Dsonar.password=admin +``` + +Additionally, Instead of passing `sonar.password` and `sonar.login` as CLI arguments, these parameters can be configured from [sonar-project.properties](sonar-project.properties) as shown below: + +``` +sonar.login=admin +sonar.password=admin +``` + +For more information, refer to the [Code quality page][]. + +## Using Docker to simplify development (optional) + +You can use Docker to improve your JHipster development experience. A number of docker-compose configuration are available in the [src/main/docker](src/main/docker) folder to launch required third party services. + +You can also fully dockerize your application and all the services that it depends on. +To achieve this, first build a docker image of your app by running: + +``` +npm run java:docker +``` + +Or build a arm64 docker image when using an arm64 processor os like MacOS with M1 processor family running: + +``` +npm run java:docker:arm64 +``` + +Then run: + +``` +docker compose -f src/main/docker/app.yml up -d +``` + +When running Docker Desktop on MacOS Big Sur or later, consider enabling experimental `Use the new Virtualization framework` for better processing performance ([disk access performance is worse](https://github.com/docker/roadmap/issues/7)). + +For more information refer to [Using Docker and Docker-Compose][], this page also contains information on the docker-compose sub-generator (`jhipster docker-compose`), which is able to generate docker configurations for one or several JHipster applications. + +## Continuous Integration (optional) + +To configure CI for your project, run the ci-cd sub-generator (`jhipster ci-cd`), this will let you generate configuration files for a number of Continuous Integration systems. Consult the [Setting up Continuous Integration][] page for more information. + +[JHipster Homepage and latest documentation]: https://www.jhipster.tech +[JHipster 8.0.0-beta.2 archive]: https://www.jhipster.tech/documentation-archive/v8.0.0-beta.2 +[Using JHipster in development]: https://www.jhipster.tech/documentation-archive/v8.0.0-beta.2/development/ +[Using Docker and Docker-Compose]: https://www.jhipster.tech/documentation-archive/v8.0.0-beta.2/docker-compose +[Using JHipster in production]: https://www.jhipster.tech/documentation-archive/v8.0.0-beta.2/production/ +[Running tests page]: https://www.jhipster.tech/documentation-archive/v8.0.0-beta.2/running-tests/ +[Code quality page]: https://www.jhipster.tech/documentation-archive/v8.0.0-beta.2/code-quality/ +[Setting up Continuous Integration]: https://www.jhipster.tech/documentation-archive/v8.0.0-beta.2/setting-up-ci/ +[Node.js]: https://nodejs.org/ +[NPM]: https://www.npmjs.com/ +[Webpack]: https://webpack.github.io/ +[BrowserSync]: https://www.browsersync.io/ +[Jest]: https://facebook.github.io/jest/ +[Leaflet]: https://leafletjs.com/ +[DefinitelyTyped]: https://definitelytyped.org/ +[Angular CLI]: https://cli.angular.io/ diff --git a/angular.json b/angular.json new file mode 100644 index 0000000..9c84ecc --- /dev/null +++ b/angular.json @@ -0,0 +1,109 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "isdashboard": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + }, + "@schematics/angular:application": { + "strict": true + } + }, + "root": "", + "sourceRoot": "src/main/webapp", + "prefix": "jhi", + "architect": { + "build": { + "builder": "@angular-builders/custom-webpack:browser", + "options": { + "customWebpackConfig": { + "path": "./webpack/webpack.custom.js" + }, + "outputPath": "target/classes/static/", + "index": "src/main/webapp/index.html", + "main": "src/main/webapp/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/main/webapp/content", + "src/main/webapp/favicon.ico", + "src/main/webapp/manifest.webapp", + "src/main/webapp/robots.txt" + ], + "styles": ["src/main/webapp/content/scss/vendor.scss", "src/main/webapp/content/scss/global.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "serviceWorker": true, + "ngswConfigPath": "ngsw-config.json", + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-builders/custom-webpack:dev-server", + "options": { + "browserTarget": "isdashboard:build:development", + "port": 4200 + }, + "configurations": { + "production": { + "browserTarget": "isdashboard:build:production" + }, + "development": { + "browserTarget": "isdashboard:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular-builders/jest:run", + "options": { + "configPath": "jest.conf.js" + } + } + } + } + }, + "cli": { + "cache": { + "enabled": true, + "path": "./target/angular/", + "environment": "all" + }, + "packageManager": "npm" + } +} diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..5d5ae65 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/jest.conf.js b/jest.conf.js new file mode 100644 index 0000000..969b41f --- /dev/null +++ b/jest.conf.js @@ -0,0 +1,29 @@ +const { pathsToModuleNameMapper } = require('ts-jest'); + +const { + compilerOptions: { paths = {}, baseUrl = './' }, +} = require('./tsconfig.json'); +const environment = require('./webpack/environment'); + +module.exports = { + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$|dayjs/esm)'], + resolver: 'jest-preset-angular/build/resolvers/ng-jest-resolver.js', + globals: { + ...environment, + }, + roots: ['', `/${baseUrl}`], + modulePaths: [`/${baseUrl}`], + setupFiles: ['jest-date-mock'], + cacheDirectory: '/target/jest-cache', + coverageDirectory: '/target/test-results/', + moduleNameMapper: pathsToModuleNameMapper(paths, { prefix: `/${baseUrl}/` }), + reporters: [ + 'default', + ['jest-junit', { outputDirectory: '/target/test-results/', outputName: 'TESTS-results-jest.xml' }], + ['jest-sonar', { outputDirectory: './target/test-results/jest', outputName: 'TESTS-results-sonar.xml' }], + ], + testMatch: ['/src/main/webapp/app/**/@(*.)@(spec.ts)'], + testEnvironmentOptions: { + url: 'https://jhipster.tech', + }, +}; diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..8d937f4 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..c4586b5 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/ngsw-config.json b/ngsw-config.json new file mode 100644 index 0000000..8d57602 --- /dev/null +++ b/ngsw-config.json @@ -0,0 +1,21 @@ +{ + "$schema": "./node_modules/@angular/service-worker/config/schema.json", + "index": "/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": ["/favicon.ico", "/index.html", "/manifest.webapp", "/*.css", "/*.js"] + } + }, + { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": ["/content/**", "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"] + } + } + ] +} diff --git a/npmw b/npmw new file mode 100644 index 0000000..12a91e5 --- /dev/null +++ b/npmw @@ -0,0 +1,42 @@ +#!/bin/sh + +basedir=`dirname "$0"` + +if [ -f "$basedir/mvnw" ]; then + bindir="$basedir/target/node" + repodir="$basedir/target/node/node_modules" + installCommand="$basedir/mvnw -Pwebapp frontend:install-node-and-npm@install-node-and-npm" + + PATH="$basedir/$builddir/:$PATH" + NPM_EXE="$basedir/$builddir/node_modules/npm/bin/npm-cli.js" + NODE_EXE="$basedir/$builddir/node" +elif [ -f "$basedir/gradlew" ]; then + bindir="$basedir/build/node/bin" + repodir="$basedir/build/node/lib/node_modules" + installCommand="$basedir/gradlew npmSetup" +else + echo "Using npm installed globally" + exec npm "$@" +fi + +NPM_EXE="$repodir/npm/bin/npm-cli.js" +NODE_EXE="$bindir/node" + +if [ ! -x "$NPM_EXE" ] || [ ! -x "$NODE_EXE" ]; then + $installCommand || true +fi + +if [ -x "$NODE_EXE" ]; then + echo "Using node installed locally $($NODE_EXE --version)" + PATH="$bindir:$PATH" +else + NODE_EXE='node' +fi + +if [ ! -x "$NPM_EXE" ]; then + echo "Local npm not found, using npm installed globally" + npm "$@" +else + echo "Using npm installed locally $($NODE_EXE $NPM_EXE --version)" + $NODE_EXE $NPM_EXE "$@" +fi diff --git a/npmw.cmd b/npmw.cmd new file mode 100644 index 0000000..b6e7980 --- /dev/null +++ b/npmw.cmd @@ -0,0 +1,31 @@ +@echo off + +setlocal + +set NPMW_DIR=%~dp0 + +if exist "%NPMW_DIR%mvnw.cmd" ( + set NODE_EXE=^"^" + set NODE_PATH=%NPMW_DIR%target\node\ + set NPM_EXE=^"%NPMW_DIR%target\node\npm.cmd^" + set INSTALL_NPM_COMMAND=^"%NPMW_DIR%mvnw.cmd^" -Pwebapp frontend:install-node-and-npm@install-node-and-npm +) else ( + set NODE_EXE=^"%NPMW_DIR%build\node\bin\node.exe^" + set NODE_PATH=%NPMW_DIR%build\node\bin\ + set NPM_EXE=^"%NPMW_DIR%build\node\lib\node_modules\npm\bin\npm-cli.js^" + set INSTALL_NPM_COMMAND=^"%NPMW_DIR%gradlew.bat^" npmSetup +) + +if not exist %NPM_EXE% ( + call %INSTALL_NPM_COMMAND% +) + +if exist %NODE_EXE% ( + Rem execute local npm with local node, whilst adding local node location to the PATH for this CMD session + endlocal & echo "%PATH%"|find /i "%NODE_PATH%;">nul || set "PATH=%NODE_PATH%;%PATH%" & call %NODE_EXE% %NPM_EXE% %* +) else if exist %NPM_EXE% ( + Rem execute local npm, whilst adding local npm location to the PATH for this CMD session + endlocal & echo "%PATH%"|find /i "%NODE_PATH%;">nul || set "PATH=%NODE_PATH%;%PATH%" & call %NPM_EXE% %* +) else ( + call npm %* +) diff --git a/package.json b/package.json new file mode 100644 index 0000000..fc29382 --- /dev/null +++ b/package.json @@ -0,0 +1,138 @@ +{ + "name": "isdashboard", + "version": "0.0.1-SNAPSHOT", + "private": true, + "description": "Description for Isdashboard", + "license": "UNLICENSED", + "scripts": { + "app:start": "./mvnw", + "app:up": "docker compose -f src/main/docker/app.yml up --wait", + "backend:build-cache": "./mvnw dependency:go-offline", + "backend:debug": "./mvnw -Dspring-boot.run.jvmArguments=\"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000\"", + "backend:doc:test": "./mvnw -ntp javadoc:javadoc --batch-mode", + "backend:info": "./mvnw -ntp enforcer:display-info --batch-mode", + "backend:nohttp:test": "./mvnw -ntp checkstyle:check --batch-mode", + "backend:start": "./mvnw -Dskip.installnodenpm -Dskip.npm", + "backend:unit:test": "./mvnw -ntp -Dskip.installnodenpm -Dskip.npm verify --batch-mode -Dlogging.level.ROOT=OFF -Dlogging.level.tech.jhipster=OFF -Dlogging.level.org.gcube.isdashboard=OFF -Dlogging.level.org.springframework=OFF -Dlogging.level.org.springframework.web=OFF -Dlogging.level.org.springframework.security=OFF", + "build": "npm run webapp:prod --", + "build-watch": "concurrently 'npm run webapp:build:dev -- --watch' npm:backend:start", + "ci:backend:test": "npm run backend:info && npm run backend:doc:test && npm run backend:nohttp:test && npm run backend:unit:test -- -P$npm_package_config_default_environment", + "ci:e2e:package": "npm run java:$npm_package_config_packaging:$npm_package_config_default_environment -- -Pe2e -Denforcer.skip=true", + "ci:e2e:prepare": "npm run ci:e2e:prepare:docker", + "ci:e2e:prepare:docker": "npm run services:up --if-present && docker ps -a", + "preci:e2e:server:start": "npm run services:db:await --if-present && npm run services:others:await --if-present", + "ci:e2e:server:start": "java -jar target/e2e.$npm_package_config_packaging --spring.profiles.active=e2e,$npm_package_config_default_environment -Dlogging.level.ROOT=OFF -Dlogging.level.tech.jhipster=OFF -Dlogging.level.org.gcube.isdashboard=OFF -Dlogging.level.org.springframework=OFF -Dlogging.level.org.springframework.web=OFF -Dlogging.level.org.springframework.security=OFF --logging.level.org.springframework.web=ERROR", + "ci:e2e:teardown": "npm run ci:e2e:teardown:docker --if-present", + "ci:frontend:build": "npm run webapp:build:$npm_package_config_default_environment", + "ci:frontend:test": "npm run ci:frontend:build && npm test", + "clean-www": "rimraf target/classes/static/app/{src,target/}", + "cleanup": "rimraf target/classes/static/", + "docker:db:up": "echo \"Docker for db no not configured for application isdashboard\"", + "java:docker": "./mvnw -ntp verify -DskipTests -Pprod jib:dockerBuild", + "java:docker:arm64": "npm run java:docker -- -Djib-maven-plugin.architecture=arm64", + "java:docker:dev": "npm run java:docker -- -Pdev,webapp", + "java:docker:prod": "npm run java:docker -- -Pprod", + "java:jar": "./mvnw -ntp verify -DskipTests --batch-mode", + "java:jar:dev": "npm run java:jar -- -Pdev,webapp", + "java:jar:prod": "npm run java:jar -- -Pprod", + "java:war": "./mvnw -ntp verify -DskipTests --batch-mode -Pwar", + "java:war:dev": "npm run java:war -- -Pdev,webapp", + "java:war:prod": "npm run java:war -- -Pprod", + "jest": "jest --coverage --logHeapUsage --maxWorkers=2 --config jest.conf.js", + "lint": "eslint . --ext .js,.ts", + "lint:fix": "npm run lint -- --fix", + "prepare": "husky install", + "prettier:check": "prettier --check \"{,src/**/,webpack/,.blueprint/**/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}\"", + "prettier:format": "prettier --write \"{,src/**/,webpack/,.blueprint/**/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}\"", + "serve": "npm run start --", + "start": "ng serve --hmr", + "start-tls": "npm run webapp:dev-ssl", + "pretest": "npm run lint", + "test": "ng test --coverage --log-heap-usage -w=2", + "test:watch": "npm run test -- --watch", + "watch": "concurrently npm:start npm:backend:start", + "webapp:build": "npm run clean-www && npm run webapp:build:dev", + "webapp:build:dev": "ng build --configuration development", + "webapp:build:prod": "ng build --configuration production", + "webapp:dev": "ng serve", + "webapp:dev-ssl": "ng serve --ssl", + "webapp:dev-verbose": "ng serve --verbose", + "webapp:prod": "npm run clean-www && npm run webapp:build:prod", + "webapp:test": "npm run test --" + }, + "config": { + "backend_port": 8080, + "default_environment": "prod", + "packaging": "jar" + }, + "dependencies": { + "@angular/common": "16.1.4", + "@angular/compiler": "16.1.4", + "@angular/core": "16.1.4", + "@angular/forms": "16.1.4", + "@angular/localize": "16.1.4", + "@angular/platform-browser": "16.1.4", + "@angular/platform-browser-dynamic": "16.1.4", + "@angular/router": "16.1.4", + "@fortawesome/angular-fontawesome": "0.13.0", + "@fortawesome/fontawesome-svg-core": "6.4.0", + "@fortawesome/free-solid-svg-icons": "6.4.0", + "@ng-bootstrap/ng-bootstrap": "15.1.0", + "@popperjs/core": "2.11.8", + "bootstrap": "5.3.0", + "dayjs": "1.11.9", + "ngx-infinite-scroll": "16.0.0", + "rxjs": "7.8.1", + "tslib": "2.6.0", + "zone.js": "0.13.1" + }, + "devDependencies": { + "@angular-builders/custom-webpack": "16.0.0", + "@angular-builders/jest": "16.0.0", + "@angular-devkit/build-angular": "16.1.4", + "@angular-eslint/eslint-plugin": "16.0.2", + "@angular/cli": "16.1.4", + "@angular/compiler-cli": "16.1.4", + "@angular/service-worker": "16.1.4", + "@types/jest": "29.5.3", + "@types/node": "18.16.19", + "@typescript-eslint/eslint-plugin": "5.61.0", + "@typescript-eslint/parser": "5.61.0", + "browser-sync": "2.29.3", + "browser-sync-webpack-plugin": "2.3.0", + "concurrently": "8.2.0", + "copy-webpack-plugin": "11.0.0", + "eslint": "8.44.0", + "eslint-config-prettier": "8.8.0", + "eslint-webpack-plugin": "4.0.1", + "generator-jhipster": "8.0.0-beta.2", + "husky": "8.0.3", + "jest": "29.6.1", + "jest-date-mock": "1.0.8", + "jest-environment-jsdom": "29.6.1", + "jest-junit": "16.0.0", + "jest-preset-angular": "13.1.1", + "jest-sonar": "0.2.16", + "lint-staged": "13.2.3", + "prettier": "2.8.8", + "prettier-plugin-java": "2.2.0", + "prettier-plugin-packagejson": "2.4.5", + "rimraf": "5.0.1", + "swagger-ui-dist": "5.1.0", + "ts-jest": "29.1.1", + "typescript": "5.1.6", + "wait-on": "7.0.1", + "webpack-bundle-analyzer": "4.9.0", + "webpack-merge": "5.9.0", + "webpack-notifier": "1.15.0" + }, + "engines": { + "node": ">=18.16.1" + }, + "cacheDirectories": [ + "node_modules" + ], + "overrides": { + "webpack": "5.88.1" + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..41905bd --- /dev/null +++ b/pom.xml @@ -0,0 +1,1033 @@ + + + 4.0.0 + + org.gcube.isdashboard + isdashboard + 0.0.1-SNAPSHOT + jar + Isdashboard + Description for Isdashboard + + + + 3.2.5 + 17 + v18.16.1 + 9.8.0 + UTF-8 + UTF-8 + yyyyMMddHHmmss + ${java.version} + ${java.version} + org.gcube.isdashboard.IsdashboardApp + -Djava.security.egd=file:/dev/./urandom -Xmx1G + jdt_apt + false + 8.0.0-beta.2 + 3.1.1 + 1.0.1 + 10.12.1 + 1.11 + 1.13.4 + 6.0.0 + 0.8.10 + amd64 + eclipse-temurin:17-jre-focal + 3.3.2 + 1.0.0 + 1.5.5.Final + 3.1.0 + 3.3.0 + 3.3.1 + 3.11.0 + 2.1 + 3.3.0 + 3.1.2 + 2.2.1 + 3.3.0 + 3.5.0 + 3.3.1 + 3.12.1 + 3.1.2 + 3.4.0 + 2.6.0 + 0.0.11 + + + + + 1.1.0 + 3.9.1.2184 + 2.37.0 + + + + + + tech.jhipster + jhipster-dependencies + ${jhipster-dependencies.version} + pom + import + + + + + + + tech.jhipster + jhipster-framework + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + org.springframework.boot + spring-boot-loader-tools + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-test + test + + + org.springframework.security + spring-security-test + test + + + org.springdoc + springdoc-openapi-starter-webmvc-api + + + com.fasterxml.jackson.datatype + jackson-datatype-hppc + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.tngtech.archunit + archunit-junit5-api + ${archunit-junit5.version} + test + + + + + com.tngtech.archunit + archunit-junit5-engine + ${archunit-junit5.version} + test + + + io.dropwizard.metrics + metrics-core + + + io.micrometer + micrometer-registry-prometheus + + + jakarta.annotation + jakarta.annotation-api + + + javax.cache + cache-api + + + org.apache.commons + commons-lang3 + + + org.ehcache + ehcache + jakarta + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + + + spring-boot:run + + + org.springframework.boot + spring-boot-maven-plugin + + + com.diffplug.spotless + spotless-maven-plugin + + + com.google.cloud.tools + jib-maven-plugin + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-eclipse-plugin + + + org.apache.maven.plugins + maven-enforcer-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + + + org.apache.maven.plugins + maven-idea-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.codehaus.mojo + properties-maven-plugin + + + org.gaul + modernizer-maven-plugin + + + org.jacoco + jacoco-maven-plugin + + + org.sonarsource.scanner.maven + sonar-maven-plugin + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + ${start-class} + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless-maven-plugin.version} + + + + + + + + spotless + process-sources + + apply + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + target + ${node.version} + ${npm.version} + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${jib-maven-plugin.image} + + + ${jib-maven-plugin.architecture} + linux + + + + + isdashboard:latest + + + + bash + + /entrypoint.sh + + + 8080 + + + ALWAYS + 0 + + USE_CURRENT_TIMESTAMP + 1000 + + + src/main/docker/jib + + + /entrypoint.sh + 755 + + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + ${git-commit-id-maven-plugin.version} + + + + revision + + + + + false + false + true + + ^git.commit.id.abbrev$ + ^git.commit.id.describe$ + ^git.branch$ + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + io.spring.nohttp + nohttp-checkstyle + ${nohttp-checkstyle.version} + + + + checkstyle.xml + pom.xml,README.md + .git/**/*,target/**/*,node_modules/**/*,node/**/* + ./ + + + + + check + + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + org.apache.maven.plugins + maven-eclipse-plugin + ${maven-eclipse-plugin.version} + + true + true + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-versions + + enforce + + + + enforce-dependencyConvergence + + + + + false + + + enforce + + + + + + + You are running an older version of Maven. JHipster requires at least Maven ${maven.version} + [${maven.version},) + + + You are running an incompatible version of Java. JHipster supports JDK 17 to 19. + [17,18),[18,19),[19,20) + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + + + ${project.build.outputDirectory} + alphabetical + + **/*IT* + **/*IntTest* + + @{argLine} -Dspring.profiles.active=${profile.test} + + + + integration-test + + integration-test + + + + verify + + verify + + + + + + org.apache.maven.plugins + maven-idea-plugin + ${maven-idea-plugin.version} + + node_modules + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + ${maven.compiler.source} + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + default-resources + validate + + copy-resources + + + ${project.build.directory}/classes + false + + # + + + + src/main/resources/ + true + + config/*.yml + + + + src/main/resources/ + false + + config/*.yml + + + + + + + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + alphabetical + + **/*IT* + **/*IntTest* + + + + + org.apache.maven.plugins + maven-war-plugin + ${maven-war-plugin.version} + + + default-war + + war + + package + + + + WEB-INF/**,META-INF/** + false + target/classes/static/ + + + src/main/webapp + + WEB-INF/** + + + + + + + org.codehaus.mojo + properties-maven-plugin + ${properties-maven-plugin.version} + + + initialize + + read-project-properties + + + + sonar-project.properties + + + + + + + org.gaul + modernizer-maven-plugin + ${modernizer-maven-plugin.version} + + + modernizer + package + + modernizer + + + + + ${java.version} + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + pre-unit-tests + + prepare-agent + + + + + post-unit-test + test + + report + + + + pre-integration-tests + + prepare-agent-integration + + + + + post-integration-tests + post-integration-test + + report-integration + + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar-maven-plugin.version} + + + + + + + + api-docs + + ,api-docs + + + + dev + + true + + + + dev${profile.tls} + testdev + + + + org.springframework.boot + spring-boot-devtools + true + + + + + + eclipse + + + m2e.version + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + + + org.eclipse.m2e + lifecycle-mapping + ${lifecycle-mapping.version} + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + prepare-agent + + + + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + install-node-and-npm + npm + + + + + + + + + + + + + + + + + IDE + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + prod + + + prod${profile.api-docs}${profile.tls}${profile.e2e} + testprod + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + target/classes/static/ + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + + + com.github.eirslett + frontend-maven-plugin + + + install-node-and-npm + + install-node-and-npm + + + + npm install + + npm + + + + webapp build test + + npm + + test + + run webapp:test + false + + + + webapp build prod + + npm + + generate-resources + + run webapp:prod + + ${project.version} + + false + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + + + + + + tls + + ,tls + + + + war + + + + org.apache.maven.plugins + maven-war-plugin + + + + + + webapp + + true + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + ${checksum-maven-plugin.version} + + + create-pre-compiled-webapp-checksum + + files + + generate-resources + + + create-compiled-webapp-checksum + + files + + compile + + checksums.csv.old + + + + + + + ${project.basedir} + + src/main/webapp/**/*.* + target/classes/static/**/*.* + package-lock.json + package.json + webpack/*.* + tsconfig.json + tsconfig.app.json + + + **/app/**/service-worker.js + **/app/**/vendor.css + + + + false + false + false + + SHA-1 + + true + true + + + + org.apache.maven.plugins + maven-antrun-plugin + ${maven-antrun-plugin.version} + + + eval-frontend-checksum + generate-resources + + run + + + + + + + + + + + + true + + + + + + com.github.eirslett + frontend-maven-plugin + + + install-node-and-npm + + install-node-and-npm + + + + npm install + + npm + + + + webapp build dev + + npm + + generate-resources + + run webapp:build + + ${project.version} + + false + + + + + + + + + dev + + + + diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..4a1bef3 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,31 @@ +sonar.projectKey=isdashboard +sonar.projectName=isdashboard generated by jhipster + +# Typescript tests files must be inside sources and tests, othewise `INFO: Test execution data ignored for 80 unknown files, including:` is shown. +sonar.sources=src +sonar.tests=src +sonar.host.url=http://localhost:9001 + +sonar.test.inclusions=src/test/**/*.*, src/main/webapp/app/**/*.spec.ts +sonar.coverage.jacoco.xmlReportPaths=target/site/**/jacoco*.xml +sonar.java.codeCoveragePlugin=jacoco +sonar.junit.reportPaths=target/surefire-reports,target/failsafe-reports +sonar.testExecutionReportPaths=target/test-results/jest/TESTS-results-sonar.xml +sonar.javascript.lcov.reportPaths=target/test-results/lcov.info + +sonar.sourceEncoding=UTF-8 +sonar.exclusions=src/main/webapp/content/**/*.*, src/main/webapp/i18n/*.js, target/classes/static/**/*.* + +sonar.issue.ignore.multicriteria=S3437,S4684,S5145,UndocumentedApi +# Rule https://rules.sonarsource.com/java/RSPEC-3437 is ignored, as a JPA-managed field cannot be transient +sonar.issue.ignore.multicriteria.S3437.resourceKey=src/main/java/**/* +sonar.issue.ignore.multicriteria.S3437.ruleKey=squid:S3437 +# Rule https://rules.sonarsource.com/java/RSPEC-4684 +sonar.issue.ignore.multicriteria.S4684.resourceKey=src/main/java/**/* +sonar.issue.ignore.multicriteria.S4684.ruleKey=java:S4684 +# Rule https://rules.sonarsource.com/java/RSPEC-5145 log filter is applied +sonar.issue.ignore.multicriteria.S5145.resourceKey=src/main/java/**/* +sonar.issue.ignore.multicriteria.S5145.ruleKey=javasecurity:S5145 +# Rule https://rules.sonarsource.com/java/RSPEC-1176 is ignored, as we want to follow "clean code" guidelines and classes, methods and arguments names should be self-explanatory +sonar.issue.ignore.multicriteria.UndocumentedApi.resourceKey=src/main/java/**/* +sonar.issue.ignore.multicriteria.UndocumentedApi.ruleKey=squid:UndocumentedApi diff --git a/src/main/docker/app.yml b/src/main/docker/app.yml new file mode 100644 index 0000000..794b8e4 --- /dev/null +++ b/src/main/docker/app.yml @@ -0,0 +1,18 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: isdashboard +services: + app: + image: isdashboard + environment: + - _JAVA_OPTIONS=-Xmx512m -Xms256m + - SPRING_PROFILES_ACTIVE=prod,api-docs + - MANAGEMENT_PROMETHEUS_METRICS_EXPORT_ENABLED=true + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:8080:8080 + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:8080/management/health'] + interval: 5s + timeout: 5s + retries: 40 diff --git a/src/main/docker/grafana/provisioning/dashboards/JVM.json b/src/main/docker/grafana/provisioning/dashboards/JVM.json new file mode 100644 index 0000000..5104abc --- /dev/null +++ b/src/main/docker/grafana/provisioning/dashboards/JVM.json @@ -0,0 +1,3778 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "limit": 100, + "name": "Annotations & Alerts", + "showIn": 0, + "type": "dashboard" + }, + { + "datasource": "Prometheus", + "enable": true, + "expr": "resets(process_uptime_seconds{application=\"$application\", instance=\"$instance\"}[1m]) > 0", + "iconColor": "rgba(255, 96, 96, 1)", + "name": "Restart Detection", + "showIn": 0, + "step": "1m", + "tagKeys": "restart-tag", + "textFormat": "uptime reset", + "titleFormat": "Restart" + } + ] + }, + "description": "Dashboard for Micrometer instrumented applications (Java, Spring Boot, Micronaut)", + "editable": true, + "gnetId": 4701, + "graphTooltip": 1, + "iteration": 1553765841423, + "links": [], + "panels": [ + { + "content": "\n# Acknowledgments\n\nThank you to [Michael Weirauch](https://twitter.com/emwexx) for creating this dashboard: see original JVM (Micrometer) dashboard at [https://grafana.com/dashboards/4701](https://grafana.com/dashboards/4701)\n\n\n\n", + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 141, + "links": [], + "mode": "markdown", + "timeFrom": null, + "timeShift": null, + "title": "Acknowledgments", + "type": "text" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 125, + "panels": [], + "repeat": null, + "title": "Quick Facts", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + "datasource": "Prometheus", + "decimals": 1, + "editable": true, + "error": false, + "format": "s", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 4 + }, + "height": "", + "id": 63, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "process_uptime_seconds{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "", + "title": "Uptime", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + "datasource": "Prometheus", + "decimals": null, + "editable": true, + "error": false, + "format": "dateTimeAsIso", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 4 + }, + "height": "", + "id": 92, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "process_start_time_seconds{application=\"$application\", instance=\"$instance\"}*1000", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "", + "title": "Start time", + "type": "singlestat", + "valueFontSize": "70%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)"], + "datasource": "Prometheus", + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 65, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})*100/sum(jvm_memory_max_bytes{application=\"$application\",instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "70,90", + "title": "Heap used", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)"], + "datasource": "Prometheus", + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 75, + "interval": null, + "links": [], + "mappingType": 2, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + }, + { + "from": "-99999999999999999999999999999999", + "text": "N/A", + "to": "0" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})*100/sum(jvm_memory_max_bytes{application=\"$application\",instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "70,90", + "title": "Non-Heap used", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + }, + { + "op": "=", + "text": "x", + "value": "" + } + ], + "valueName": "current" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 126, + "panels": [], + "repeat": null, + "title": "I/O Overview", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 111, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\"}[1m]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HTTP", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "HTTP": "#890f02", + "HTTP - 5xx": "#bf1b00" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 112, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\", status=~\"5..\"}[1m]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HTTP - 5xx", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Errors", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 113, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_sum{application=\"$application\", instance=\"$instance\", status!~\"5..\"}[1m]))/sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\", status!~\"5..\"}[1m]))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "HTTP - AVG", + "refId": "A" + }, + { + "expr": "max(http_server_requests_seconds_max{application=\"$application\", instance=\"$instance\", status!~\"5..\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "HTTP - MAX", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Duration", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 127, + "panels": [], + "repeat": null, + "title": "JVM Memory", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 24, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 25, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Non-Heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 26, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + }, + { + "expr": "process_memory_vss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": true, + "intervalFactor": 2, + "legendFormat": "vss", + "metric": "", + "refId": "D", + "step": 2400 + }, + { + "expr": "process_memory_rss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "rss", + "refId": "E", + "step": 2400 + }, + { + "expr": "process_memory_pss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "pss", + "refId": "F", + "step": 2400 + }, + { + "expr": "process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "swap", + "refId": "G", + "step": 2400 + }, + { + "expr": "process_memory_swappss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "swappss", + "refId": "H", + "step": 2400 + }, + { + "expr": "process_memory_pss_bytes{application=\"$application\", instance=\"$instance\"} + process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "phys (pss+swap)", + "refId": "I", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Total", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 128, + "panels": [], + "repeat": null, + "title": "JVM Misc", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 24 + }, + "id": 106, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_cpu_usage{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "system", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "process_cpu_usage{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "process", + "refId": "B" + }, + { + "expr": "avg_over_time(process_cpu_usage{application=\"$application\", instance=\"$instance\"}[1h])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "process-1h", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "CPU", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 1, + "format": "percentunit", + "label": "", + "logBase": 1, + "max": "1", + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 24 + }, + "id": 93, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_load_average_1m{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "system-1m", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 2, + "refId": "B" + }, + { + "expr": "system_cpu_count{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "cpu", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Load", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 1, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 24 + }, + "id": 32, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_threads_live{application=\"$application\", instance=\"$instance\"} or jvm_threads_live_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "live", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_threads_daemon{application=\"$application\", instance=\"$instance\"} or jvm_threads_daemon_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "daemon", + "metric": "", + "refId": "B", + "step": 2400 + }, + { + "expr": "jvm_threads_peak{application=\"$application\", instance=\"$instance\"} or jvm_threads_peak_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "peak", + "refId": "C", + "step": 2400 + }, + { + "expr": "process_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "process", + "refId": "D", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Threads", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "blocked": "#bf1b00", + "new": "#fce2de", + "runnable": "#7eb26d", + "terminated": "#511749", + "timed-waiting": "#c15c17", + "waiting": "#eab839" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 24 + }, + "id": 124, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_threads_states_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{state}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Thread States", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "debug": "#1F78C1", + "error": "#BF1B00", + "info": "#508642", + "trace": "#6ED0E0", + "warn": "#EAB839" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 18, + "x": 0, + "y": 31 + }, + "height": "", + "id": 91, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "error", + "yaxis": 1 + }, + { + "alias": "warn", + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "increase(logback_events_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{level}}", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Log Events (1m)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 31 + }, + "id": 61, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "process_open_fds{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "open", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "process_max_fds{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "B", + "step": 2400 + }, + { + "expr": "process_files_open{application=\"$application\", instance=\"$instance\"} or process_files_open_files{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "open", + "refId": "C" + }, + { + "expr": "process_files_max{application=\"$application\", instance=\"$instance\"} or process_files_max_files{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "D" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "File Descriptors", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 10, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 129, + "panels": [], + "repeat": "persistence_counts", + "title": "JVM Memory Pools (Heap)", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 39 + }, + "id": 3, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": "jvm_memory_pool_heap", + "scopedVars": { + "jvm_memory_pool_heap": { + "selected": false, + "text": "PS Eden Space", + "value": "PS Eden Space" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 39 + }, + "id": 134, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 3, + "scopedVars": { + "jvm_memory_pool_heap": { + "selected": false, + "text": "PS Old Gen", + "value": "PS Old Gen" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 39 + }, + "id": 135, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 3, + "scopedVars": { + "jvm_memory_pool_heap": { + "selected": false, + "text": "PS Survivor Space", + "value": "PS Survivor Space" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 46 + }, + "id": 130, + "panels": [], + "repeat": null, + "title": "JVM Memory Pools (Non-Heap)", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 47 + }, + "id": 78, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": "jvm_memory_pool_nonheap", + "scopedVars": { + "jvm_memory_pool_nonheap": { + "selected": false, + "text": "Metaspace", + "value": "Metaspace" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_nonheap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 47 + }, + "id": 136, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 78, + "scopedVars": { + "jvm_memory_pool_nonheap": { + "selected": false, + "text": "Compressed Class Space", + "value": "Compressed Class Space" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_nonheap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 47 + }, + "id": 137, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 78, + "scopedVars": { + "jvm_memory_pool_nonheap": { + "selected": false, + "text": "Code Cache", + "value": "Code Cache" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_nonheap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 54 + }, + "id": 131, + "panels": [], + "repeat": null, + "title": "Garbage Collection", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 55 + }, + "id": 98, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_pause_seconds_count{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "{{action}} ({{cause}})", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Collections", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 55 + }, + "id": 101, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_pause_seconds_sum{application=\"$application\", instance=\"$instance\"}[1m])/rate(jvm_gc_pause_seconds_count{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "avg {{action}} ({{cause}})", + "refId": "A" + }, + { + "expr": "jvm_gc_pause_seconds_max{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "max {{action}} ({{cause}})", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Pause Durations", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 55 + }, + "id": 99, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_memory_allocated_bytes_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "allocated", + "refId": "A" + }, + { + "expr": "rate(jvm_gc_memory_promoted_bytes_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "promoted", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Allocated/Promoted", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 62 + }, + "id": 132, + "panels": [], + "repeat": null, + "title": "Classloading", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 63 + }, + "id": 37, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_classes_loaded{application=\"$application\", instance=\"$instance\"} or jvm_classes_loaded_classes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "loaded", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Classes loaded", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 63 + }, + "id": 38, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "delta(jvm_classes_loaded{application=\"$application\",instance=\"$instance\"}[5m]) or delta(jvm_classes_loaded_classes{application=\"$application\",instance=\"$instance\"}[5m])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "delta", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Class delta (5m)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["ops", "short"], + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 70 + }, + "id": 133, + "panels": [], + "repeat": null, + "title": "Buffer Pools", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 71 + }, + "id": 33, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"direct\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_buffer_total_capacity_bytes{application=\"$application\", instance=\"$instance\", id=\"direct\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "capacity", + "metric": "", + "refId": "B", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Direct Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 71 + }, + "id": 83, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_count{application=\"$application\", instance=\"$instance\", id=\"direct\"} or jvm_buffer_count_buffers{application=\"$application\", instance=\"$instance\", id=\"direct\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "count", + "metric": "", + "refId": "A", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Direct Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 71 + }, + "id": 85, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"mapped\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_buffer_total_capacity_bytes{application=\"$application\", instance=\"$instance\", id=\"mapped\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "capacity", + "metric": "", + "refId": "B", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Mapped Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 71 + }, + "id": 84, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_count{application=\"$application\", instance=\"$instance\", id=\"mapped\"} or jvm_buffer_count_buffers{application=\"$application\", instance=\"$instance\", id=\"mapped\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "count", + "metric": "", + "refId": "A", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Mapped Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "10s", + "schemaVersion": 18, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "text": "test", + "value": "test" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Application", + "multi": false, + "name": "application", + "options": [], + "query": "label_values(application)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "text": "localhost:8080", + "value": "localhost:8080" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Instance", + "multi": false, + "multiFormat": "glob", + "name": "instance", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\"}, instance)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": true, + "label": "JVM Memory Pools Heap", + "multi": false, + "multiFormat": "glob", + "name": "jvm_memory_pool_heap", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"},id)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": true, + "label": "JVM Memory Pools Non-Heap", + "multi": false, + "multiFormat": "glob", + "name": "jvm_memory_pool_nonheap", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"},id)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 2, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "now": true, + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "browser", + "title": "JVM (Micrometer)", + "uid": "Ud1CFe3iz", + "version": 1 +} diff --git a/src/main/docker/grafana/provisioning/dashboards/dashboard.yml b/src/main/docker/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 0000000..4817a83 --- /dev/null +++ b/src/main/docker/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: 'Prometheus' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/provisioning/dashboards diff --git a/src/main/docker/grafana/provisioning/datasources/datasource.yml b/src/main/docker/grafana/provisioning/datasources/datasource.yml new file mode 100644 index 0000000..57b2bb3 --- /dev/null +++ b/src/main/docker/grafana/provisioning/datasources/datasource.yml @@ -0,0 +1,50 @@ +apiVersion: 1 + +# list of datasources that should be deleted from the database +deleteDatasources: + - name: Prometheus + orgId: 1 + +# list of datasources to insert/update depending +# whats available in the database +datasources: + # name of the datasource. Required + - name: Prometheus + # datasource type. Required + type: prometheus + # access mode. direct or proxy. Required + access: proxy + # org id. will default to orgId 1 if not specified + orgId: 1 + # url + # On MacOS, replace localhost by host.docker.internal + url: http://localhost:9090 + # database password, if used + password: + # database user, if used + user: + # database name, if used + database: + # enable/disable basic auth + basicAuth: false + # basic auth username + basicAuthUser: admin + # basic auth password + basicAuthPassword: admin + # enable/disable with credentials headers + withCredentials: + # mark as default datasource. Max one per org + isDefault: true + # fields that will be converted to json and stored in json_data + jsonData: + graphiteVersion: '1.1' + tlsAuth: false + tlsAuthWithCACert: false + # json object of data that will be encrypted. + secureJsonData: + tlsCACert: '...' + tlsClientCert: '...' + tlsClientKey: '...' + version: 1 + # allow users to edit datasources from the UI. + editable: true diff --git a/src/main/docker/jhipster-control-center.yml b/src/main/docker/jhipster-control-center.yml new file mode 100644 index 0000000..2b50df1 --- /dev/null +++ b/src/main/docker/jhipster-control-center.yml @@ -0,0 +1,48 @@ +## How to use JHCC docker compose +# To allow JHCC to reach JHipster application from a docker container note that we set the host as host.docker.internal +# To reach the application from a browser, you need to add '127.0.0.1 host.docker.internal' to your hosts file. +### Discovery mode +# JHCC support 3 kinds of discovery mode: Consul, Eureka and static +# In order to use one, please set SPRING_PROFILES_ACTIVE to one (and only one) of this values: consul,eureka,static +### Discovery properties +# According to the discovery mode choose as Spring profile, you have to set the right properties +# please note that current properties are set to run JHCC with default values, personalize them if needed +# and remove those from other modes. You can only have one mode active. +#### Eureka +# - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://admin:admin@host.docker.internal:8761/eureka/ +#### Consul +# - SPRING_CLOUD_CONSUL_HOST=host.docker.internal +# - SPRING_CLOUD_CONSUL_PORT=8500 +#### Static +# Add instances to "MyApp" +# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYAPP_0_URI=http://host.docker.internal:8081 +# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYAPP_1_URI=http://host.docker.internal:8082 +# Or add a new application named MyNewApp +# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYNEWAPP_0_URI=http://host.docker.internal:8080 +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production + +#### IMPORTANT +# If you choose Consul or Eureka mode: +# Do not forget to remove the prefix "127.0.0.1" in front of their port in order to expose them. +# This is required because JHCC need to communicate with Consul or Eureka. +# - In Consul mode, the ports are in the consul.yml file. +# - In Eureka mode, the ports are in the jhipster-registry.yml file. + +name: isdashboard +services: + jhipster-control-center: + image: 'jhipster/jhipster-control-center:v0.5.0' + command: + - /bin/sh + - -c + # Patch /etc/hosts to support resolving host.docker.internal to the internal IP address used by the host in all OSes + - echo "`ip route | grep default | cut -d ' ' -f3` host.docker.internal" | tee -a /etc/hosts > /dev/null && java -jar /jhipster-control-center.jar + environment: + - _JAVA_OPTIONS=-Xmx512m -Xms256m + - SPRING_PROFILES_ACTIVE=prod,api-docs,static + - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_ISDASHBOARD_0_URI=http://host.docker.internal:8080 + - LOGGING_FILE_NAME=/tmp/jhipster-control-center.log + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:7419:7419 diff --git a/src/main/docker/jib/entrypoint.sh b/src/main/docker/jib/entrypoint.sh new file mode 100644 index 0000000..11455a8 --- /dev/null +++ b/src/main/docker/jib/entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +echo "The application will start in ${JHIPSTER_SLEEP}s..." && sleep ${JHIPSTER_SLEEP} + +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [[ ${!var:-} && ${!fileVar:-} ]]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + local val="$def" + if [[ ${!var:-} ]]; then + val="${!var}" + elif [[ ${!fileVar:-} ]]; then + val="$(< "${!fileVar}")" + fi + + if [[ -n $val ]]; then + export "$var"="$val" + fi + + unset "$fileVar" +} + +file_env 'SPRING_DATASOURCE_URL' +file_env 'SPRING_DATASOURCE_USERNAME' +file_env 'SPRING_DATASOURCE_PASSWORD' +file_env 'SPRING_LIQUIBASE_URL' +file_env 'SPRING_LIQUIBASE_USER' +file_env 'SPRING_LIQUIBASE_PASSWORD' +file_env 'JHIPSTER_REGISTRY_PASSWORD' + +exec java ${JAVA_OPTS} -noverify -XX:+AlwaysPreTouch -Djava.security.egd=file:/dev/./urandom -cp /app/resources/:/app/classes/:/app/libs/* "org.gcube.isdashboard.IsdashboardApp" "$@" diff --git a/src/main/docker/monitoring.yml b/src/main/docker/monitoring.yml new file mode 100644 index 0000000..5ee7697 --- /dev/null +++ b/src/main/docker/monitoring.yml @@ -0,0 +1,31 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: isdashboard +services: + prometheus: + image: prom/prometheus:v2.45.0 + volumes: + - ./prometheus/:/etc/prometheus/ + command: + - '--config.file=/etc/prometheus/prometheus.yml' + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:9090:9090 + # On MacOS, remove next line and replace localhost by host.docker.internal in prometheus/prometheus.yml and + # grafana/provisioning/datasources/datasource.yml + network_mode: 'host' # to test locally running service + grafana: + image: grafana/grafana:10.0.2 + volumes: + - ./grafana/provisioning/:/etc/grafana/provisioning/ + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + - GF_INSTALL_PLUGINS=grafana-piechart-panel + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:3000:3000 + # On MacOS, remove next line and replace localhost by host.docker.internal in prometheus/prometheus.yml and + # grafana/provisioning/datasources/datasource.yml + network_mode: 'host' # to test locally running service diff --git a/src/main/docker/prometheus/prometheus.yml b/src/main/docker/prometheus/prometheus.yml new file mode 100644 index 0000000..b370a2f --- /dev/null +++ b/src/main/docker/prometheus/prometheus.yml @@ -0,0 +1,31 @@ +# Sample global config for monitoring JHipster applications +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + evaluation_interval: 15s # By default, scrape targets every 15 seconds. + # scrape_timeout is set to the global default (10s). + + # Attach these labels to any time series or alerts when communicating with + # external systems (federation, remote storage, Alertmanager). + external_labels: + monitor: 'jhipster' + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + + # Override the global default and scrape targets from this job every 5 seconds. + scrape_interval: 5s + + # scheme defaults to 'http' enable https in case your application is server via https + #scheme: https + # basic auth is not needed by default. See https://www.jhipster.tech/monitoring/#configuring-metrics-forwarding for details + #basic_auth: + # username: admin + # password: admin + metrics_path: /management/prometheus + static_configs: + - targets: + # On MacOS, replace localhost by host.docker.internal + - localhost:8080 diff --git a/src/main/docker/sonar.yml b/src/main/docker/sonar.yml new file mode 100644 index 0000000..72d18bc --- /dev/null +++ b/src/main/docker/sonar.yml @@ -0,0 +1,15 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: isdashboard +services: + sonar: + container_name: sonarqube + image: sonarqube:10.1.0-community + # Forced authentication redirect for UI is turned off for out of the box experience while trying out SonarQube + # For real use cases delete SONAR_FORCEAUTHENTICATION variable or set SONAR_FORCEAUTHENTICATION=true + environment: + - SONAR_FORCEAUTHENTICATION=false + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:9001:9000 + - 127.0.0.1:9000:9000 diff --git a/src/main/java/org/gcube/isdashboard/ApplicationWebXml.java b/src/main/java/org/gcube/isdashboard/ApplicationWebXml.java new file mode 100644 index 0000000..ffaeef0 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/ApplicationWebXml.java @@ -0,0 +1,19 @@ +package org.gcube.isdashboard; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import tech.jhipster.config.DefaultProfileUtil; + +/** + * This is a helper Java class that provides an alternative to creating a {@code web.xml}. + * This will be invoked only when the application is deployed to a Servlet container like Tomcat, JBoss etc. + */ +public class ApplicationWebXml extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + // set a default to use when no profile is configured. + DefaultProfileUtil.addDefaultProfile(application.application()); + return application.sources(IsdashboardApp.class); + } +} diff --git a/src/main/java/org/gcube/isdashboard/GeneratedByJHipster.java b/src/main/java/org/gcube/isdashboard/GeneratedByJHipster.java new file mode 100644 index 0000000..9fff425 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/GeneratedByJHipster.java @@ -0,0 +1,13 @@ +package org.gcube.isdashboard; + +import jakarta.annotation.Generated; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Generated(value = "JHipster", comments = "Generated by JHipster 8.0.0-beta.2") +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.TYPE }) +public @interface GeneratedByJHipster { +} diff --git a/src/main/java/org/gcube/isdashboard/IsdashboardApp.java b/src/main/java/org/gcube/isdashboard/IsdashboardApp.java new file mode 100644 index 0000000..02cb944 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/IsdashboardApp.java @@ -0,0 +1,107 @@ +package org.gcube.isdashboard; + +import jakarta.annotation.PostConstruct; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.gcube.isdashboard.config.ApplicationProperties; +import org.gcube.isdashboard.config.CRLFLogConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.core.env.Environment; +import tech.jhipster.config.DefaultProfileUtil; +import tech.jhipster.config.JHipsterConstants; + +@SpringBootApplication +@EnableConfigurationProperties({ ApplicationProperties.class }) +public class IsdashboardApp { + + private static final Logger log = LoggerFactory.getLogger(IsdashboardApp.class); + + private final Environment env; + + public IsdashboardApp(Environment env) { + this.env = env; + } + + /** + * Initializes isdashboard. + *

+ * Spring profiles can be configured with a program argument --spring.profiles.active=your-active-profile + *

+ * You can find more information on how profiles work with JHipster on https://www.jhipster.tech/profiles/. + */ + @PostConstruct + public void initApplication() { + Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); + if ( + activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) && + activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION) + ) { + log.error( + "You have misconfigured your application! It should not run " + "with both the 'dev' and 'prod' profiles at the same time." + ); + } + if ( + activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) && + activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_CLOUD) + ) { + log.error( + "You have misconfigured your application! It should not " + "run with both the 'dev' and 'cloud' profiles at the same time." + ); + } + } + + /** + * Main method, used to run the application. + * + * @param args the command line arguments. + */ + public static void main(String[] args) { + SpringApplication app = new SpringApplication(IsdashboardApp.class); + DefaultProfileUtil.addDefaultProfile(app); + Environment env = app.run(args).getEnvironment(); + logApplicationStartup(env); + } + + private static void logApplicationStartup(Environment env) { + String protocol = Optional.ofNullable(env.getProperty("server.ssl.key-store")).map(key -> "https").orElse("http"); + String serverPort = env.getProperty("server.port"); + String contextPath = Optional + .ofNullable(env.getProperty("server.servlet.context-path")) + .filter(StringUtils::isNotBlank) + .orElse("/"); + String hostAddress = "localhost"; + try { + hostAddress = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.warn("The host name could not be determined, using `localhost` as fallback"); + } + log.info( + CRLFLogConverter.CRLF_SAFE_MARKER, + """ + + ---------------------------------------------------------- + \tApplication '{}' is running! Access URLs: + \tLocal: \t\t{}://localhost:{}{} + \tExternal: \t{}://{}:{}{} + \tProfile(s): \t{} + ----------------------------------------------------------""", + env.getProperty("spring.application.name"), + protocol, + serverPort, + contextPath, + protocol, + hostAddress, + serverPort, + contextPath, + env.getActiveProfiles().length == 0 ? env.getDefaultProfiles() : env.getActiveProfiles() + ); + } +} diff --git a/src/main/java/org/gcube/isdashboard/aop/logging/LoggingAspect.java b/src/main/java/org/gcube/isdashboard/aop/logging/LoggingAspect.java new file mode 100644 index 0000000..c33be78 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/aop/logging/LoggingAspect.java @@ -0,0 +1,115 @@ +package org.gcube.isdashboard.aop.logging; + +import java.util.Arrays; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; +import tech.jhipster.config.JHipsterConstants; + +/** + * Aspect for logging execution of service and repository Spring components. + * + * By default, it only runs with the "dev" profile. + */ +@Aspect +public class LoggingAspect { + + private final Environment env; + + public LoggingAspect(Environment env) { + this.env = env; + } + + /** + * Pointcut that matches all repositories, services and Web REST endpoints. + */ + @Pointcut( + "within(@org.springframework.stereotype.Repository *)" + + " || within(@org.springframework.stereotype.Service *)" + + " || within(@org.springframework.web.bind.annotation.RestController *)" + ) + public void springBeanPointcut() { + // Method is empty as this is just a Pointcut, the implementations are in the advices. + } + + /** + * Pointcut that matches all Spring beans in the application's main packages. + */ + @Pointcut( + "within(org.gcube.isdashboard.repository..*)" + + " || within(org.gcube.isdashboard.service..*)" + + " || within(org.gcube.isdashboard.web.rest..*)" + ) + public void applicationPackagePointcut() { + // Method is empty as this is just a Pointcut, the implementations are in the advices. + } + + /** + * Retrieves the {@link Logger} associated to the given {@link JoinPoint}. + * + * @param joinPoint join point we want the logger for. + * @return {@link Logger} associated to the given {@link JoinPoint}. + */ + private Logger logger(JoinPoint joinPoint) { + return LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringTypeName()); + } + + /** + * Advice that logs methods throwing exceptions. + * + * @param joinPoint join point for advice. + * @param e exception. + */ + @AfterThrowing(pointcut = "applicationPackagePointcut() && springBeanPointcut()", throwing = "e") + public void logAfterThrowing(JoinPoint joinPoint, Throwable e) { + if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) { + logger(joinPoint) + .error( + "Exception in {}() with cause = '{}' and exception = '{}'", + joinPoint.getSignature().getName(), + e.getCause() != null ? e.getCause() : "NULL", + e.getMessage(), + e + ); + } else { + logger(joinPoint) + .error( + "Exception in {}() with cause = {}", + joinPoint.getSignature().getName(), + e.getCause() != null ? e.getCause() : "NULL" + ); + } + } + + /** + * Advice that logs when a method is entered and exited. + * + * @param joinPoint join point for advice. + * @return result. + * @throws Throwable throws {@link IllegalArgumentException}. + */ + @Around("applicationPackagePointcut() && springBeanPointcut()") + public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { + Logger log = logger(joinPoint); + if (log.isDebugEnabled()) { + log.debug("Enter: {}() with argument[s] = {}", joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs())); + } + try { + Object result = joinPoint.proceed(); + if (log.isDebugEnabled()) { + log.debug("Exit: {}() with result = {}", joinPoint.getSignature().getName(), result); + } + return result; + } catch (IllegalArgumentException e) { + log.error("Illegal argument: {} in {}()", Arrays.toString(joinPoint.getArgs()), joinPoint.getSignature().getName()); + throw e; + } + } +} diff --git a/src/main/java/org/gcube/isdashboard/aop/logging/package-info.java b/src/main/java/org/gcube/isdashboard/aop/logging/package-info.java new file mode 100644 index 0000000..513278a --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/aop/logging/package-info.java @@ -0,0 +1,4 @@ +/** + * Logging aspect. + */ +package org.gcube.isdashboard.aop.logging; diff --git a/src/main/java/org/gcube/isdashboard/config/ApplicationProperties.java b/src/main/java/org/gcube/isdashboard/config/ApplicationProperties.java new file mode 100644 index 0000000..f491ca3 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/ApplicationProperties.java @@ -0,0 +1,16 @@ +package org.gcube.isdashboard.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties specific to Isdashboard. + *

+ * Properties are configured in the {@code application.yml} file. + * See {@link tech.jhipster.config.JHipsterProperties} for a good example. + */ +@ConfigurationProperties(prefix = "application", ignoreUnknownFields = false) +public class ApplicationProperties { + // jhipster-needle-application-properties-property + // jhipster-needle-application-properties-property-getter + // jhipster-needle-application-properties-property-class +} diff --git a/src/main/java/org/gcube/isdashboard/config/AsyncConfiguration.java b/src/main/java/org/gcube/isdashboard/config/AsyncConfiguration.java new file mode 100644 index 0000000..de7908c --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/AsyncConfiguration.java @@ -0,0 +1,48 @@ +package org.gcube.isdashboard.config; + +import java.util.concurrent.Executor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; +import org.springframework.boot.autoconfigure.task.TaskExecutionProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import tech.jhipster.async.ExceptionHandlingAsyncTaskExecutor; + +@Configuration +@EnableAsync +@EnableScheduling +@Profile("!testdev & !testprod") +public class AsyncConfiguration implements AsyncConfigurer { + + private final Logger log = LoggerFactory.getLogger(AsyncConfiguration.class); + + private final TaskExecutionProperties taskExecutionProperties; + + public AsyncConfiguration(TaskExecutionProperties taskExecutionProperties) { + this.taskExecutionProperties = taskExecutionProperties; + } + + @Override + @Bean(name = "taskExecutor") + public Executor getAsyncExecutor() { + log.debug("Creating Async Task Executor"); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(taskExecutionProperties.getPool().getCoreSize()); + executor.setMaxPoolSize(taskExecutionProperties.getPool().getMaxSize()); + executor.setQueueCapacity(taskExecutionProperties.getPool().getQueueCapacity()); + executor.setThreadNamePrefix(taskExecutionProperties.getThreadNamePrefix()); + return new ExceptionHandlingAsyncTaskExecutor(executor); + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new SimpleAsyncUncaughtExceptionHandler(); + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/CRLFLogConverter.java b/src/main/java/org/gcube/isdashboard/config/CRLFLogConverter.java new file mode 100644 index 0000000..77c65ff --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/CRLFLogConverter.java @@ -0,0 +1,67 @@ +package org.gcube.isdashboard.config; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.CompositeConverter; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; +import org.springframework.boot.ansi.AnsiColor; +import org.springframework.boot.ansi.AnsiElement; +import org.springframework.boot.ansi.AnsiOutput; +import org.springframework.boot.ansi.AnsiStyle; + +/** + * Log filter to prevent attackers from forging log entries by submitting input containing CRLF characters. + * CRLF characters are replaced with a red colored _ character. + * + * @see Log Forging Description + * @see JHipster issue + */ +public class CRLFLogConverter extends CompositeConverter { + + public static final Marker CRLF_SAFE_MARKER = MarkerFactory.getMarker("CRLF_SAFE"); + + private static final String[] SAFE_LOGGERS = { + "org.hibernate", + "org.springframework.boot.autoconfigure", + "org.springframework.boot.diagnostics", + }; + private static final Map ELEMENTS; + + static { + Map ansiElements = new HashMap<>(); + ansiElements.put("faint", AnsiStyle.FAINT); + ansiElements.put("red", AnsiColor.RED); + ansiElements.put("green", AnsiColor.GREEN); + ansiElements.put("yellow", AnsiColor.YELLOW); + ansiElements.put("blue", AnsiColor.BLUE); + ansiElements.put("magenta", AnsiColor.MAGENTA); + ansiElements.put("cyan", AnsiColor.CYAN); + ELEMENTS = Collections.unmodifiableMap(ansiElements); + } + + @Override + protected String transform(ILoggingEvent event, String in) { + AnsiElement element = ELEMENTS.get(getFirstOption()); + if ((event.getMarker() != null && event.getMarker().contains(CRLF_SAFE_MARKER)) || isLoggerSafe(event)) { + return in; + } + String replacement = element == null ? "_" : toAnsiString("_", element); + return in.replaceAll("[\n\r\t]", replacement); + } + + protected boolean isLoggerSafe(ILoggingEvent event) { + for (String safeLogger : SAFE_LOGGERS) { + if (event.getLoggerName().startsWith(safeLogger)) { + return true; + } + } + return false; + } + + protected String toAnsiString(String in, AnsiElement element) { + return AnsiOutput.toString(element, in); + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/CacheConfiguration.java b/src/main/java/org/gcube/isdashboard/config/CacheConfiguration.java new file mode 100644 index 0000000..28d55f4 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/CacheConfiguration.java @@ -0,0 +1,66 @@ +package org.gcube.isdashboard.config; + +import java.time.Duration; +import org.ehcache.config.builders.*; +import org.ehcache.jsr107.Eh107Configuration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.info.GitProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.context.annotation.*; +import tech.jhipster.config.JHipsterProperties; +import tech.jhipster.config.cache.PrefixedKeyGenerator; + +@Configuration +@EnableCaching +public class CacheConfiguration { + + private GitProperties gitProperties; + private BuildProperties buildProperties; + private final javax.cache.configuration.Configuration jcacheConfiguration; + + public CacheConfiguration(JHipsterProperties jHipsterProperties) { + JHipsterProperties.Cache.Ehcache ehcache = jHipsterProperties.getCache().getEhcache(); + + jcacheConfiguration = + Eh107Configuration.fromEhcacheCacheConfiguration( + CacheConfigurationBuilder + .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(ehcache.getMaxEntries())) + .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(ehcache.getTimeToLiveSeconds()))) + .build() + ); + } + + @Bean + public JCacheManagerCustomizer cacheManagerCustomizer() { + return cm -> { + // jhipster-needle-ehcache-add-entry + }; + } + + private void createCache(javax.cache.CacheManager cm, String cacheName) { + javax.cache.Cache cache = cm.getCache(cacheName); + if (cache != null) { + cache.clear(); + } else { + cm.createCache(cacheName, jcacheConfiguration); + } + } + + @Autowired(required = false) + public void setGitProperties(GitProperties gitProperties) { + this.gitProperties = gitProperties; + } + + @Autowired(required = false) + public void setBuildProperties(BuildProperties buildProperties) { + this.buildProperties = buildProperties; + } + + @Bean + public KeyGenerator keyGenerator() { + return new PrefixedKeyGenerator(this.gitProperties, this.buildProperties); + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/DateTimeFormatConfiguration.java b/src/main/java/org/gcube/isdashboard/config/DateTimeFormatConfiguration.java new file mode 100644 index 0000000..35187f5 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/DateTimeFormatConfiguration.java @@ -0,0 +1,20 @@ +package org.gcube.isdashboard.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Configure the converters to use the ISO format for dates by default. + */ +@Configuration +public class DateTimeFormatConfiguration implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + registrar.registerFormatters(registry); + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/JacksonConfiguration.java b/src/main/java/org/gcube/isdashboard/config/JacksonConfiguration.java new file mode 100644 index 0000000..4331378 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/JacksonConfiguration.java @@ -0,0 +1,24 @@ +package org.gcube.isdashboard.config; + +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfiguration { + + /** + * Support for Java date and time API. + * @return the corresponding Jackson module. + */ + @Bean + public JavaTimeModule javaTimeModule() { + return new JavaTimeModule(); + } + + @Bean + public Jdk8Module jdk8TimeModule() { + return new Jdk8Module(); + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/LocaleConfiguration.java b/src/main/java/org/gcube/isdashboard/config/LocaleConfiguration.java new file mode 100644 index 0000000..1a03688 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/LocaleConfiguration.java @@ -0,0 +1,26 @@ +package org.gcube.isdashboard.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.*; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import tech.jhipster.config.locale.AngularCookieLocaleResolver; + +@Configuration +public class LocaleConfiguration implements WebMvcConfigurer { + + @Bean + public LocaleResolver localeResolver() { + AngularCookieLocaleResolver cookieLocaleResolver = new AngularCookieLocaleResolver(); + cookieLocaleResolver.setCookieName("NG_TRANSLATE_LANG_KEY"); + return cookieLocaleResolver; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); + localeChangeInterceptor.setParamName("language"); + registry.addInterceptor(localeChangeInterceptor); + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/LoggingAspectConfiguration.java b/src/main/java/org/gcube/isdashboard/config/LoggingAspectConfiguration.java new file mode 100644 index 0000000..13e945e --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/LoggingAspectConfiguration.java @@ -0,0 +1,17 @@ +package org.gcube.isdashboard.config; + +import org.gcube.isdashboard.aop.logging.LoggingAspect; +import org.springframework.context.annotation.*; +import org.springframework.core.env.Environment; +import tech.jhipster.config.JHipsterConstants; + +@Configuration +@EnableAspectJAutoProxy +public class LoggingAspectConfiguration { + + @Bean + @Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) + public LoggingAspect loggingAspect(Environment env) { + return new LoggingAspect(env); + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/LoggingConfiguration.java b/src/main/java/org/gcube/isdashboard/config/LoggingConfiguration.java new file mode 100644 index 0000000..b678490 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/LoggingConfiguration.java @@ -0,0 +1,47 @@ +package org.gcube.isdashboard.config; + +import static tech.jhipster.config.logging.LoggingUtils.*; + +import ch.qos.logback.classic.LoggerContext; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import tech.jhipster.config.JHipsterProperties; + +/* + * Configures the console and Logstash log appenders from the app properties + */ +@Configuration +public class LoggingConfiguration { + + public LoggingConfiguration( + @Value("${spring.application.name}") String appName, + @Value("${server.port}") String serverPort, + JHipsterProperties jHipsterProperties, + ObjectMapper mapper + ) throws JsonProcessingException { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + + Map map = new HashMap<>(); + map.put("app_name", appName); + map.put("app_port", serverPort); + String customFields = mapper.writeValueAsString(map); + + JHipsterProperties.Logging loggingProperties = jHipsterProperties.getLogging(); + JHipsterProperties.Logging.Logstash logstashProperties = loggingProperties.getLogstash(); + + if (loggingProperties.isUseJsonFormat()) { + addJsonConsoleAppender(context, customFields); + } + if (logstashProperties.isEnabled()) { + addLogstashTcpSocketAppender(context, customFields, logstashProperties); + } + if (loggingProperties.isUseJsonFormat() || logstashProperties.isEnabled()) { + addContextListener(context, customFields, loggingProperties); + } + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/SecurityConfiguration.java b/src/main/java/org/gcube/isdashboard/config/SecurityConfiguration.java new file mode 100644 index 0000000..45af0af --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/SecurityConfiguration.java @@ -0,0 +1,95 @@ +package org.gcube.isdashboard.config; + +import static org.springframework.security.config.Customizer.withDefaults; + +import org.gcube.isdashboard.security.*; +import org.gcube.isdashboard.web.filter.SpaWebFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import tech.jhipster.config.JHipsterProperties; +import tech.jhipster.web.filter.CookieCsrfFilter; + +@Configuration +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfiguration { + + private final JHipsterProperties jHipsterProperties; + + public SecurityConfiguration(JHipsterProperties jHipsterProperties) { + this.jHipsterProperties = jHipsterProperties; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(withDefaults()) + .csrf(csrf -> + csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + // See https://stackoverflow.com/q/74447118/65681 + .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) + ) + .addFilterAfter(new SpaWebFilter(), BasicAuthenticationFilter.class) + .addFilterAfter(new CookieCsrfFilter(), BasicAuthenticationFilter.class) + .headers(headers -> + headers + .contentSecurityPolicy(csp -> csp.policyDirectives(jHipsterProperties.getSecurity().getContentSecurityPolicy())) + .frameOptions(FrameOptionsConfig::sameOrigin) + .referrerPolicy(referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)) + .permissionsPolicy(permissions -> + permissions.policy( + "camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()" + ) + ) + ) + .authorizeHttpRequests(authz -> + // prettier-ignore + authz + .requestMatchers("/", "/index.html", "/*.js", "/*.map", "/*.css").permitAll() + .requestMatchers("/*.ico", "/*.png", "/*.svg", "/*.webapp").permitAll() + .requestMatchers("/app/**").permitAll() + .requestMatchers("/i18n/**").permitAll() + .requestMatchers("/content/**").permitAll() + .requestMatchers("/swagger-ui/**").permitAll() + .requestMatchers("/api/authenticate").permitAll() + .requestMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN) + .requestMatchers("/api/**").authenticated() + .requestMatchers("/v3/api-docs/**").hasAuthority(AuthoritiesConstants.ADMIN) + .requestMatchers("/management/health").permitAll() + .requestMatchers("/management/health/**").permitAll() + .requestMatchers("/management/info").permitAll() + .requestMatchers("/management/prometheus").permitAll() + .requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) + ) + .exceptionHandling(exceptionHanding -> + exceptionHanding.defaultAuthenticationEntryPointFor( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + new OrRequestMatcher(new AntPathRequestMatcher("/api/**")) + ) + ) + .formLogin(formLogin -> + formLogin + .loginProcessingUrl("/api/authentication") + .successHandler((request, response, authentication) -> response.setStatus(HttpStatus.OK.value())) + .failureHandler((request, response, exception) -> response.setStatus(HttpStatus.UNAUTHORIZED.value())) + .permitAll() + ) + .logout(logout -> + logout.logoutUrl("/api/logout").logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()).permitAll() + ); + return http.build(); + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/StaticResourcesWebConfiguration.java b/src/main/java/org/gcube/isdashboard/config/StaticResourcesWebConfiguration.java new file mode 100644 index 0000000..14bd799 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/StaticResourcesWebConfiguration.java @@ -0,0 +1,59 @@ +package org.gcube.isdashboard.config; + +import java.util.concurrent.TimeUnit; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.JHipsterProperties; + +@Configuration +@Profile({ JHipsterConstants.SPRING_PROFILE_PRODUCTION }) +public class StaticResourcesWebConfiguration implements WebMvcConfigurer { + + protected static final String[] RESOURCE_LOCATIONS = new String[] { + "classpath:/static/", + "classpath:/static/content/", + "classpath:/static/i18n/", + }; + protected static final String[] RESOURCE_PATHS = new String[] { + "/*.js", + "/*.css", + "/*.svg", + "/*.png", + "*.ico", + "/content/**", + "/i18n/*", + }; + + private final JHipsterProperties jhipsterProperties; + + public StaticResourcesWebConfiguration(JHipsterProperties jHipsterProperties) { + this.jhipsterProperties = jHipsterProperties; + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + ResourceHandlerRegistration resourceHandlerRegistration = appendResourceHandler(registry); + initializeResourceHandler(resourceHandlerRegistration); + } + + protected ResourceHandlerRegistration appendResourceHandler(ResourceHandlerRegistry registry) { + return registry.addResourceHandler(RESOURCE_PATHS); + } + + protected void initializeResourceHandler(ResourceHandlerRegistration resourceHandlerRegistration) { + resourceHandlerRegistration.addResourceLocations(RESOURCE_LOCATIONS).setCacheControl(getCacheControl()); + } + + protected CacheControl getCacheControl() { + return CacheControl.maxAge(getJHipsterHttpCacheProperty(), TimeUnit.DAYS).cachePublic(); + } + + private int getJHipsterHttpCacheProperty() { + return jhipsterProperties.getHttp().getCache().getTimeToLiveInDays(); + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/WebConfigurer.java b/src/main/java/org/gcube/isdashboard/config/WebConfigurer.java new file mode 100644 index 0000000..02df4cf --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/WebConfigurer.java @@ -0,0 +1,98 @@ +package org.gcube.isdashboard.config; + +import static java.net.URLDecoder.decode; + +import jakarta.servlet.*; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.server.*; +import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; +import org.springframework.util.CollectionUtils; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.JHipsterProperties; + +/** + * Configuration of web application with Servlet 3.0 APIs. + */ +@Configuration +public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer { + + private final Logger log = LoggerFactory.getLogger(WebConfigurer.class); + + private final Environment env; + + private final JHipsterProperties jHipsterProperties; + + public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties) { + this.env = env; + this.jHipsterProperties = jHipsterProperties; + } + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + if (env.getActiveProfiles().length != 0) { + log.info("Web application configuration, using profiles: {}", (Object[]) env.getActiveProfiles()); + } + + log.info("Web application fully configured"); + } + + /** + * Customize the Servlet engine: Mime types, the document root, the cache. + */ + @Override + public void customize(WebServerFactory server) { + // When running in an IDE or with ./mvnw spring-boot:run, set location of the static web assets. + setLocationForStaticAssets(server); + } + + private void setLocationForStaticAssets(WebServerFactory server) { + if (server instanceof ConfigurableServletWebServerFactory servletWebServer) { + File root; + String prefixPath = resolvePathPrefix(); + root = new File(prefixPath + "target/classes/static/"); + if (root.exists() && root.isDirectory()) { + servletWebServer.setDocumentRoot(root); + } + } + } + + /** + * Resolve path prefix to static resources. + */ + private String resolvePathPrefix() { + String fullExecutablePath = decode(this.getClass().getResource("").getPath(), StandardCharsets.UTF_8); + String rootPath = Paths.get(".").toUri().normalize().getPath(); + String extractedPath = fullExecutablePath.replace(rootPath, ""); + int extractionEndIndex = extractedPath.indexOf("target/"); + if (extractionEndIndex <= 0) { + return ""; + } + return extractedPath.substring(0, extractionEndIndex); + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = jHipsterProperties.getCors(); + if (!CollectionUtils.isEmpty(config.getAllowedOrigins()) || !CollectionUtils.isEmpty(config.getAllowedOriginPatterns())) { + log.debug("Registering CORS filter"); + source.registerCorsConfiguration("/api/**", config); + source.registerCorsConfiguration("/management/**", config); + source.registerCorsConfiguration("/v3/api-docs", config); + source.registerCorsConfiguration("/swagger-ui/**", config); + } + return new CorsFilter(source); + } +} diff --git a/src/main/java/org/gcube/isdashboard/config/package-info.java b/src/main/java/org/gcube/isdashboard/config/package-info.java new file mode 100644 index 0000000..5fb8d00 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/config/package-info.java @@ -0,0 +1,4 @@ +/** + * Application configuration. + */ +package org.gcube.isdashboard.config; diff --git a/src/main/java/org/gcube/isdashboard/package-info.java b/src/main/java/org/gcube/isdashboard/package-info.java new file mode 100644 index 0000000..620005d --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/package-info.java @@ -0,0 +1,4 @@ +/** + * Application root. + */ +package org.gcube.isdashboard; diff --git a/src/main/java/org/gcube/isdashboard/security/AuthoritiesConstants.java b/src/main/java/org/gcube/isdashboard/security/AuthoritiesConstants.java new file mode 100644 index 0000000..53776cc --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/security/AuthoritiesConstants.java @@ -0,0 +1,15 @@ +package org.gcube.isdashboard.security; + +/** + * Constants for Spring Security authorities. + */ +public final class AuthoritiesConstants { + + public static final String ADMIN = "ROLE_ADMIN"; + + public static final String USER = "ROLE_USER"; + + public static final String ANONYMOUS = "ROLE_ANONYMOUS"; + + private AuthoritiesConstants() {} +} diff --git a/src/main/java/org/gcube/isdashboard/security/SecurityUtils.java b/src/main/java/org/gcube/isdashboard/security/SecurityUtils.java new file mode 100644 index 0000000..927142c --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/security/SecurityUtils.java @@ -0,0 +1,86 @@ +package org.gcube.isdashboard.security; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Stream; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * Utility class for Spring Security. + */ +public final class SecurityUtils { + + private SecurityUtils() {} + + /** + * Get the login of the current user. + * + * @return the login of the current user. + */ + public static Optional getCurrentUserLogin() { + SecurityContext securityContext = SecurityContextHolder.getContext(); + return Optional.ofNullable(extractPrincipal(securityContext.getAuthentication())); + } + + private static String extractPrincipal(Authentication authentication) { + if (authentication == null) { + return null; + } else if (authentication.getPrincipal() instanceof UserDetails springSecurityUser) { + return springSecurityUser.getUsername(); + } else if (authentication.getPrincipal() instanceof String s) { + return s; + } + return null; + } + + /** + * Check if a user is authenticated. + * + * @return true if the user is authenticated, false otherwise. + */ + public static boolean isAuthenticated() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null && getAuthorities(authentication).noneMatch(AuthoritiesConstants.ANONYMOUS::equals); + } + + /** + * Checks if the current user has any of the authorities. + * + * @param authorities the authorities to check. + * @return true if the current user has any of the authorities, false otherwise. + */ + public static boolean hasCurrentUserAnyOfAuthorities(String... authorities) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return ( + authentication != null && getAuthorities(authentication).anyMatch(authority -> Arrays.asList(authorities).contains(authority)) + ); + } + + /** + * Checks if the current user has none of the authorities. + * + * @param authorities the authorities to check. + * @return true if the current user has none of the authorities, false otherwise. + */ + public static boolean hasCurrentUserNoneOfAuthorities(String... authorities) { + return !hasCurrentUserAnyOfAuthorities(authorities); + } + + /** + * Checks if the current user has a specific authority. + * + * @param authority the authority to check. + * @return true if the current user has the authority, false otherwise. + */ + public static boolean hasCurrentUserThisAuthority(String authority) { + return hasCurrentUserAnyOfAuthorities(authority); + } + + private static Stream getAuthorities(Authentication authentication) { + return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority); + } +} diff --git a/src/main/java/org/gcube/isdashboard/security/package-info.java b/src/main/java/org/gcube/isdashboard/security/package-info.java new file mode 100644 index 0000000..ad9a552 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/security/package-info.java @@ -0,0 +1,4 @@ +/** + * Application security utilities. + */ +package org.gcube.isdashboard.security; diff --git a/src/main/java/org/gcube/isdashboard/web/filter/SpaWebFilter.java b/src/main/java/org/gcube/isdashboard/web/filter/SpaWebFilter.java new file mode 100644 index 0000000..494cf83 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/web/filter/SpaWebFilter.java @@ -0,0 +1,32 @@ +package org.gcube.isdashboard.web.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.web.filter.OncePerRequestFilter; + +public class SpaWebFilter extends OncePerRequestFilter { + + /** + * Forwards any unmapped paths (except those containing a period) to the client {@code index.html}. + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String path = request.getRequestURI(); + if ( + !path.startsWith("/api") && + !path.startsWith("/management") && + !path.startsWith("/v3/api-docs") && + !path.contains(".") && + path.matches("/(.*)") + ) { + request.getRequestDispatcher("/index.html").forward(request, response); + return; + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/org/gcube/isdashboard/web/filter/package-info.java b/src/main/java/org/gcube/isdashboard/web/filter/package-info.java new file mode 100644 index 0000000..ab6ad30 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/web/filter/package-info.java @@ -0,0 +1,4 @@ +/** + * Request chain filters. + */ +package org.gcube.isdashboard.web.filter; diff --git a/src/main/java/org/gcube/isdashboard/web/rest/AccountResource.java b/src/main/java/org/gcube/isdashboard/web/rest/AccountResource.java new file mode 100644 index 0000000..9cf6290 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/web/rest/AccountResource.java @@ -0,0 +1,78 @@ +package org.gcube.isdashboard.web.rest; + +import com.fasterxml.jackson.annotation.JsonCreator; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Set; +import java.util.stream.Collectors; +import org.gcube.isdashboard.security.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class AccountResource { + + private final Logger log = LoggerFactory.getLogger(AccountResource.class); + + private static class AccountResourceException extends RuntimeException {} + + /** + * {@code GET /account} : get the current user. + * + * @return the current user. + * @throws AccountResourceException {@code 500 (Internal Server Error)} if the user couldn't be returned. + */ + @GetMapping("/account") + public UserVM getAccount() { + String login = SecurityUtils.getCurrentUserLogin().orElseThrow(AccountResourceException::new); + Set authorities = SecurityContextHolder + .getContext() + .getAuthentication() + .getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new UserVM(login, authorities); + } + + /** + * {@code GET /authenticate} : check if the user is authenticated, and return its login. + * + * @param request the HTTP request. + * @return the login if the user is authenticated. + */ + @GetMapping("/authenticate") + public String isAuthenticated(HttpServletRequest request) { + log.debug("REST request to check if the current user is authenticated"); + return request.getRemoteUser(); + } + + private static class UserVM { + + private String login; + private Set authorities; + + @JsonCreator + UserVM(String login, Set authorities) { + this.login = login; + this.authorities = authorities; + } + + public boolean isActivated() { + return true; + } + + public Set getAuthorities() { + return authorities; + } + + public String getLogin() { + return login; + } + } +} diff --git a/src/main/java/org/gcube/isdashboard/web/rest/errors/BadRequestAlertException.java b/src/main/java/org/gcube/isdashboard/web/rest/errors/BadRequestAlertException.java new file mode 100644 index 0000000..e9dd499 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/web/rest/errors/BadRequestAlertException.java @@ -0,0 +1,50 @@ +package org.gcube.isdashboard.web.rest.errors; + +import java.net.URI; +import org.springframework.http.HttpStatus; +import org.springframework.web.ErrorResponseException; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder; + +@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep +public class BadRequestAlertException extends ErrorResponseException { + + private static final long serialVersionUID = 1L; + + private final String entityName; + + private final String errorKey; + + public BadRequestAlertException(String defaultMessage, String entityName, String errorKey) { + this(ErrorConstants.DEFAULT_TYPE, defaultMessage, entityName, errorKey); + } + + public BadRequestAlertException(URI type, String defaultMessage, String entityName, String errorKey) { + super( + HttpStatus.BAD_REQUEST, + ProblemDetailWithCauseBuilder + .instance() + .withStatus(HttpStatus.BAD_REQUEST.value()) + .withType(type) + .withTitle(defaultMessage) + .withProperty("message", "error." + errorKey) + .withProperty("params", entityName) + .build(), + null + ); + this.entityName = entityName; + this.errorKey = errorKey; + } + + public String getEntityName() { + return entityName; + } + + public String getErrorKey() { + return errorKey; + } + + public ProblemDetailWithCause getProblemDetailWithCause() { + return (ProblemDetailWithCause) this.getBody(); + } +} diff --git a/src/main/java/org/gcube/isdashboard/web/rest/errors/ErrorConstants.java b/src/main/java/org/gcube/isdashboard/web/rest/errors/ErrorConstants.java new file mode 100644 index 0000000..d098a91 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/web/rest/errors/ErrorConstants.java @@ -0,0 +1,13 @@ +package org.gcube.isdashboard.web.rest.errors; + +import java.net.URI; + +public final class ErrorConstants { + + public static final String ERR_VALIDATION = "error.validation"; + public static final String PROBLEM_BASE_URL = "https://www.jhipster.tech/problem"; + public static final URI DEFAULT_TYPE = URI.create(PROBLEM_BASE_URL + "/problem-with-message"); + public static final URI CONSTRAINT_VIOLATION_TYPE = URI.create(PROBLEM_BASE_URL + "/constraint-violation"); + + private ErrorConstants() {} +} diff --git a/src/main/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslator.java b/src/main/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslator.java new file mode 100644 index 0000000..b5deac0 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslator.java @@ -0,0 +1,253 @@ +package org.gcube.isdashboard.web.rest.errors; + +import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.lang.Nullable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.ErrorResponse; +import org.springframework.web.ErrorResponseException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder; +import tech.jhipster.web.util.HeaderUtil; + +/** + * Controller advice to translate the server side exceptions to client-friendly json structures. + * The error response follows RFC7807 - Problem Details for HTTP APIs (https://tools.ietf.org/html/rfc7807). + */ +@ControllerAdvice +public class ExceptionTranslator extends ResponseEntityExceptionHandler { + + private static final String FIELD_ERRORS_KEY = "fieldErrors"; + private static final String MESSAGE_KEY = "message"; + private static final String PATH_KEY = "path"; + private static final boolean CASUAL_CHAIN_ENABLED = false; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final Environment env; + + public ExceptionTranslator(Environment env) { + this.env = env; + } + + @ExceptionHandler + public ResponseEntity handleAnyException(Throwable ex, NativeWebRequest request) { + ProblemDetailWithCause pdCause = wrapAndCustomizeProblem(ex, request); + return handleExceptionInternal( + (Exception) ex, + pdCause, + buildHeaders(ex, request), + HttpStatusCode.valueOf(pdCause.getStatus()), + request + ); + } + + @Nullable + @Override + protected ResponseEntity handleExceptionInternal( + Exception ex, + @Nullable Object body, + HttpHeaders headers, + HttpStatusCode statusCode, + WebRequest request + ) { + body = body == null ? wrapAndCustomizeProblem((Throwable) ex, (NativeWebRequest) request) : body; + return super.handleExceptionInternal(ex, body, headers, statusCode, request); + } + + protected ProblemDetailWithCause wrapAndCustomizeProblem(Throwable ex, NativeWebRequest request) { + return customizeProblem(getProblemDetailWithCause(ex), ex, request); + } + + private ProblemDetailWithCause getProblemDetailWithCause(Throwable ex) { + if ( + ex instanceof ErrorResponseException exp && exp.getBody() instanceof ProblemDetailWithCause + ) return (ProblemDetailWithCause) exp.getBody(); + return ProblemDetailWithCauseBuilder.instance().withStatus(toStatus(ex).value()).build(); + } + + protected ProblemDetailWithCause customizeProblem(ProblemDetailWithCause problem, Throwable err, NativeWebRequest request) { + if (problem.getStatus() <= 0) problem.setStatus(toStatus(err)); + + if (problem.getType() == null || problem.getType().equals(URI.create("about:blank"))) problem.setType(getMappedType(err)); + + // higher precedence to Custom/ResponseStatus types + String title = extractTitle(err, problem.getStatus()); + String problemTitle = problem.getTitle(); + if (problemTitle == null || !problemTitle.equals(title)) { + problem.setTitle(title); + } + + if (problem.getDetail() == null) { + // higher precedence to cause + problem.setDetail(getCustomizedErrorDetails(err)); + } + + Map problemProperties = problem.getProperties(); + if (problemProperties == null || !problemProperties.containsKey(MESSAGE_KEY)) problem.setProperty( + MESSAGE_KEY, + getMappedMessageKey(err) != null ? getMappedMessageKey(err) : "error.http." + problem.getStatus() + ); + + if (problemProperties == null || !problemProperties.containsKey(PATH_KEY)) problem.setProperty(PATH_KEY, getPathValue(request)); + + if ( + (err instanceof MethodArgumentNotValidException) && + (problemProperties == null || !problemProperties.containsKey(FIELD_ERRORS_KEY)) + ) problem.setProperty(FIELD_ERRORS_KEY, getFieldErrors((MethodArgumentNotValidException) err)); + + problem.setCause(buildCause(err.getCause(), request).orElse(null)); + + return problem; + } + + private String extractTitle(Throwable err, int statusCode) { + return getCustomizedTitle(err) != null ? getCustomizedTitle(err) : extractTitleForResponseStatus(err, statusCode); + } + + private List getFieldErrors(MethodArgumentNotValidException ex) { + return ex + .getBindingResult() + .getFieldErrors() + .stream() + .map(f -> + new FieldErrorVM( + f.getObjectName().replaceFirst("DTO$", ""), + f.getField(), + StringUtils.isNotBlank(f.getDefaultMessage()) ? f.getDefaultMessage() : f.getCode() + ) + ) + .toList(); + } + + private String extractTitleForResponseStatus(Throwable err, int statusCode) { + ResponseStatus specialStatus = extractResponseStatus(err); + return specialStatus == null ? HttpStatus.valueOf(statusCode).getReasonPhrase() : specialStatus.reason(); + } + + private String extractURI(NativeWebRequest request) { + HttpServletRequest nativeRequest = request.getNativeRequest(HttpServletRequest.class); + return nativeRequest != null ? nativeRequest.getRequestURI() : StringUtils.EMPTY; + } + + private HttpStatus toStatus(final Throwable throwable) { + // Let the ErrorResponse take this responsibility + if (throwable instanceof ErrorResponse err) return HttpStatus.valueOf(err.getBody().getStatus()); + + return Optional + .ofNullable(getMappedStatus(throwable)) + .orElse( + Optional.ofNullable(resolveResponseStatus(throwable)).map(ResponseStatus::value).orElse(HttpStatus.INTERNAL_SERVER_ERROR) + ); + } + + private ResponseStatus extractResponseStatus(final Throwable throwable) { + return Optional.ofNullable(resolveResponseStatus(throwable)).orElse(null); + } + + private ResponseStatus resolveResponseStatus(final Throwable type) { + final ResponseStatus candidate = findMergedAnnotation(type.getClass(), ResponseStatus.class); + return candidate == null && type.getCause() != null ? resolveResponseStatus(type.getCause()) : candidate; + } + + private URI getMappedType(Throwable err) { + if (err instanceof MethodArgumentNotValidException exp) return ErrorConstants.CONSTRAINT_VIOLATION_TYPE; + return ErrorConstants.DEFAULT_TYPE; + } + + private String getMappedMessageKey(Throwable err) { + if (err instanceof MethodArgumentNotValidException) return ErrorConstants.ERR_VALIDATION; + return null; + } + + private String getCustomizedTitle(Throwable err) { + if (err instanceof MethodArgumentNotValidException exp) return "Method argument not valid"; + return null; + } + + private String getCustomizedErrorDetails(Throwable err) { + Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); + if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) { + if (err instanceof HttpMessageConversionException) return "Unable to convert http message"; + if (containsPackageName(err.getMessage())) return "Unexpected runtime exception"; + } + return err.getCause() != null ? err.getCause().getMessage() : err.getMessage(); + } + + private HttpStatus getMappedStatus(Throwable err) { + // Where we disagree with Spring defaults + if (err instanceof AccessDeniedException) return HttpStatus.FORBIDDEN; + if (err instanceof BadCredentialsException) return HttpStatus.UNAUTHORIZED; + return null; + } + + private URI getPathValue(NativeWebRequest request) { + if (request == null) return URI.create("about:blank"); + return URI.create(extractURI(request)); + } + + private HttpHeaders buildHeaders(Throwable err, NativeWebRequest request) { + return err instanceof BadRequestAlertException + ? HeaderUtil.createFailureAlert( + applicationName, + true, + ((BadRequestAlertException) err).getEntityName(), + ((BadRequestAlertException) err).getErrorKey(), + ((BadRequestAlertException) err).getMessage() + ) + : null; + } + + public Optional buildCause(final Throwable throwable, NativeWebRequest request) { + if (throwable != null && isCasualChainEnabled()) { + return Optional.of(customizeProblem(getProblemDetailWithCause(throwable), throwable, request)); + } + return Optional.ofNullable(null); + } + + private boolean isCasualChainEnabled() { + // Customize as per the needs + return CASUAL_CHAIN_ENABLED; + } + + private boolean containsPackageName(String message) { + // This list is for sure not complete + return StringUtils.containsAny( + message, + "org.", + "java.", + "net.", + "jakarta.", + "javax.", + "com.", + "io.", + "de.", + "org.gcube.isdashboard" + ); + } +} diff --git a/src/main/java/org/gcube/isdashboard/web/rest/errors/FieldErrorVM.java b/src/main/java/org/gcube/isdashboard/web/rest/errors/FieldErrorVM.java new file mode 100644 index 0000000..73cbe3f --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/web/rest/errors/FieldErrorVM.java @@ -0,0 +1,32 @@ +package org.gcube.isdashboard.web.rest.errors; + +import java.io.Serializable; + +public class FieldErrorVM implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String objectName; + + private final String field; + + private final String message; + + public FieldErrorVM(String dto, String field, String message) { + this.objectName = dto; + this.field = field; + this.message = message; + } + + public String getObjectName() { + return objectName; + } + + public String getField() { + return field; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/org/gcube/isdashboard/web/rest/errors/package-info.java b/src/main/java/org/gcube/isdashboard/web/rest/errors/package-info.java new file mode 100644 index 0000000..ce505c9 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/web/rest/errors/package-info.java @@ -0,0 +1,4 @@ +/** + * Rest layer error handling. + */ +package org.gcube.isdashboard.web.rest.errors; diff --git a/src/main/java/org/gcube/isdashboard/web/rest/package-info.java b/src/main/java/org/gcube/isdashboard/web/rest/package-info.java new file mode 100644 index 0000000..c5c6dd5 --- /dev/null +++ b/src/main/java/org/gcube/isdashboard/web/rest/package-info.java @@ -0,0 +1,4 @@ +/** + * Rest layer. + */ +package org.gcube.isdashboard.web.rest; diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..5be7dbe --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,10 @@ + + ${AnsiColor.GREEN} ██╗${AnsiColor.RED} ██╗ ██╗ ████████╗ ███████╗ ██████╗ ████████╗ ████████╗ ███████╗ + ${AnsiColor.GREEN} ██║${AnsiColor.RED} ██║ ██║ ╚══██╔══╝ ██╔═══██╗ ██╔════╝ ╚══██╔══╝ ██╔═════╝ ██╔═══██╗ + ${AnsiColor.GREEN} ██║${AnsiColor.RED} ████████║ ██║ ███████╔╝ ╚█████╗ ██║ ██████╗ ███████╔╝ + ${AnsiColor.GREEN}██╗ ██║${AnsiColor.RED} ██╔═══██║ ██║ ██╔════╝ ╚═══██╗ ██║ ██╔═══╝ ██╔══██║ + ${AnsiColor.GREEN}╚██████╔╝${AnsiColor.RED} ██║ ██║ ████████╗ ██║ ██████╔╝ ██║ ████████╗ ██║ ╚██╗ + ${AnsiColor.GREEN} ╚═════╝ ${AnsiColor.RED} ╚═╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═╝ + +${AnsiColor.BRIGHT_BLUE}:: JHipster 🤓 :: Running Spring Boot ${spring-boot.version} :: Startup profile(s) ${spring.profiles.active} :: +:: https://www.jhipster.tech ::${AnsiColor.DEFAULT} diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml new file mode 100644 index 0000000..16a32e8 --- /dev/null +++ b/src/main/resources/config/application-dev.yml @@ -0,0 +1,83 @@ +# =================================================================== +# Spring Boot configuration for the "dev" profile. +# +# This configuration overrides the application.yml file. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +logging: + level: + ROOT: DEBUG + tech.jhipster: DEBUG + org.hibernate.SQL: DEBUG + org.gcube.isdashboard: DEBUG + +spring: + devtools: + restart: + enabled: true + additional-exclude: static/** + livereload: + enabled: false # we use Webpack dev server + BrowserSync for livereload + jackson: + serialization: + indent-output: true + messages: + cache-duration: PT1S # 1 second, see the ISO 8601 standard + thymeleaf: + cache: false + +server: + port: 8080 + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +jhipster: + cache: # Cache configuration + ehcache: # Ehcache configuration + time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache + max-entries: 100 # Number of objects in each cache entry + # CORS is only enabled by default with the "dev" profile + cors: + # Allow Ionic for JHipster by default (* no longer allowed in Spring Boot 2.4+) + allowed-origins: 'http://localhost:8100,https://localhost:8100,http://localhost:9000,https://localhost:9000,http://localhost:4200,https://localhost:4200' + # Enable CORS when running in GitHub Codespaces + allowed-origin-patterns: 'https://*.githubpreview.dev' + allowed-methods: '*' + allowed-headers: '*' + exposed-headers: 'Link,X-Total-Count,X-${jhipster.clientApp.name}-alert,X-${jhipster.clientApp.name}-error,X-${jhipster.clientApp.name}-params' + allow-credentials: true + max-age: 1800 + security: + remember-me: + # security key (this key should be unique for your application, and kept secret) + key: 6038bcb93c3f8cc61c163e71aa55573477e46c3535cf0624212567f396b89e7be83d90b03885a818d14b1b7f4928ab548561 + logging: + use-json-format: false # By default, logs are not in Json format + logstash: # Forward logs to logstash over a socket, used by LoggingConfiguration + enabled: false + host: localhost + port: 5000 + ring-buffer-size: 512 +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml new file mode 100644 index 0000000..9f035b0 --- /dev/null +++ b/src/main/resources/config/application-prod.yml @@ -0,0 +1,98 @@ +# =================================================================== +# Spring Boot configuration for the "prod" profile. +# +# This configuration overrides the application.yml file. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +logging: + level: + ROOT: INFO + tech.jhipster: INFO + org.gcube.isdashboard: INFO + +management: + prometheus: + metrics: + export: + enabled: false + +spring: + devtools: + restart: + enabled: false + livereload: + enabled: false + thymeleaf: + cache: true + +# =================================================================== +# To enable TLS in production, generate a certificate using: +# keytool -genkey -alias isdashboard -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650 +# +# You can also use Let's Encrypt: +# See details in topic "Create a Java Keystore (.JKS) from Let's Encrypt Certificates" on https://maximilian-boehm.com/en-gb/blog +# +# Then, modify the server.ssl properties so your "server" configuration looks like: +# +# server: +# port: 443 +# ssl: +# key-store: classpath:config/tls/keystore.p12 +# key-store-password: password +# key-store-type: PKCS12 +# key-alias: selfsigned +# # The ciphers suite enforce the security by deactivating some old and deprecated SSL cipher, this list was tested against SSL Labs (https://www.ssllabs.com/ssltest/) +# ciphers: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 ,TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 ,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA,TLS_RSA_WITH_CAMELLIA_256_CBC_SHA,TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA,TLS_RSA_WITH_CAMELLIA_128_CBC_SHA +# =================================================================== +server: + port: 8080 + shutdown: graceful # see https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-graceful-shutdown + compression: + enabled: true + mime-types: text/html,text/xml,text/plain,text/css,application/javascript,application/json,image/svg+xml + min-response-size: 1024 + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +jhipster: + http: + cache: # Used by the CachingHttpHeadersFilter + timeToLiveInDays: 1461 + cache: # Cache configuration + ehcache: # Ehcache configuration + time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache + max-entries: 1000 # Number of objects in each cache entry + security: + remember-me: + # security key (this key should be unique for your application, and kept secret) + key: 6038bcb93c3f8cc61c163e71aa55573477e46c3535cf0624212567f396b89e7be83d90b03885a818d14b1b7f4928ab548561 + logging: + use-json-format: false # By default, logs are not in Json format + logstash: # Forward logs to logstash over a socket, used by LoggingConfiguration + enabled: false + host: localhost + port: 5000 + ring-buffer-size: 512 +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: diff --git a/src/main/resources/config/application-tls.yml b/src/main/resources/config/application-tls.yml new file mode 100644 index 0000000..039f6f4 --- /dev/null +++ b/src/main/resources/config/application-tls.yml @@ -0,0 +1,19 @@ +# =================================================================== +# Activate this profile to enable TLS and HTTP/2. +# +# JHipster has generated a self-signed certificate, which will be used to encrypt traffic. +# As your browser will not understand this certificate, you will need to import it. +# +# Another (easiest) solution with Chrome is to enable the "allow-insecure-localhost" flag +# at chrome://flags/#allow-insecure-localhost +# =================================================================== +server: + ssl: + key-store: classpath:config/tls/keystore.p12 + key-store-password: password + key-store-type: PKCS12 + key-alias: selfsigned + ciphers: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + enabled-protocols: TLSv1.2 + http2: + enabled: true diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml new file mode 100644 index 0000000..b317922 --- /dev/null +++ b/src/main/resources/config/application.yml @@ -0,0 +1,192 @@ +# =================================================================== +# Spring Boot configuration. +# +# This configuration will be overridden by the Spring profile you use, +# for example application-dev.yml if you use the "dev" profile. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +--- +# Conditionally disable springdoc on missing api-docs profile +spring: + config: + activate: + on-profile: '!api-docs' +springdoc: + api-docs: + enabled: false +--- +management: + endpoints: + web: + base-path: /management + exposure: + include: + - configprops + - env + - health + - info + - jhimetrics + - jhiopenapigroups + - logfile + - loggers + - prometheus + - threaddump + - caches + endpoint: + health: + show-details: when_authorized + roles: 'ROLE_ADMIN' + probes: + enabled: true + group: + liveness: + include: livenessState + readiness: + include: readinessState + jhimetrics: + enabled: true + info: + git: + mode: full + env: + enabled: true + health: + mail: + enabled: false # When using the MailService, configure an SMTP server and set this to true + prometheus: + metrics: + export: + enabled: true + step: 60 + enable: + http: true + jvm: true + logback: true + process: true + system: true + distribution: + percentiles-histogram: + all: true + percentiles: + all: 0, 0.5, 0.75, 0.95, 0.99, 1.0 + tags: + application: ${spring.application.name} + web: + server: + request: + autotime: + enabled: true + +spring: + application: + name: isdashboard + profiles: + # The commented value for `active` can be replaced with valid Spring profiles to load. + # Otherwise, it will be filled in by maven when building the JAR file + # Either way, it can be overridden by `--spring.profiles.active` value passed in the commandline or `-Dspring.profiles.active` set in `JAVA_OPTS` + active: #spring.profiles.active# + group: + dev: + - dev + - api-docs + # Uncomment to activate TLS for the dev profile + #- tls + jmx: + enabled: false + messages: + basename: i18n/messages + main: + allow-bean-definition-overriding: true + mvc: + problemdetails: + enabled: true + security: + user: + name: admin + password: admin + roles: + - ADMIN + - USER + task: + execution: + thread-name-prefix: isdashboard-task- + pool: + core-size: 2 + max-size: 50 + queue-capacity: 10000 + scheduling: + thread-name-prefix: isdashboard-scheduling- + pool: + size: 2 + thymeleaf: + mode: HTML + output: + ansi: + console-available: true + +server: + servlet: + session: + cookie: + http-only: true + +springdoc: + show-actuator: true + +# Properties to be exposed on the /info management endpoint +info: + # Comma separated list of profiles that will trigger the ribbon to show + display-ribbon-on-profiles: 'dev' + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +jhipster: + clientApp: + name: 'isdashboardApp' + # By default CORS is disabled. Uncomment to enable. + # cors: + # allowed-origins: "http://localhost:8100,http://localhost:9000" + # allowed-methods: "*" + # allowed-headers: "*" + # exposed-headers: "Link,X-Total-Count,X-${jhipster.clientApp.name}-alert,X-${jhipster.clientApp.name}-error,X-${jhipster.clientApp.name}-params" + # allow-credentials: true + # max-age: 1800 + mail: + from: isdashboard@localhost + api-docs: + default-include-pattern: ${server.servlet.context-path:}/api/** + management-include-pattern: ${server.servlet.context-path:}/management/** + title: Isdashboard API + description: Isdashboard API documentation + version: 0.0.1 + terms-of-service-url: + contact-name: + contact-url: + contact-email: + license: unlicensed + license-url: + security: + content-security-policy: "default-src 'self'; frame-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:" +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: diff --git a/src/main/resources/config/tls/keystore.p12 b/src/main/resources/config/tls/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..278046a285b1dfa22196bedfcd317c97c39b746f GIT binary patch literal 2768 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 rpeE2BLr-h91)7mBbiw=>kxSQiMaaatRiM& + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html new file mode 100644 index 0000000..690e856 --- /dev/null +++ b/src/main/resources/templates/error.html @@ -0,0 +1,92 @@ + + + + + + Your request cannot be processed + + + +
+

Your request cannot be processed :(

+ +

Sorry, an error has occurred.

+ + Status:  ()
+ + Message: 
+
+
+ + diff --git a/src/main/webapp/404.html b/src/main/webapp/404.html new file mode 100644 index 0000000..7569d7e --- /dev/null +++ b/src/main/webapp/404.html @@ -0,0 +1,58 @@ + + + + + Page Not Found + + + + + +

Page Not Found

+

Sorry, but the page you were trying to view does not exist.

+ + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..f1611b5 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,13 @@ + + + + + html + text/html;charset=utf-8 + + + diff --git a/src/main/webapp/app/admin/admin-routing.module.ts b/src/main/webapp/app/admin/admin-routing.module.ts new file mode 100644 index 0000000..4b766f6 --- /dev/null +++ b/src/main/webapp/app/admin/admin-routing.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +/* jhipster-needle-add-admin-module-import - JHipster will add admin modules imports here */ + +@NgModule({ + imports: [ + /* jhipster-needle-add-admin-module - JHipster will add admin modules here */ + RouterModule.forChild([ + { + path: 'docs', + loadComponent: () => import('./docs/docs.component'), + title: 'API', + }, + /* jhipster-needle-add-admin-route - JHipster will add admin routes here */ + ]), + ], +}) +export default class AdminRoutingModule {} diff --git a/src/main/webapp/app/admin/docs/docs.component.html b/src/main/webapp/app/admin/docs/docs.component.html new file mode 100644 index 0000000..2402552 --- /dev/null +++ b/src/main/webapp/app/admin/docs/docs.component.html @@ -0,0 +1,10 @@ + diff --git a/src/main/webapp/app/admin/docs/docs.component.scss b/src/main/webapp/app/admin/docs/docs.component.scss new file mode 100644 index 0000000..bb9a6cc --- /dev/null +++ b/src/main/webapp/app/admin/docs/docs.component.scss @@ -0,0 +1,6 @@ +@import 'bootstrap/scss/functions'; +@import 'bootstrap/scss/variables'; + +iframe { + background: white; +} diff --git a/src/main/webapp/app/admin/docs/docs.component.ts b/src/main/webapp/app/admin/docs/docs.component.ts new file mode 100644 index 0000000..ea41883 --- /dev/null +++ b/src/main/webapp/app/admin/docs/docs.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'jhi-docs', + templateUrl: './docs.component.html', + styleUrls: ['./docs.component.scss'], +}) +export default class DocsComponent {} diff --git a/src/main/webapp/app/app-page-title-strategy.ts b/src/main/webapp/app/app-page-title-strategy.ts new file mode 100644 index 0000000..08c6d71 --- /dev/null +++ b/src/main/webapp/app/app-page-title-strategy.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; + +@Injectable() +export class AppPageTitleStrategy extends TitleStrategy { + constructor() { + super(); + } + + override updateTitle(routerState: RouterStateSnapshot): void { + let pageTitle = this.buildTitle(routerState); + if (!pageTitle) { + pageTitle = 'Isdashboard'; + } + document.title = pageTitle; + } +} diff --git a/src/main/webapp/app/app-routing.module.ts b/src/main/webapp/app/app-routing.module.ts new file mode 100644 index 0000000..e146c0a --- /dev/null +++ b/src/main/webapp/app/app-routing.module.ts @@ -0,0 +1,52 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { errorRoute } from './layouts/error/error.route'; +import { DEBUG_INFO_ENABLED } from 'app/app.constants'; +import { Authority } from 'app/config/authority.constants'; + +import HomeComponent from './home/home.component'; +import NavbarComponent from './layouts/navbar/navbar.component'; +import LoginComponent from './login/login.component'; + +import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'; + +@NgModule({ + imports: [ + RouterModule.forRoot( + [ + { + path: '', + component: HomeComponent, + title: 'Welcome, Java Hipster!', + }, + { + path: '', + component: NavbarComponent, + outlet: 'navbar', + }, + { + path: 'admin', + data: { + authorities: [Authority.ADMIN], + }, + canActivate: [UserRouteAccessService], + loadChildren: () => import('./admin/admin-routing.module'), + }, + { + path: 'login', + component: LoginComponent, + title: 'Sign in', + }, + { + path: '', + loadChildren: () => import(`./entities/entity-routing.module`).then(({ EntityRoutingModule }) => EntityRoutingModule), + }, + ...errorRoute, + ], + { enableTracing: DEBUG_INFO_ENABLED, bindToComponentInputs: true } + ), + ], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/src/main/webapp/app/app.constants.ts b/src/main/webapp/app/app.constants.ts new file mode 100644 index 0000000..695015c --- /dev/null +++ b/src/main/webapp/app/app.constants.ts @@ -0,0 +1,9 @@ +// These constants are injected via webpack DefinePlugin variables. +// You can add more variables in webpack.common.js or in profile specific webpack..js files. +// If you change the values in the webpack config files, you need to re run webpack to update the application + +declare const __DEBUG_INFO_ENABLED__: boolean; +declare const __VERSION__: string; + +export const VERSION = __VERSION__; +export const DEBUG_INFO_ENABLED = __DEBUG_INFO_ENABLED__; diff --git a/src/main/webapp/app/app.module.ts b/src/main/webapp/app/app.module.ts new file mode 100644 index 0000000..8081aeb --- /dev/null +++ b/src/main/webapp/app/app.module.ts @@ -0,0 +1,49 @@ +import { NgModule, LOCALE_ID } from '@angular/core'; +import { registerLocaleData } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; +import locale from '@angular/common/locales/en'; +import { BrowserModule, Title } from '@angular/platform-browser'; +import { TitleStrategy } from '@angular/router'; +import { ServiceWorkerModule } from '@angular/service-worker'; +import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; +import dayjs from 'dayjs/esm'; +import { NgbDateAdapter, NgbDatepickerConfig } from '@ng-bootstrap/ng-bootstrap'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import './config/dayjs'; +import { AppRoutingModule } from './app-routing.module'; +// jhipster-needle-angular-add-module-import JHipster will add new module here +import { NgbDateDayjsAdapter } from './config/datepicker-adapter'; +import { fontAwesomeIcons } from './config/font-awesome-icons'; +import { httpInterceptorProviders } from 'app/core/interceptor/index'; +import MainComponent from './layouts/main/main.component'; +import MainModule from './layouts/main/main.module'; +import { AppPageTitleStrategy } from './app-page-title-strategy'; + +@NgModule({ + imports: [ + BrowserModule, + // jhipster-needle-angular-add-module JHipster will add new module here + AppRoutingModule, + // Set this to true to enable service worker (PWA) + ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }), + HttpClientModule, + MainModule, + ], + providers: [ + Title, + { provide: LOCALE_ID, useValue: 'en' }, + { provide: NgbDateAdapter, useClass: NgbDateDayjsAdapter }, + httpInterceptorProviders, + { provide: TitleStrategy, useClass: AppPageTitleStrategy }, + ], + bootstrap: [MainComponent], +}) +export class AppModule { + constructor(applicationConfigService: ApplicationConfigService, iconLibrary: FaIconLibrary, dpConfig: NgbDatepickerConfig) { + applicationConfigService.setEndpointPrefix(SERVER_API_URL); + registerLocaleData(locale); + iconLibrary.addIcons(...fontAwesomeIcons); + dpConfig.minDate = { year: dayjs().subtract(100, 'year').year(), month: 1, day: 1 }; + } +} diff --git a/src/main/webapp/app/config/authority.constants.ts b/src/main/webapp/app/config/authority.constants.ts new file mode 100644 index 0000000..1501bcf --- /dev/null +++ b/src/main/webapp/app/config/authority.constants.ts @@ -0,0 +1,4 @@ +export enum Authority { + ADMIN = 'ROLE_ADMIN', + USER = 'ROLE_USER', +} diff --git a/src/main/webapp/app/config/datepicker-adapter.ts b/src/main/webapp/app/config/datepicker-adapter.ts new file mode 100644 index 0000000..3f8b16c --- /dev/null +++ b/src/main/webapp/app/config/datepicker-adapter.ts @@ -0,0 +1,20 @@ +/** + * Angular bootstrap Date adapter + */ +import { Injectable } from '@angular/core'; +import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; +import dayjs from 'dayjs/esm'; + +@Injectable() +export class NgbDateDayjsAdapter extends NgbDateAdapter { + fromModel(date: dayjs.Dayjs | null): NgbDateStruct | null { + if (date && dayjs.isDayjs(date) && date.isValid()) { + return { year: date.year(), month: date.month() + 1, day: date.date() }; + } + return null; + } + + toModel(date: NgbDateStruct | null): dayjs.Dayjs | null { + return date ? dayjs(`${date.year}-${date.month}-${date.day}`) : null; + } +} diff --git a/src/main/webapp/app/config/dayjs.ts b/src/main/webapp/app/config/dayjs.ts new file mode 100644 index 0000000..23e825d --- /dev/null +++ b/src/main/webapp/app/config/dayjs.ts @@ -0,0 +1,11 @@ +import dayjs from 'dayjs/esm'; +import customParseFormat from 'dayjs/esm/plugin/customParseFormat'; +import duration from 'dayjs/esm/plugin/duration'; +import relativeTime from 'dayjs/esm/plugin/relativeTime'; + +// jhipster-needle-i18n-language-dayjs-imports - JHipster will import languages from dayjs here + +// DAYJS CONFIGURATION +dayjs.extend(customParseFormat); +dayjs.extend(duration); +dayjs.extend(relativeTime); diff --git a/src/main/webapp/app/config/error.constants.ts b/src/main/webapp/app/config/error.constants.ts new file mode 100644 index 0000000..eff19a3 --- /dev/null +++ b/src/main/webapp/app/config/error.constants.ts @@ -0,0 +1,3 @@ +export const PROBLEM_BASE_URL = 'https://www.jhipster.tech/problem'; +export const EMAIL_ALREADY_USED_TYPE = `${PROBLEM_BASE_URL}/email-already-used`; +export const LOGIN_ALREADY_USED_TYPE = `${PROBLEM_BASE_URL}/login-already-used`; diff --git a/src/main/webapp/app/config/font-awesome-icons.ts b/src/main/webapp/app/config/font-awesome-icons.ts new file mode 100644 index 0000000..7fcf169 --- /dev/null +++ b/src/main/webapp/app/config/font-awesome-icons.ts @@ -0,0 +1,83 @@ +import { + faArrowLeft, + faAsterisk, + faBan, + faBars, + faBell, + faBook, + faCalendarAlt, + faCheck, + faCloud, + faCogs, + faDatabase, + faEye, + faFlag, + faHeart, + faHome, + faList, + faLock, + faPencilAlt, + faPlus, + faRoad, + faSave, + faSearch, + faSignOutAlt, + faSignInAlt, + faSort, + faSortDown, + faSortUp, + faSync, + faTachometerAlt, + faTasks, + faThList, + faTimes, + faTrashAlt, + faUser, + faUserPlus, + faUsers, + faUsersCog, + faWrench, + // jhipster-needle-add-icon-import +} from '@fortawesome/free-solid-svg-icons'; + +export const fontAwesomeIcons = [ + faArrowLeft, + faAsterisk, + faBan, + faBars, + faBell, + faBook, + faCalendarAlt, + faCheck, + faCloud, + faCogs, + faDatabase, + faEye, + faFlag, + faHeart, + faHome, + faList, + faLock, + faPencilAlt, + faPlus, + faRoad, + faSave, + faSearch, + faSignOutAlt, + faSignInAlt, + faSort, + faSortDown, + faSortUp, + faSync, + faTachometerAlt, + faTasks, + faThList, + faTimes, + faTrashAlt, + faUser, + faUserPlus, + faUsers, + faUsersCog, + faWrench, + // jhipster-needle-add-icon-import +]; diff --git a/src/main/webapp/app/config/input.constants.ts b/src/main/webapp/app/config/input.constants.ts new file mode 100644 index 0000000..1e3978a --- /dev/null +++ b/src/main/webapp/app/config/input.constants.ts @@ -0,0 +1,2 @@ +export const DATE_FORMAT = 'YYYY-MM-DD'; +export const DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm'; diff --git a/src/main/webapp/app/config/navigation.constants.ts b/src/main/webapp/app/config/navigation.constants.ts new file mode 100644 index 0000000..609160d --- /dev/null +++ b/src/main/webapp/app/config/navigation.constants.ts @@ -0,0 +1,5 @@ +export const ASC = 'asc'; +export const DESC = 'desc'; +export const SORT = 'sort'; +export const ITEM_DELETED_EVENT = 'deleted'; +export const DEFAULT_SORT_DATA = 'defaultSort'; diff --git a/src/main/webapp/app/config/pagination.constants.ts b/src/main/webapp/app/config/pagination.constants.ts new file mode 100644 index 0000000..6bee3ff --- /dev/null +++ b/src/main/webapp/app/config/pagination.constants.ts @@ -0,0 +1,3 @@ +export const TOTAL_COUNT_RESPONSE_HEADER = 'X-Total-Count'; +export const PAGE_HEADER = 'page'; +export const ITEMS_PER_PAGE = 20; diff --git a/src/main/webapp/app/config/uib-pagination.config.ts b/src/main/webapp/app/config/uib-pagination.config.ts new file mode 100644 index 0000000..ecabe16 --- /dev/null +++ b/src/main/webapp/app/config/uib-pagination.config.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { NgbPaginationConfig } from '@ng-bootstrap/ng-bootstrap'; + +import { ITEMS_PER_PAGE } from 'app/config/pagination.constants'; + +@Injectable({ providedIn: 'root' }) +export class PaginationConfig { + constructor(config: NgbPaginationConfig) { + config.boundaryLinks = true; + config.maxSize = 5; + config.pageSize = ITEMS_PER_PAGE; + config.size = 'sm'; + } +} diff --git a/src/main/webapp/app/core/auth/account.model.ts b/src/main/webapp/app/core/auth/account.model.ts new file mode 100644 index 0000000..22e083c --- /dev/null +++ b/src/main/webapp/app/core/auth/account.model.ts @@ -0,0 +1,12 @@ +export class Account { + constructor( + public activated: boolean, + public authorities: string[], + public email: string, + public firstName: string | null, + public langKey: string, + public lastName: string | null, + public login: string, + public imageUrl: string | null + ) {} +} diff --git a/src/main/webapp/app/core/auth/account.service.spec.ts b/src/main/webapp/app/core/auth/account.service.spec.ts new file mode 100644 index 0000000..fa7a5f9 --- /dev/null +++ b/src/main/webapp/app/core/auth/account.service.spec.ts @@ -0,0 +1,198 @@ +jest.mock('app/core/auth/state-storage.service'); + +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { Account } from 'app/core/auth/account.model'; +import { Authority } from 'app/config/authority.constants'; +import { StateStorageService } from 'app/core/auth/state-storage.service'; + +import { AccountService } from './account.service'; + +function accountWithAuthorities(authorities: string[]): Account { + return { + activated: true, + authorities, + email: '', + firstName: '', + langKey: '', + lastName: '', + login: '', + imageUrl: '', + }; +} + +describe('Account Service', () => { + let service: AccountService; + let httpMock: HttpTestingController; + let mockStorageService: StateStorageService; + let mockRouter: Router; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])], + providers: [StateStorageService], + }); + + service = TestBed.inject(AccountService); + httpMock = TestBed.inject(HttpTestingController); + mockStorageService = TestBed.inject(StateStorageService); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigateByUrl').mockImplementation(() => Promise.resolve(true)); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('authenticate', () => { + it('authenticationState should emit null if input is null', () => { + // GIVEN + let userIdentity: Account | null = accountWithAuthorities([]); + service.getAuthenticationState().subscribe(account => (userIdentity = account)); + + // WHEN + service.authenticate(null); + + // THEN + expect(userIdentity).toBeNull(); + expect(service.isAuthenticated()).toBe(false); + }); + + it('authenticationState should emit the same account as was in input parameter', () => { + // GIVEN + const expectedResult = accountWithAuthorities([]); + let userIdentity: Account | null = null; + service.getAuthenticationState().subscribe(account => (userIdentity = account)); + + // WHEN + service.authenticate(expectedResult); + + // THEN + expect(userIdentity).toEqual(expectedResult); + expect(service.isAuthenticated()).toBe(true); + }); + }); + + describe('identity', () => { + it('should call /account only once if last call have not returned', () => { + // When I call + service.identity().subscribe(); + // Once more + service.identity().subscribe(); + // Then there is only request + httpMock.expectOne({ method: 'GET' }); + }); + + it('should call /account only once if not logged out after first authentication and should call /account again if user has logged out', () => { + // Given the user is authenticated + service.identity().subscribe(); + httpMock.expectOne({ method: 'GET' }).flush({}); + + // When I call + service.identity().subscribe(); + + // Then there is no second request + httpMock.expectNone({ method: 'GET' }); + + // When I log out + service.authenticate(null); + // and then call + service.identity().subscribe(); + + // Then there is a new request + httpMock.expectOne({ method: 'GET' }); + }); + + describe('navigateToStoredUrl', () => { + it('should navigate to the previous stored url post successful authentication', () => { + // GIVEN + mockStorageService.getUrl = jest.fn(() => 'admin/users?page=0'); + + // WHEN + service.identity().subscribe(); + httpMock.expectOne({ method: 'GET' }).flush({}); + + // THEN + expect(mockStorageService.getUrl).toHaveBeenCalledTimes(1); + expect(mockStorageService.clearUrl).toHaveBeenCalledTimes(1); + expect(mockRouter.navigateByUrl).toHaveBeenCalledWith('admin/users?page=0'); + }); + + it('should not navigate to the previous stored url when authentication fails', () => { + // WHEN + service.identity().subscribe(); + httpMock.expectOne({ method: 'GET' }).error(new ErrorEvent('')); + + // THEN + expect(mockStorageService.getUrl).not.toHaveBeenCalled(); + expect(mockStorageService.clearUrl).not.toHaveBeenCalled(); + expect(mockRouter.navigateByUrl).not.toHaveBeenCalled(); + }); + + it('should not navigate to the previous stored url when no such url exists post successful authentication', () => { + // GIVEN + mockStorageService.getUrl = jest.fn(() => null); + + // WHEN + service.identity().subscribe(); + httpMock.expectOne({ method: 'GET' }).flush({}); + + // THEN + expect(mockStorageService.getUrl).toHaveBeenCalledTimes(1); + expect(mockStorageService.clearUrl).not.toHaveBeenCalled(); + expect(mockRouter.navigateByUrl).not.toHaveBeenCalled(); + }); + }); + }); + + describe('hasAnyAuthority', () => { + describe('hasAnyAuthority string parameter', () => { + it('should return false if user is not logged', () => { + const hasAuthority = service.hasAnyAuthority(Authority.USER); + expect(hasAuthority).toBe(false); + }); + + it('should return false if user is logged and has not authority', () => { + service.authenticate(accountWithAuthorities([Authority.USER])); + + const hasAuthority = service.hasAnyAuthority(Authority.ADMIN); + + expect(hasAuthority).toBe(false); + }); + + it('should return true if user is logged and has authority', () => { + service.authenticate(accountWithAuthorities([Authority.USER])); + + const hasAuthority = service.hasAnyAuthority(Authority.USER); + + expect(hasAuthority).toBe(true); + }); + }); + + describe('hasAnyAuthority array parameter', () => { + it('should return false if user is not logged', () => { + const hasAuthority = service.hasAnyAuthority([Authority.USER]); + expect(hasAuthority).toBeFalsy(); + }); + + it('should return false if user is logged and has not authority', () => { + service.authenticate(accountWithAuthorities([Authority.USER])); + + const hasAuthority = service.hasAnyAuthority([Authority.ADMIN]); + + expect(hasAuthority).toBe(false); + }); + + it('should return true if user is logged and has authority', () => { + service.authenticate(accountWithAuthorities([Authority.USER])); + + const hasAuthority = service.hasAnyAuthority([Authority.USER, Authority.ADMIN]); + + expect(hasAuthority).toBe(true); + }); + }); + }); +}); diff --git a/src/main/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts new file mode 100644 index 0000000..e2e8342 --- /dev/null +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { HttpClient } from '@angular/common/http'; +import { Observable, ReplaySubject, of } from 'rxjs'; +import { shareReplay, tap, catchError } from 'rxjs/operators'; + +import { StateStorageService } from 'app/core/auth/state-storage.service'; +import { ApplicationConfigService } from '../config/application-config.service'; +import { Account } from 'app/core/auth/account.model'; + +@Injectable({ providedIn: 'root' }) +export class AccountService { + private userIdentity: Account | null = null; + private authenticationState = new ReplaySubject(1); + private accountCache$?: Observable | null; + + constructor( + private http: HttpClient, + private stateStorageService: StateStorageService, + private router: Router, + private applicationConfigService: ApplicationConfigService + ) {} + + authenticate(identity: Account | null): void { + this.userIdentity = identity; + this.authenticationState.next(this.userIdentity); + if (!identity) { + this.accountCache$ = null; + } + } + + hasAnyAuthority(authorities: string[] | string): boolean { + if (!this.userIdentity) { + return false; + } + if (!Array.isArray(authorities)) { + authorities = [authorities]; + } + return this.userIdentity.authorities.some((authority: string) => authorities.includes(authority)); + } + + identity(force?: boolean): Observable { + if (!this.accountCache$ || force) { + this.accountCache$ = this.fetch().pipe( + tap((account: Account) => { + this.authenticate(account); + + this.navigateToStoredUrl(); + }), + shareReplay() + ); + } + return this.accountCache$.pipe(catchError(() => of(null))); + } + + isAuthenticated(): boolean { + return this.userIdentity !== null; + } + + getAuthenticationState(): Observable { + return this.authenticationState.asObservable(); + } + + private fetch(): Observable { + return this.http.get(this.applicationConfigService.getEndpointFor('api/account')); + } + + private navigateToStoredUrl(): void { + // previousState can be set in the authExpiredInterceptor and in the userRouteAccessService + // if login is successful, go to stored previousState and clear previousState + const previousUrl = this.stateStorageService.getUrl(); + if (previousUrl) { + this.stateStorageService.clearUrl(); + this.router.navigateByUrl(previousUrl); + } + } +} diff --git a/src/main/webapp/app/core/auth/auth-session.service.ts b/src/main/webapp/app/core/auth/auth-session.service.ts new file mode 100644 index 0000000..b8f8eaa --- /dev/null +++ b/src/main/webapp/app/core/auth/auth-session.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { ApplicationConfigService } from '../config/application-config.service'; +import { Login } from 'app/login/login.model'; + +@Injectable({ providedIn: 'root' }) +export class AuthServerProvider { + constructor(private http: HttpClient, private applicationConfigService: ApplicationConfigService) {} + + login(credentials: Login): Observable<{}> { + const data = + `username=${encodeURIComponent(credentials.username)}` + + `&password=${encodeURIComponent(credentials.password)}` + + `&remember-me=${credentials.rememberMe ? 'true' : 'false'}` + + '&submit=Login'; + + const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded'); + + return this.http.post(this.applicationConfigService.getEndpointFor('api/authentication'), data, { headers }); + } + + logout(): Observable { + // logout from the server + return this.http.post(this.applicationConfigService.getEndpointFor('api/logout'), {}).pipe( + map(() => { + // to get a new csrf token call the api + this.http.get(this.applicationConfigService.getEndpointFor('api/account')).subscribe(); + }) + ); + } +} diff --git a/src/main/webapp/app/core/auth/state-storage.service.ts b/src/main/webapp/app/core/auth/state-storage.service.ts new file mode 100644 index 0000000..af8b562 --- /dev/null +++ b/src/main/webapp/app/core/auth/state-storage.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class StateStorageService { + private previousUrlKey = 'previousUrl'; + private authenticationKey = 'jhi-authenticationToken'; + + storeUrl(url: string): void { + sessionStorage.setItem(this.previousUrlKey, JSON.stringify(url)); + } + + getUrl(): string | null { + const previousUrl = sessionStorage.getItem(this.previousUrlKey); + return previousUrl ? (JSON.parse(previousUrl) as string | null) : previousUrl; + } + + clearUrl(): void { + sessionStorage.removeItem(this.previousUrlKey); + } + + storeAuthenticationToken(authenticationToken: string, rememberMe: boolean): void { + authenticationToken = JSON.stringify(authenticationToken); + this.clearAuthenticationToken(); + if (rememberMe) { + localStorage.setItem(this.authenticationKey, authenticationToken); + } else { + sessionStorage.setItem(this.authenticationKey, authenticationToken); + } + } + + getAuthenticationToken(): string | null { + const authenticationToken = localStorage.getItem(this.authenticationKey) ?? sessionStorage.getItem(this.authenticationKey); + return authenticationToken ? (JSON.parse(authenticationToken) as string | null) : authenticationToken; + } + + clearAuthenticationToken(): void { + sessionStorage.removeItem(this.authenticationKey); + localStorage.removeItem(this.authenticationKey); + } +} diff --git a/src/main/webapp/app/core/auth/user-route-access.service.ts b/src/main/webapp/app/core/auth/user-route-access.service.ts new file mode 100644 index 0000000..17481e9 --- /dev/null +++ b/src/main/webapp/app/core/auth/user-route-access.service.ts @@ -0,0 +1,33 @@ +import { inject, isDevMode } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; +import { map } from 'rxjs/operators'; + +import { AccountService } from 'app/core/auth/account.service'; +import { StateStorageService } from './state-storage.service'; + +export const UserRouteAccessService: CanActivateFn = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const accountService = inject(AccountService); + const router = inject(Router); + const stateStorageService = inject(StateStorageService); + return accountService.identity().pipe( + map(account => { + if (account) { + const authorities = next.data['authorities']; + + if (!authorities || authorities.length === 0 || accountService.hasAnyAuthority(authorities)) { + return true; + } + + if (isDevMode()) { + console.error('User has not any of required authorities: ', authorities); + } + router.navigate(['accessdenied']); + return false; + } + + stateStorageService.storeUrl(state.url); + router.navigate(['/login']); + return false; + }) + ); +}; diff --git a/src/main/webapp/app/core/config/application-config.service.spec.ts b/src/main/webapp/app/core/config/application-config.service.spec.ts new file mode 100644 index 0000000..4451c9b --- /dev/null +++ b/src/main/webapp/app/core/config/application-config.service.spec.ts @@ -0,0 +1,40 @@ +import { TestBed } from '@angular/core/testing'; + +import { ApplicationConfigService } from './application-config.service'; + +describe('ApplicationConfigService', () => { + let service: ApplicationConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ApplicationConfigService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('without prefix', () => { + it('should return correctly', () => { + expect(service.getEndpointFor('api')).toEqual('api'); + }); + + it('should return correctly when passing microservice', () => { + expect(service.getEndpointFor('api', 'microservice')).toEqual('services/microservice/api'); + }); + }); + + describe('with prefix', () => { + beforeEach(() => { + service.setEndpointPrefix('prefix/'); + }); + + it('should return correctly', () => { + expect(service.getEndpointFor('api')).toEqual('prefix/api'); + }); + + it('should return correctly when passing microservice', () => { + expect(service.getEndpointFor('api', 'microservice')).toEqual('prefix/services/microservice/api'); + }); + }); +}); diff --git a/src/main/webapp/app/core/config/application-config.service.ts b/src/main/webapp/app/core/config/application-config.service.ts new file mode 100644 index 0000000..0102e5f --- /dev/null +++ b/src/main/webapp/app/core/config/application-config.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplicationConfigService { + private endpointPrefix = ''; + private microfrontend = false; + + setEndpointPrefix(endpointPrefix: string): void { + this.endpointPrefix = endpointPrefix; + } + + setMicrofrontend(microfrontend = true): void { + this.microfrontend = microfrontend; + } + + isMicrofrontend(): boolean { + return this.microfrontend; + } + + getEndpointFor(api: string, microservice?: string): string { + if (microservice) { + return `${this.endpointPrefix}services/${microservice}/${api}`; + } + return `${this.endpointPrefix}${api}`; + } +} diff --git a/src/main/webapp/app/core/interceptor/auth-expired.interceptor.ts b/src/main/webapp/app/core/interceptor/auth-expired.interceptor.ts new file mode 100644 index 0000000..de93afd --- /dev/null +++ b/src/main/webapp/app/core/interceptor/auth-expired.interceptor.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { Router } from '@angular/router'; + +import { LoginService } from 'app/login/login.service'; +import { StateStorageService } from 'app/core/auth/state-storage.service'; +import { AccountService } from 'app/core/auth/account.service'; + +@Injectable() +export class AuthExpiredInterceptor implements HttpInterceptor { + constructor( + private loginService: LoginService, + private stateStorageService: StateStorageService, + private router: Router, + private accountService: AccountService + ) {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe( + tap({ + error: (err: HttpErrorResponse) => { + if (err.status === 401 && err.url && !err.url.includes('api/account') && this.accountService.isAuthenticated()) { + if (err.url.includes(this.loginService.logoutUrl())) { + this.loginService.logoutInClient(); + return; + } + this.stateStorageService.storeUrl(this.router.routerState.snapshot.url); + this.loginService.logout(); + this.router.navigate(['/login']); + } + }, + }) + ); + } +} diff --git a/src/main/webapp/app/core/interceptor/error-handler.interceptor.ts b/src/main/webapp/app/core/interceptor/error-handler.interceptor.ts new file mode 100644 index 0000000..71389e1 --- /dev/null +++ b/src/main/webapp/app/core/interceptor/error-handler.interceptor.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { HttpInterceptor, HttpRequest, HttpErrorResponse, HttpHandler, HttpEvent } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +import { EventManager, EventWithContent } from 'app/core/util/event-manager.service'; + +@Injectable() +export class ErrorHandlerInterceptor implements HttpInterceptor { + constructor(private eventManager: EventManager) {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe( + tap({ + error: (err: HttpErrorResponse) => { + if (!(err.status === 401 && (err.message === '' || err.url?.includes('api/account')))) { + this.eventManager.broadcast(new EventWithContent('isdashboardApp.httpError', err)); + } + }, + }) + ); + } +} diff --git a/src/main/webapp/app/core/interceptor/index.ts b/src/main/webapp/app/core/interceptor/index.ts new file mode 100644 index 0000000..a4f96d0 --- /dev/null +++ b/src/main/webapp/app/core/interceptor/index.ts @@ -0,0 +1,23 @@ +import { HTTP_INTERCEPTORS } from '@angular/common/http'; + +import { AuthExpiredInterceptor } from 'app/core/interceptor/auth-expired.interceptor'; +import { ErrorHandlerInterceptor } from 'app/core/interceptor/error-handler.interceptor'; +import { NotificationInterceptor } from 'app/core/interceptor/notification.interceptor'; + +export const httpInterceptorProviders = [ + { + provide: HTTP_INTERCEPTORS, + useClass: AuthExpiredInterceptor, + multi: true, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: ErrorHandlerInterceptor, + multi: true, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: NotificationInterceptor, + multi: true, + }, +]; diff --git a/src/main/webapp/app/core/interceptor/notification.interceptor.ts b/src/main/webapp/app/core/interceptor/notification.interceptor.ts new file mode 100644 index 0000000..cf6abff --- /dev/null +++ b/src/main/webapp/app/core/interceptor/notification.interceptor.ts @@ -0,0 +1,34 @@ +import { HttpInterceptor, HttpRequest, HttpResponse, HttpHandler, HttpEvent } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +import { AlertService } from 'app/core/util/alert.service'; + +@Injectable() +export class NotificationInterceptor implements HttpInterceptor { + constructor(private alertService: AlertService) {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe( + tap((event: HttpEvent) => { + if (event instanceof HttpResponse) { + let alert: string | null = null; + + for (const headerKey of event.headers.keys()) { + if (headerKey.toLowerCase().endsWith('app-alert')) { + alert = event.headers.get(headerKey); + } + } + + if (alert) { + this.alertService.addAlert({ + type: 'success', + message: alert, + }); + } + } + }) + ); + } +} diff --git a/src/main/webapp/app/core/request/request-util.ts b/src/main/webapp/app/core/request/request-util.ts new file mode 100644 index 0000000..694a238 --- /dev/null +++ b/src/main/webapp/app/core/request/request-util.ts @@ -0,0 +1,23 @@ +import { HttpParams } from '@angular/common/http'; + +export const createRequestOption = (req?: any): HttpParams => { + let options: HttpParams = new HttpParams(); + + if (req) { + Object.keys(req).forEach(key => { + if (key !== 'sort' && req[key] !== undefined) { + for (const value of [].concat(req[key]).filter(v => v !== '')) { + options = options.append(key, value); + } + } + }); + + if (req.sort) { + req.sort.forEach((val: string) => { + options = options.append('sort', val); + }); + } + } + + return options; +}; diff --git a/src/main/webapp/app/core/request/request.model.ts b/src/main/webapp/app/core/request/request.model.ts new file mode 100644 index 0000000..5de2b69 --- /dev/null +++ b/src/main/webapp/app/core/request/request.model.ts @@ -0,0 +1,11 @@ +export interface Pagination { + page: number; + size: number; + sort: string[]; +} + +export interface Search { + query: string; +} + +export interface SearchWithPagination extends Search, Pagination {} diff --git a/src/main/webapp/app/core/util/alert.service.spec.ts b/src/main/webapp/app/core/util/alert.service.spec.ts new file mode 100644 index 0000000..65beb0f --- /dev/null +++ b/src/main/webapp/app/core/util/alert.service.spec.ts @@ -0,0 +1,233 @@ +import { inject, TestBed } from '@angular/core/testing'; + +import { Alert, AlertService } from './alert.service'; + +describe('Alert service test', () => { + describe('Alert Service Test', () => { + let extAlerts: Alert[]; + + beforeEach(() => { + TestBed.configureTestingModule({}); + jest.useFakeTimers(); + extAlerts = []; + }); + + it('should produce a proper alert object and fetch it', inject([AlertService], (service: AlertService) => { + expect( + service.addAlert({ + type: 'success', + message: 'Hello Jhipster', + timeout: 3000, + toast: true, + position: 'top left', + }) + ).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + id: 0, + timeout: 3000, + toast: true, + position: 'top left', + } as Alert) + ); + + expect(service.get().length).toBe(1); + expect(service.get()[0]).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + id: 0, + timeout: 3000, + toast: true, + position: 'top left', + } as Alert) + ); + })); + + it('should produce a proper alert object and add it to external alert objects array', inject( + [AlertService], + (service: AlertService) => { + expect( + service.addAlert( + { + type: 'success', + message: 'Hello Jhipster', + timeout: 3000, + toast: true, + position: 'top left', + }, + extAlerts + ) + ).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + id: 0, + timeout: 3000, + toast: true, + position: 'top left', + } as Alert) + ); + + expect(extAlerts.length).toBe(1); + expect(extAlerts[0]).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + id: 0, + timeout: 3000, + toast: true, + position: 'top left', + } as Alert) + ); + } + )); + + it('should produce an alert object with correct id', inject([AlertService], (service: AlertService) => { + service.addAlert({ type: 'info', message: 'Hello Jhipster info' }); + expect(service.addAlert({ type: 'success', message: 'Hello Jhipster success' })).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster success', + id: 1, + } as Alert) + ); + + expect(service.get().length).toBe(2); + expect(service.get()[1]).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster success', + id: 1, + } as Alert) + ); + })); + + it('should close an alert correctly', inject([AlertService], (service: AlertService) => { + const alert0 = service.addAlert({ type: 'info', message: 'Hello Jhipster info' }); + const alert1 = service.addAlert({ type: 'info', message: 'Hello Jhipster info 2' }); + const alert2 = service.addAlert({ type: 'success', message: 'Hello Jhipster success' }); + expect(alert2).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster success', + id: 2, + } as Alert) + ); + + expect(service.get().length).toBe(3); + alert1.close?.(service.get()); + expect(service.get().length).toBe(2); + expect(service.get()[1]).not.toEqual( + expect.objectContaining({ + type: 'info', + message: 'Hello Jhipster info 2', + id: 1, + } as Alert) + ); + alert2.close?.(service.get()); + expect(service.get().length).toBe(1); + expect(service.get()[0]).not.toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster success', + id: 2, + } as Alert) + ); + alert0.close?.(service.get()); + expect(service.get().length).toBe(0); + })); + + it('should close an alert on timeout correctly', inject([AlertService], (service: AlertService) => { + service.addAlert({ type: 'info', message: 'Hello Jhipster info' }); + + expect(service.get().length).toBe(1); + + jest.advanceTimersByTime(6000); + + expect(service.get().length).toBe(0); + })); + + it('should clear alerts', inject([AlertService], (service: AlertService) => { + service.addAlert({ type: 'info', message: 'Hello Jhipster info' }); + service.addAlert({ type: 'danger', message: 'Hello Jhipster info' }); + service.addAlert({ type: 'success', message: 'Hello Jhipster info' }); + expect(service.get().length).toBe(3); + service.clear(); + expect(service.get().length).toBe(0); + })); + + it('should produce a scoped alert', inject([AlertService], (service: AlertService) => { + expect( + service.addAlert( + { + type: 'success', + message: 'Hello Jhipster', + timeout: 3000, + toast: true, + position: 'top left', + }, + [] + ) + ).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + id: 0, + timeout: 3000, + toast: true, + position: 'top left', + } as Alert) + ); + + expect(service.get().length).toBe(0); + })); + + it('should produce a success message', inject([AlertService], (service: AlertService) => { + expect(service.addAlert({ type: 'success', message: 'Hello Jhipster' })).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + } as Alert) + ); + })); + + it('should produce a success message with custom position', inject([AlertService], (service: AlertService) => { + expect(service.addAlert({ type: 'success', message: 'Hello Jhipster', position: 'bottom left' })).toEqual( + expect.objectContaining({ + type: 'success', + message: 'Hello Jhipster', + position: 'bottom left', + } as Alert) + ); + })); + + it('should produce a error message', inject([AlertService], (service: AlertService) => { + expect(service.addAlert({ type: 'danger', message: 'Hello Jhipster' })).toEqual( + expect.objectContaining({ + type: 'danger', + message: 'Hello Jhipster', + } as Alert) + ); + })); + + it('should produce a warning message', inject([AlertService], (service: AlertService) => { + expect(service.addAlert({ type: 'warning', message: 'Hello Jhipster' })).toEqual( + expect.objectContaining({ + type: 'warning', + message: 'Hello Jhipster', + } as Alert) + ); + })); + + it('should produce a info message', inject([AlertService], (service: AlertService) => { + expect(service.addAlert({ type: 'info', message: 'Hello Jhipster' })).toEqual( + expect.objectContaining({ + type: 'info', + message: 'Hello Jhipster', + } as Alert) + ); + })); + }); +}); diff --git a/src/main/webapp/app/core/util/alert.service.ts b/src/main/webapp/app/core/util/alert.service.ts new file mode 100644 index 0000000..941ce67 --- /dev/null +++ b/src/main/webapp/app/core/util/alert.service.ts @@ -0,0 +1,73 @@ +import { Injectable, SecurityContext, NgZone } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +export type AlertType = 'success' | 'danger' | 'warning' | 'info'; + +export interface Alert { + id?: number; + type: AlertType; + message?: string; + timeout?: number; + toast?: boolean; + position?: string; + close?: (alerts: Alert[]) => void; +} + +@Injectable({ + providedIn: 'root', +}) +export class AlertService { + timeout = 5000; + toast = false; + position = 'top right'; + + // unique id for each alert. Starts from 0. + private alertId = 0; + private alerts: Alert[] = []; + + constructor(private sanitizer: DomSanitizer, private ngZone: NgZone) {} + + clear(): void { + this.alerts = []; + } + + get(): Alert[] { + return this.alerts; + } + + /** + * Adds alert to alerts array and returns added alert. + * @param alert Alert to add. If `timeout`, `toast` or `position` is missing then applying default value. + * @param extAlerts If missing then adding `alert` to `AlertService` internal array and alerts can be retrieved by `get()`. + * Else adding `alert` to `extAlerts`. + * @returns Added alert + */ + addAlert(alert: Alert, extAlerts?: Alert[]): Alert { + alert.id = this.alertId++; + + alert.message = this.sanitizer.sanitize(SecurityContext.HTML, alert.message ?? '') ?? ''; + alert.timeout = alert.timeout ?? this.timeout; + alert.toast = alert.toast ?? this.toast; + alert.position = alert.position ?? this.position; + alert.close = (alertsArray: Alert[]) => this.closeAlert(alert.id!, alertsArray); + + (extAlerts ?? this.alerts).push(alert); + + if (alert.timeout > 0) { + setTimeout(() => { + this.closeAlert(alert.id!, extAlerts ?? this.alerts); + }, alert.timeout); + } + + return alert; + } + + private closeAlert(alertId: number, extAlerts?: Alert[]): void { + const alerts = extAlerts ?? this.alerts; + const alertIndex = alerts.map(alert => alert.id).indexOf(alertId); + // if found alert then remove + if (alertIndex >= 0) { + alerts.splice(alertIndex, 1); + } + } +} diff --git a/src/main/webapp/app/core/util/data-util.service.spec.ts b/src/main/webapp/app/core/util/data-util.service.spec.ts new file mode 100644 index 0000000..fccbcc6 --- /dev/null +++ b/src/main/webapp/app/core/util/data-util.service.spec.ts @@ -0,0 +1,34 @@ +import { TestBed } from '@angular/core/testing'; + +import { DataUtils } from './data-util.service'; + +describe('Data Utils Service Test', () => { + let service: DataUtils; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DataUtils], + }); + service = TestBed.inject(DataUtils); + }); + + describe('byteSize', () => { + it('should return the bytesize of the text', () => { + expect(service.byteSize('Hello JHipster')).toBe(`10.5 bytes`); + }); + }); + + describe('openFile', () => { + it('should open the file in the new window', () => { + const newWindow = { ...window }; + newWindow.document.write = jest.fn(); + window.open = jest.fn(() => newWindow); + window.URL.createObjectURL = jest.fn(); + // 'JHipster' in base64 is 'SkhpcHN0ZXI=' + const data = 'SkhpcHN0ZXI='; + const contentType = 'text/plain'; + service.openFile(data, contentType); + expect(window.open).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/main/webapp/app/core/util/data-util.service.ts b/src/main/webapp/app/core/util/data-util.service.ts new file mode 100644 index 0000000..37e0387 --- /dev/null +++ b/src/main/webapp/app/core/util/data-util.service.ts @@ -0,0 +1,130 @@ +import { Injectable } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { Observable, Observer } from 'rxjs'; +import { Buffer } from 'buffer'; + +export type FileLoadErrorType = 'not.image' | 'could.not.extract'; + +export interface FileLoadError { + message: string; + key: FileLoadErrorType; + params?: any; +} + +/** + * An utility service for data. + */ +@Injectable({ + providedIn: 'root', +}) +export class DataUtils { + /** + * Method to find the byte size of the string provides + */ + byteSize(base64String: string): string { + return this.formatAsBytes(this.size(base64String)); + } + + /** + * Method to open file + */ + openFile(data: string, contentType: string | null | undefined): void { + contentType = contentType ?? ''; + + const byteCharacters = Buffer.from(data, 'base64').toString('binary'); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { + type: contentType, + }); + const fileURL = window.URL.createObjectURL(blob); + const win = window.open(fileURL); + win!.onload = function () { + URL.revokeObjectURL(fileURL); + }; + } + + /** + * Sets the base 64 data & file type of the 1st file on the event (event.target.files[0]) in the passed entity object + * and returns an observable. + * + * @param event the object containing the file (at event.target.files[0]) + * @param editForm the form group where the input field is located + * @param field the field name to set the file's 'base 64 data' on + * @param isImage boolean representing if the file represented by the event is an image + * @returns an observable that loads file to form field and completes if sussessful + * or returns error as FileLoadError on failure + */ + loadFileToForm(event: Event, editForm: FormGroup, field: string, isImage: boolean): Observable { + return new Observable((observer: Observer) => { + const eventTarget: HTMLInputElement | null = event.target as HTMLInputElement | null; + if (eventTarget?.files?.[0]) { + const file: File = eventTarget.files[0]; + if (isImage && !file.type.startsWith('image/')) { + const error: FileLoadError = { + message: `File was expected to be an image but was found to be '${file.type}'`, + key: 'not.image', + params: { fileType: file.type }, + }; + observer.error(error); + } else { + const fieldContentType: string = field + 'ContentType'; + this.toBase64(file, (base64Data: string) => { + editForm.patchValue({ + [field]: base64Data, + [fieldContentType]: file.type, + }); + observer.next(); + observer.complete(); + }); + } + } else { + const error: FileLoadError = { + message: 'Could not extract file', + key: 'could.not.extract', + params: { event }, + }; + observer.error(error); + } + }); + } + + /** + * Method to convert the file to base64 + */ + private toBase64(file: File, callback: (base64Data: string) => void): void { + const fileReader: FileReader = new FileReader(); + fileReader.onload = (e: ProgressEvent) => { + if (typeof e.target?.result === 'string') { + const base64Data: string = e.target.result.substring(e.target.result.indexOf('base64,') + 'base64,'.length); + callback(base64Data); + } + }; + fileReader.readAsDataURL(file); + } + + private endsWith(suffix: string, str: string): boolean { + return str.includes(suffix, str.length - suffix.length); + } + + private paddingSize(value: string): number { + if (this.endsWith('==', value)) { + return 2; + } + if (this.endsWith('=', value)) { + return 1; + } + return 0; + } + + private size(value: string): number { + return (value.length / 4) * 3 - this.paddingSize(value); + } + + private formatAsBytes(size: number): string { + return size.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + ' bytes'; // NOSONAR + } +} diff --git a/src/main/webapp/app/core/util/event-manager.service.spec.ts b/src/main/webapp/app/core/util/event-manager.service.spec.ts new file mode 100644 index 0000000..910009c --- /dev/null +++ b/src/main/webapp/app/core/util/event-manager.service.spec.ts @@ -0,0 +1,84 @@ +import { inject, TestBed } from '@angular/core/testing'; + +import { EventManager, EventWithContent } from './event-manager.service'; + +describe('Event Manager tests', () => { + describe('EventWithContent', () => { + it('should create correctly EventWithContent', () => { + // WHEN + const eventWithContent = new EventWithContent('name', 'content'); + + // THEN + expect(eventWithContent).toEqual({ name: 'name', content: 'content' }); + }); + }); + + describe('EventManager', () => { + let recievedEvent: EventWithContent | string | null; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [EventManager], + }); + recievedEvent = null; + }); + + it('should not fail when nosubscriber and broadcasting', inject([EventManager], (eventManager: EventManager) => { + expect(eventManager.observer).toBeUndefined(); + eventManager.broadcast({ name: 'modifier', content: 'modified something' }); + })); + + it('should create an observable and callback when broadcasted EventWithContent', inject( + [EventManager], + (eventManager: EventManager) => { + // GIVEN + eventManager.subscribe('modifier', (event: EventWithContent | string) => (recievedEvent = event)); + + // WHEN + eventManager.broadcast({ name: 'unrelatedModifier', content: 'unreleated modification' }); + // THEN + expect(recievedEvent).toBeNull(); + + // WHEN + eventManager.broadcast({ name: 'modifier', content: 'modified something' }); + // THEN + expect(recievedEvent).toEqual({ name: 'modifier', content: 'modified something' }); + } + )); + + it('should create an observable and callback when broadcasted string', inject([EventManager], (eventManager: EventManager) => { + // GIVEN + eventManager.subscribe('modifier', (event: EventWithContent | string) => (recievedEvent = event)); + + // WHEN + eventManager.broadcast('unrelatedModifier'); + // THEN + expect(recievedEvent).toBeNull(); + + // WHEN + eventManager.broadcast('modifier'); + // THEN + expect(recievedEvent).toEqual('modifier'); + })); + + it('should subscribe to multiple events', inject([EventManager], (eventManager: EventManager) => { + // GIVEN + eventManager.subscribe(['modifier', 'modifier2'], (event: EventWithContent | string) => (recievedEvent = event)); + + // WHEN + eventManager.broadcast('unrelatedModifier'); + // THEN + expect(recievedEvent).toBeNull(); + + // WHEN + eventManager.broadcast({ name: 'modifier', content: 'modified something' }); + // THEN + expect(recievedEvent).toEqual({ name: 'modifier', content: 'modified something' }); + + // WHEN + eventManager.broadcast('modifier2'); + // THEN + expect(recievedEvent).toEqual('modifier2'); + })); + }); +}); diff --git a/src/main/webapp/app/core/util/event-manager.service.ts b/src/main/webapp/app/core/util/event-manager.service.ts new file mode 100644 index 0000000..1730369 --- /dev/null +++ b/src/main/webapp/app/core/util/event-manager.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; +import { Observable, Observer, Subscription } from 'rxjs'; +import { filter, share } from 'rxjs/operators'; + +export class EventWithContent { + constructor(public name: string, public content: T) {} +} + +/** + * An utility class to manage RX events + */ +@Injectable({ + providedIn: 'root', +}) +export class EventManager { + observable: Observable | string>; + observer?: Observer | string>; + + constructor() { + this.observable = new Observable((observer: Observer | string>) => { + this.observer = observer; + }).pipe(share()); + } + + /** + * Method to broadcast the event to observer + */ + broadcast(event: EventWithContent | string): void { + if (this.observer) { + this.observer.next(event); + } + } + + /** + * Method to subscribe to an event with callback + * @param eventNames Single event name or array of event names to what subscribe + * @param callback Callback to run when the event occurs + */ + subscribe(eventNames: string | string[], callback: (event: EventWithContent | string) => void): Subscription { + if (typeof eventNames === 'string') { + eventNames = [eventNames]; + } + return this.observable + .pipe( + filter((event: EventWithContent | string) => { + for (const eventName of eventNames) { + if ((typeof event === 'string' && event === eventName) || (typeof event !== 'string' && event.name === eventName)) { + return true; + } + } + return false; + }) + ) + .subscribe(callback); + } + + /** + * Method to unsubscribe the subscription + */ + destroy(subscriber: Subscription): void { + subscriber.unsubscribe(); + } +} diff --git a/src/main/webapp/app/core/util/operators.spec.ts b/src/main/webapp/app/core/util/operators.spec.ts new file mode 100644 index 0000000..429647c --- /dev/null +++ b/src/main/webapp/app/core/util/operators.spec.ts @@ -0,0 +1,18 @@ +import { filterNaN, isPresent } from './operators'; + +describe('Operators Test', () => { + describe('isPresent', () => { + it('should remove null and undefined values', () => { + expect([1, null, undefined].filter(isPresent)).toEqual([1]); + }); + }); + + describe('filterNaN', () => { + it('should return 0 for NaN', () => { + expect(filterNaN(NaN)).toBe(0); + }); + it('should return number for a number', () => { + expect(filterNaN(12345)).toBe(12345); + }); + }); +}); diff --git a/src/main/webapp/app/core/util/operators.ts b/src/main/webapp/app/core/util/operators.ts new file mode 100644 index 0000000..c224592 --- /dev/null +++ b/src/main/webapp/app/core/util/operators.ts @@ -0,0 +1,9 @@ +/* + * Function used to workaround https://github.com/microsoft/TypeScript/issues/16069 + * es2019 alternative `const filteredArr = myArr.flatMap((x) => x ? x : []);` + */ +export function isPresent(t: T | undefined | null | void): t is T { + return t !== undefined && t !== null; +} + +export const filterNaN = (input: number): number => (isNaN(input) ? 0 : input); diff --git a/src/main/webapp/app/core/util/parse-links.service.spec.ts b/src/main/webapp/app/core/util/parse-links.service.spec.ts new file mode 100644 index 0000000..40b6c75 --- /dev/null +++ b/src/main/webapp/app/core/util/parse-links.service.spec.ts @@ -0,0 +1,36 @@ +import { inject, TestBed } from '@angular/core/testing'; + +import { ParseLinks } from './parse-links.service'; + +describe('Parse links service test', () => { + describe('Parse Links Service Test', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ParseLinks], + }); + }); + + it('should throw an error when passed an empty string', inject([ParseLinks], (service: ParseLinks) => { + expect(function () { + service.parse(''); + }).toThrow(new Error('input must not be of zero length')); + })); + + it('should throw an error when passed without comma', inject([ParseLinks], (service: ParseLinks) => { + expect(function () { + service.parse('test'); + }).toThrow(new Error('section could not be split on ";"')); + })); + + it('should throw an error when passed without semicolon', inject([ParseLinks], (service: ParseLinks) => { + expect(function () { + service.parse('test,test2'); + }).toThrow(new Error('section could not be split on ";"')); + })); + + it('should return links when headers are passed', inject([ParseLinks], (service: ParseLinks) => { + const links = { last: 0, first: 0 }; + expect(service.parse(' ; rel="last",; rel="first"')).toEqual(links); + })); + }); +}); diff --git a/src/main/webapp/app/core/util/parse-links.service.ts b/src/main/webapp/app/core/util/parse-links.service.ts new file mode 100644 index 0000000..dc1eb0e --- /dev/null +++ b/src/main/webapp/app/core/util/parse-links.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; + +/** + * An utility service for link parsing. + */ +@Injectable({ + providedIn: 'root', +}) +export class ParseLinks { + /** + * Method to parse the links + */ + parse(header: string): { [key: string]: number } { + if (header.length === 0) { + throw new Error('input must not be of zero length'); + } + + // Split parts by comma + const parts: string[] = header.split(','); + const links: { [key: string]: number } = {}; + + // Parse each part into a named link + parts.forEach(p => { + const section: string[] = p.split(';'); + + if (section.length !== 2) { + throw new Error('section could not be split on ";"'); + } + + const url: string = section[0].replace(/<(.*)>/, '$1').trim(); // NOSONAR + const queryString: { [key: string]: string | undefined } = {}; + + url.replace(/([^?=&]+)(=([^&]*))?/g, (_$0: string, $1: string | undefined, _$2: string | undefined, $3: string | undefined) => { + if ($1 !== undefined) { + queryString[$1] = $3; + } + return $3 ?? ''; + }); + + if (queryString.page !== undefined) { + const name: string = section[1].replace(/rel="(.*)"/, '$1').trim(); + links[name] = parseInt(queryString.page, 10); + } + }); + return links; + } +} diff --git a/src/main/webapp/app/entities/entity-navbar-items.ts b/src/main/webapp/app/entities/entity-navbar-items.ts new file mode 100644 index 0000000..9f96a68 --- /dev/null +++ b/src/main/webapp/app/entities/entity-navbar-items.ts @@ -0,0 +1,3 @@ +import NavbarItem from 'app/layouts/navbar/navbar-item.model'; + +export const EntityNavbarItems: NavbarItem[] = []; diff --git a/src/main/webapp/app/entities/entity-routing.module.ts b/src/main/webapp/app/entities/entity-routing.module.ts new file mode 100644 index 0000000..fe1354d --- /dev/null +++ b/src/main/webapp/app/entities/entity-routing.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + /* jhipster-needle-add-entity-route - JHipster will add entity modules routes here */ + ]), + ], +}) +export class EntityRoutingModule {} diff --git a/src/main/webapp/app/home/home.component.html b/src/main/webapp/app/home/home.component.html new file mode 100644 index 0000000..7373be6 --- /dev/null +++ b/src/main/webapp/app/home/home.component.html @@ -0,0 +1,49 @@ +
+
+ +
+ +
+

Welcome, Java Hipster! (Isdashboard)

+ +

This is your homepage

+ +
+
+ You are logged in as user "{{ account.login }}". +
+ +
+ If you want to + sign in, you can try the default accounts:
- Administrator (login="admin" and password="admin")
- User (login="user" and + password="user").
+
+
+ +

If you have any question on JHipster:

+ + + +

+ If you like JHipster, don't forget to give us a star on + GitHub! +

+
+
diff --git a/src/main/webapp/app/home/home.component.scss b/src/main/webapp/app/home/home.component.scss new file mode 100644 index 0000000..573df2d --- /dev/null +++ b/src/main/webapp/app/home/home.component.scss @@ -0,0 +1,23 @@ +/* ========================================================================== +Main page styles +========================================================================== */ + +.hipster { + display: inline-block; + width: 347px; + height: 497px; + background: url('/content/images/jhipster_family_member_2.svg') no-repeat center top; + background-size: contain; +} + +/* wait autoprefixer update to allow simple generation of high pixel density media query */ +@media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (-moz-min-device-pixel-ratio: 2), + only screen and (-o-min-device-pixel-ratio: 2/1), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 2dppx) { + .hipster { + background: url('/content/images/jhipster_family_member_2.svg') no-repeat center top; + background-size: contain; + } +} diff --git a/src/main/webapp/app/home/home.component.spec.ts b/src/main/webapp/app/home/home.component.spec.ts new file mode 100644 index 0000000..c512223 --- /dev/null +++ b/src/main/webapp/app/home/home.component.spec.ts @@ -0,0 +1,111 @@ +jest.mock('app/core/auth/account.service'); + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of, Subject } from 'rxjs'; + +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; + +import HomeComponent from './home.component'; + +describe('Home Component', () => { + let comp: HomeComponent; + let fixture: ComponentFixture; + let mockAccountService: AccountService; + let mockRouter: Router; + const account: Account = { + activated: true, + authorities: [], + email: '', + firstName: null, + langKey: '', + lastName: null, + login: 'login', + imageUrl: null, + }; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HomeComponent, RouterTestingModule.withRoutes([])], + providers: [AccountService], + }) + .overrideTemplate(HomeComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + comp = fixture.componentInstance; + mockAccountService = TestBed.inject(AccountService); + mockAccountService.identity = jest.fn(() => of(null)); + mockAccountService.getAuthenticationState = jest.fn(() => of(null)); + + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + }); + + describe('ngOnInit', () => { + it('Should synchronize account variable with current account', () => { + // GIVEN + const authenticationState = new Subject(); + mockAccountService.getAuthenticationState = jest.fn(() => authenticationState.asObservable()); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(comp.account).toBeNull(); + + // WHEN + authenticationState.next(account); + + // THEN + expect(comp.account).toEqual(account); + + // WHEN + authenticationState.next(null); + + // THEN + expect(comp.account).toBeNull(); + }); + }); + + describe('login', () => { + it('Should navigate to /login on login', () => { + // WHEN + comp.login(); + + // THEN + expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']); + }); + }); + + describe('ngOnDestroy', () => { + it('Should destroy authentication state subscription on component destroy', () => { + // GIVEN + const authenticationState = new Subject(); + mockAccountService.getAuthenticationState = jest.fn(() => authenticationState.asObservable()); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(comp.account).toBeNull(); + + // WHEN + authenticationState.next(account); + + // THEN + expect(comp.account).toEqual(account); + + // WHEN + comp.ngOnDestroy(); + authenticationState.next(null); + + // THEN + expect(comp.account).toEqual(account); + }); + }); +}); diff --git a/src/main/webapp/app/home/home.component.ts b/src/main/webapp/app/home/home.component.ts new file mode 100644 index 0000000..92c8e87 --- /dev/null +++ b/src/main/webapp/app/home/home.component.ts @@ -0,0 +1,39 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; + +@Component({ + standalone: true, + selector: 'jhi-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'], + imports: [SharedModule, RouterModule], +}) +export default class HomeComponent implements OnInit, OnDestroy { + account: Account | null = null; + + private readonly destroy$ = new Subject(); + + constructor(private accountService: AccountService, private router: Router) {} + + ngOnInit(): void { + this.accountService + .getAuthenticationState() + .pipe(takeUntil(this.destroy$)) + .subscribe(account => (this.account = account)); + } + + login(): void { + this.router.navigate(['/login']); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/src/main/webapp/app/layouts/error/error.component.html b/src/main/webapp/app/layouts/error/error.component.html new file mode 100644 index 0000000..b5356cd --- /dev/null +++ b/src/main/webapp/app/layouts/error/error.component.html @@ -0,0 +1,15 @@ +
+
+
+ +
+ +
+

Error page!

+ +
+
{{ errorMessage }}
+
+
+
+
diff --git a/src/main/webapp/app/layouts/error/error.component.ts b/src/main/webapp/app/layouts/error/error.component.ts new file mode 100644 index 0000000..802a458 --- /dev/null +++ b/src/main/webapp/app/layouts/error/error.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import SharedModule from 'app/shared/shared.module'; + +@Component({ + standalone: true, + selector: 'jhi-error', + templateUrl: './error.component.html', + imports: [SharedModule], +}) +export default class ErrorComponent implements OnInit { + errorMessage?: string; + + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + this.route.data.subscribe(routeData => { + if (routeData.errorMessage) { + this.errorMessage = routeData.errorMessage; + } + }); + } +} diff --git a/src/main/webapp/app/layouts/error/error.route.ts b/src/main/webapp/app/layouts/error/error.route.ts new file mode 100644 index 0000000..fff1cd2 --- /dev/null +++ b/src/main/webapp/app/layouts/error/error.route.ts @@ -0,0 +1,31 @@ +import { Routes } from '@angular/router'; + +import ErrorComponent from './error.component'; + +export const errorRoute: Routes = [ + { + path: 'error', + component: ErrorComponent, + title: 'Error page!', + }, + { + path: 'accessdenied', + component: ErrorComponent, + data: { + errorMessage: 'You are not authorized to access this page.', + }, + title: 'Error page!', + }, + { + path: '404', + component: ErrorComponent, + data: { + errorMessage: 'The page does not exist.', + }, + title: 'Error page!', + }, + { + path: '**', + redirectTo: '/404', + }, +]; diff --git a/src/main/webapp/app/layouts/footer/footer.component.html b/src/main/webapp/app/layouts/footer/footer.component.html new file mode 100644 index 0000000..47a5100 --- /dev/null +++ b/src/main/webapp/app/layouts/footer/footer.component.html @@ -0,0 +1,3 @@ + diff --git a/src/main/webapp/app/layouts/footer/footer.component.ts b/src/main/webapp/app/layouts/footer/footer.component.ts new file mode 100644 index 0000000..7ab0938 --- /dev/null +++ b/src/main/webapp/app/layouts/footer/footer.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'jhi-footer', + templateUrl: './footer.component.html', +}) +export default class FooterComponent {} diff --git a/src/main/webapp/app/layouts/main/main.component.html b/src/main/webapp/app/layouts/main/main.component.html new file mode 100644 index 0000000..3ac9be9 --- /dev/null +++ b/src/main/webapp/app/layouts/main/main.component.html @@ -0,0 +1,13 @@ + + +
+ +
+ +
+
+ +
+ + +
diff --git a/src/main/webapp/app/layouts/main/main.component.spec.ts b/src/main/webapp/app/layouts/main/main.component.spec.ts new file mode 100644 index 0000000..9d60656 --- /dev/null +++ b/src/main/webapp/app/layouts/main/main.component.spec.ts @@ -0,0 +1,118 @@ +jest.mock('app/core/auth/account.service'); + +import { waitForAsync, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { Router, TitleStrategy } from '@angular/router'; +import { Title } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DOCUMENT } from '@angular/common'; +import { Component } from '@angular/core'; +import { of } from 'rxjs'; + +import { AccountService } from 'app/core/auth/account.service'; + +import MainComponent from './main.component'; +import { AppPageTitleStrategy } from 'app/app-page-title-strategy'; + +describe('MainComponent', () => { + let comp: MainComponent; + let fixture: ComponentFixture; + let titleService: Title; + let mockAccountService: AccountService; + const routerState: any = { snapshot: { root: { data: {} } } }; + let router: Router; + let document: Document; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [MainComponent], + providers: [Title, AccountService, { provide: TitleStrategy, useClass: AppPageTitleStrategy }], + }) + .overrideTemplate(MainComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MainComponent); + comp = fixture.componentInstance; + titleService = TestBed.inject(Title); + mockAccountService = TestBed.inject(AccountService); + mockAccountService.identity = jest.fn(() => of(null)); + mockAccountService.getAuthenticationState = jest.fn(() => of(null)); + router = TestBed.inject(Router); + document = TestBed.inject(DOCUMENT); + }); + + describe('page title', () => { + const defaultPageTitle = 'Isdashboard'; + const parentRoutePageTitle = 'parentTitle'; + const childRoutePageTitle = 'childTitle'; + + beforeEach(() => { + routerState.snapshot.root = { data: {} }; + jest.spyOn(titleService, 'setTitle'); + comp.ngOnInit(); + }); + + describe('navigation end', () => { + it('should set page title to default title if pageTitle is missing on routes', fakeAsync(() => { + // WHEN + router.navigateByUrl(''); + tick(); + + // THEN + expect(document.title).toBe(defaultPageTitle); + })); + + it('should set page title to root route pageTitle if there is no child routes', fakeAsync(() => { + // GIVEN + router.resetConfig([{ path: '', title: parentRoutePageTitle, component: BlankComponent }]); + + // WHEN + router.navigateByUrl(''); + tick(); + + // THEN + expect(document.title).toBe(parentRoutePageTitle); + })); + + it('should set page title to child route pageTitle if child routes exist and pageTitle is set for child route', fakeAsync(() => { + // GIVEN + router.resetConfig([ + { + path: 'home', + title: parentRoutePageTitle, + children: [{ path: '', title: childRoutePageTitle, component: BlankComponent }], + }, + ]); + + // WHEN + router.navigateByUrl('home'); + tick(); + + // THEN + expect(document.title).toBe(childRoutePageTitle); + })); + + it('should set page title to parent route pageTitle if child routes exists but pageTitle is not set for child route data', fakeAsync(() => { + // GIVEN + router.resetConfig([ + { + path: 'home', + title: parentRoutePageTitle, + children: [{ path: '', component: BlankComponent }], + }, + ]); + + // WHEN + router.navigateByUrl('home'); + tick(); + + // THEN + expect(document.title).toBe(parentRoutePageTitle); + })); + }); + }); +}); + +@Component({ template: '' }) +export class BlankComponent {} diff --git a/src/main/webapp/app/layouts/main/main.component.ts b/src/main/webapp/app/layouts/main/main.component.ts new file mode 100644 index 0000000..594b7a6 --- /dev/null +++ b/src/main/webapp/app/layouts/main/main.component.ts @@ -0,0 +1,19 @@ +import { Component, OnInit } from '@angular/core'; + +import { AccountService } from 'app/core/auth/account.service'; +import { AppPageTitleStrategy } from 'app/app-page-title-strategy'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'jhi-main', + templateUrl: './main.component.html', + providers: [AppPageTitleStrategy], +}) +export default class MainComponent implements OnInit { + constructor(private router: Router, private appPageTitleStrategy: AppPageTitleStrategy, private accountService: AccountService) {} + + ngOnInit(): void { + // try to log in automatically + this.accountService.identity().subscribe(); + } +} diff --git a/src/main/webapp/app/layouts/main/main.module.ts b/src/main/webapp/app/layouts/main/main.module.ts new file mode 100644 index 0000000..b2f5676 --- /dev/null +++ b/src/main/webapp/app/layouts/main/main.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import MainComponent from './main.component'; +import FooterComponent from '../footer/footer.component'; +import PageRibbonComponent from '../profiles/page-ribbon.component'; + +@NgModule({ + imports: [SharedModule, RouterModule, FooterComponent, PageRibbonComponent], + declarations: [MainComponent], +}) +export default class MainModule {} diff --git a/src/main/webapp/app/layouts/navbar/navbar-item.model.d.ts b/src/main/webapp/app/layouts/navbar/navbar-item.model.d.ts new file mode 100644 index 0000000..0c4b493 --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/navbar-item.model.d.ts @@ -0,0 +1,6 @@ +type NavbarItem = { + name: string; + route: string; +}; + +export default NavbarItem; diff --git a/src/main/webapp/app/layouts/navbar/navbar.component.html b/src/main/webapp/app/layouts/navbar/navbar.component.html new file mode 100644 index 0000000..24ec09c --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/navbar.component.html @@ -0,0 +1,107 @@ + diff --git a/src/main/webapp/app/layouts/navbar/navbar.component.scss b/src/main/webapp/app/layouts/navbar/navbar.component.scss new file mode 100644 index 0000000..4c038a2 --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/navbar.component.scss @@ -0,0 +1,36 @@ +@import 'bootstrap/scss/functions'; +@import 'bootstrap/scss/variables'; + +/* ========================================================================== +Navbar +========================================================================== */ + +.navbar-version { + font-size: 0.65em; + color: $navbar-dark-color; +} + +.profile-image { + height: 1.75em; + width: 1.75em; +} + +.navbar { + padding: 0.2rem 1rem; + + a.nav-link { + font-weight: 400; + } +} + +/* ========================================================================== +Logo styles +========================================================================== */ +.logo-img { + height: 45px; + width: 45px; + display: inline-block; + vertical-align: middle; + background: url('/content/images/logo-jhipster.png') no-repeat center center; + background-size: contain; +} diff --git a/src/main/webapp/app/layouts/navbar/navbar.component.spec.ts b/src/main/webapp/app/layouts/navbar/navbar.component.spec.ts new file mode 100644 index 0000000..e3f84b5 --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/navbar.component.spec.ts @@ -0,0 +1,95 @@ +jest.mock('app/login/login.service'); + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; + +import { ProfileInfo } from 'app/layouts/profiles/profile-info.model'; +import { Account } from 'app/core/auth/account.model'; +import { AccountService } from 'app/core/auth/account.service'; +import { ProfileService } from 'app/layouts/profiles/profile.service'; +import { LoginService } from 'app/login/login.service'; + +import NavbarComponent from './navbar.component'; + +describe('Navbar Component', () => { + let comp: NavbarComponent; + let fixture: ComponentFixture; + let accountService: AccountService; + let profileService: ProfileService; + const account: Account = { + activated: true, + authorities: [], + email: '', + firstName: 'John', + langKey: '', + lastName: 'Doe', + login: 'john.doe', + imageUrl: '', + }; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NavbarComponent, HttpClientTestingModule, RouterTestingModule.withRoutes([])], + providers: [LoginService], + }) + .overrideTemplate(NavbarComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NavbarComponent); + comp = fixture.componentInstance; + accountService = TestBed.inject(AccountService); + profileService = TestBed.inject(ProfileService); + }); + + it('Should call profileService.getProfileInfo on init', () => { + // GIVEN + jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(new ProfileInfo())); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(profileService.getProfileInfo).toHaveBeenCalled(); + }); + + it('Should hold current authenticated user in variable account', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(comp.account).toBeNull(); + + // WHEN + accountService.authenticate(account); + + // THEN + expect(comp.account).toEqual(account); + + // WHEN + accountService.authenticate(null); + + // THEN + expect(comp.account).toBeNull(); + }); + + it('Should hold current authenticated user in variable account if user is authenticated before page load', () => { + // GIVEN + accountService.authenticate(account); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(comp.account).toEqual(account); + + // WHEN + accountService.authenticate(null); + + // THEN + expect(comp.account).toBeNull(); + }); +}); diff --git a/src/main/webapp/app/layouts/navbar/navbar.component.ts b/src/main/webapp/app/layouts/navbar/navbar.component.ts new file mode 100644 index 0000000..398326e --- /dev/null +++ b/src/main/webapp/app/layouts/navbar/navbar.component.ts @@ -0,0 +1,70 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; + +import { StateStorageService } from 'app/core/auth/state-storage.service'; +import SharedModule from 'app/shared/shared.module'; +import HasAnyAuthorityDirective from 'app/shared/auth/has-any-authority.directive'; +import { VERSION } from 'app/app.constants'; +import { Account } from 'app/core/auth/account.model'; +import { AccountService } from 'app/core/auth/account.service'; +import { LoginService } from 'app/login/login.service'; +import { ProfileService } from 'app/layouts/profiles/profile.service'; +import { EntityNavbarItems } from 'app/entities/entity-navbar-items'; +import NavbarItem from './navbar-item.model'; + +@Component({ + standalone: true, + selector: 'jhi-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'], + imports: [RouterModule, SharedModule, HasAnyAuthorityDirective], +}) +export default class NavbarComponent implements OnInit { + inProduction?: boolean; + isNavbarCollapsed = true; + openAPIEnabled?: boolean; + version = ''; + account: Account | null = null; + entitiesNavbarItems: NavbarItem[] = []; + + constructor( + private loginService: LoginService, + private accountService: AccountService, + private profileService: ProfileService, + private router: Router + ) { + if (VERSION) { + this.version = VERSION.toLowerCase().startsWith('v') ? VERSION : `v${VERSION}`; + } + } + + ngOnInit(): void { + this.entitiesNavbarItems = EntityNavbarItems; + this.profileService.getProfileInfo().subscribe(profileInfo => { + this.inProduction = profileInfo.inProduction; + this.openAPIEnabled = profileInfo.openAPIEnabled; + }); + + this.accountService.getAuthenticationState().subscribe(account => { + this.account = account; + }); + } + + collapseNavbar(): void { + this.isNavbarCollapsed = true; + } + + login(): void { + this.router.navigate(['/login']); + } + + logout(): void { + this.collapseNavbar(); + this.loginService.logout(); + this.router.navigate(['']); + } + + toggleNavbar(): void { + this.isNavbarCollapsed = !this.isNavbarCollapsed; + } +} diff --git a/src/main/webapp/app/layouts/profiles/page-ribbon.component.scss b/src/main/webapp/app/layouts/profiles/page-ribbon.component.scss new file mode 100644 index 0000000..88b0602 --- /dev/null +++ b/src/main/webapp/app/layouts/profiles/page-ribbon.component.scss @@ -0,0 +1,25 @@ +/* ========================================================================== +Developement Ribbon +========================================================================== */ +.ribbon { + background-color: rgba(170, 0, 0, 0.5); + overflow: hidden; + position: absolute; + top: 40px; + white-space: nowrap; + width: 15em; + z-index: 9999; + pointer-events: none; + opacity: 0.75; + a { + color: #fff; + display: block; + font-weight: 400; + margin: 1px 0; + padding: 10px 50px; + text-align: center; + text-decoration: none; + text-shadow: 0 0 5px #444; + pointer-events: none; + } +} diff --git a/src/main/webapp/app/layouts/profiles/page-ribbon.component.spec.ts b/src/main/webapp/app/layouts/profiles/page-ribbon.component.spec.ts new file mode 100644 index 0000000..4d5176c --- /dev/null +++ b/src/main/webapp/app/layouts/profiles/page-ribbon.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; + +import { ProfileInfo } from 'app/layouts/profiles/profile-info.model'; +import { ProfileService } from 'app/layouts/profiles/profile.service'; + +import PageRibbonComponent from './page-ribbon.component'; + +describe('Page Ribbon Component', () => { + let comp: PageRibbonComponent; + let fixture: ComponentFixture; + let profileService: ProfileService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, PageRibbonComponent], + }) + .overrideTemplate(PageRibbonComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PageRibbonComponent); + comp = fixture.componentInstance; + profileService = TestBed.inject(ProfileService); + }); + + it('Should call profileService.getProfileInfo on init', () => { + // GIVEN + jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(new ProfileInfo())); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(profileService.getProfileInfo).toHaveBeenCalled(); + }); +}); diff --git a/src/main/webapp/app/layouts/profiles/page-ribbon.component.ts b/src/main/webapp/app/layouts/profiles/page-ribbon.component.ts new file mode 100644 index 0000000..c8a00d3 --- /dev/null +++ b/src/main/webapp/app/layouts/profiles/page-ribbon.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { ProfileService } from './profile.service'; + +@Component({ + standalone: true, + selector: 'jhi-page-ribbon', + template: ` + + `, + styleUrls: ['./page-ribbon.component.scss'], + imports: [SharedModule], +}) +export default class PageRibbonComponent implements OnInit { + ribbonEnv$?: Observable; + + constructor(private profileService: ProfileService) {} + + ngOnInit(): void { + this.ribbonEnv$ = this.profileService.getProfileInfo().pipe(map(profileInfo => profileInfo.ribbonEnv)); + } +} diff --git a/src/main/webapp/app/layouts/profiles/profile-info.model.ts b/src/main/webapp/app/layouts/profiles/profile-info.model.ts new file mode 100644 index 0000000..8c769c7 --- /dev/null +++ b/src/main/webapp/app/layouts/profiles/profile-info.model.ts @@ -0,0 +1,15 @@ +export interface InfoResponse { + 'display-ribbon-on-profiles'?: string; + git?: any; + build?: any; + activeProfiles?: string[]; +} + +export class ProfileInfo { + constructor( + public activeProfiles?: string[], + public ribbonEnv?: string, + public inProduction?: boolean, + public openAPIEnabled?: boolean + ) {} +} diff --git a/src/main/webapp/app/layouts/profiles/profile.service.ts b/src/main/webapp/app/layouts/profiles/profile.service.ts new file mode 100644 index 0000000..b379d3c --- /dev/null +++ b/src/main/webapp/app/layouts/profiles/profile.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { map, shareReplay } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { ProfileInfo, InfoResponse } from './profile-info.model'; + +@Injectable({ providedIn: 'root' }) +export class ProfileService { + private infoUrl = this.applicationConfigService.getEndpointFor('management/info'); + private profileInfo$?: Observable; + + constructor(private http: HttpClient, private applicationConfigService: ApplicationConfigService) {} + + getProfileInfo(): Observable { + if (this.profileInfo$) { + return this.profileInfo$; + } + + this.profileInfo$ = this.http.get(this.infoUrl).pipe( + map((response: InfoResponse) => { + const profileInfo: ProfileInfo = { + activeProfiles: response.activeProfiles, + inProduction: response.activeProfiles?.includes('prod'), + openAPIEnabled: response.activeProfiles?.includes('api-docs'), + }; + if (response.activeProfiles && response['display-ribbon-on-profiles']) { + const displayRibbonOnProfiles = response['display-ribbon-on-profiles'].split(','); + const ribbonProfiles = displayRibbonOnProfiles.filter(profile => response.activeProfiles?.includes(profile)); + if (ribbonProfiles.length > 0) { + profileInfo.ribbonEnv = ribbonProfiles[0]; + } + } + return profileInfo; + }), + shareReplay() + ); + return this.profileInfo$; + } +} diff --git a/src/main/webapp/app/login/login.component.html b/src/main/webapp/app/login/login.component.html new file mode 100644 index 0000000..6d74ab8 --- /dev/null +++ b/src/main/webapp/app/login/login.component.html @@ -0,0 +1,47 @@ +
+
+
+

Sign in

+
+ Failed to sign in! Please check your credentials and try again. +
+
+
+ + +
+ +
+ + +
+ + + + +
+
+
+
diff --git a/src/main/webapp/app/login/login.component.spec.ts b/src/main/webapp/app/login/login.component.spec.ts new file mode 100644 index 0000000..b537b60 --- /dev/null +++ b/src/main/webapp/app/login/login.component.spec.ts @@ -0,0 +1,152 @@ +jest.mock('app/core/auth/account.service'); +jest.mock('app/login/login.service'); + +import { ElementRef } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FormBuilder } from '@angular/forms'; +import { Router, Navigation } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of, throwError } from 'rxjs'; + +import { AccountService } from 'app/core/auth/account.service'; + +import { LoginService } from './login.service'; +import LoginComponent from './login.component'; + +describe('LoginComponent', () => { + let comp: LoginComponent; + let fixture: ComponentFixture; + let mockRouter: Router; + let mockAccountService: AccountService; + let mockLoginService: LoginService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule.withRoutes([]), LoginComponent], + providers: [ + FormBuilder, + AccountService, + { + provide: LoginService, + useValue: { + login: jest.fn(() => of({})), + }, + }, + ], + }) + .overrideTemplate(LoginComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + comp = fixture.componentInstance; + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockLoginService = TestBed.inject(LoginService); + mockAccountService = TestBed.inject(AccountService); + }); + + describe('ngOnInit', () => { + it('Should call accountService.identity on Init', () => { + // GIVEN + mockAccountService.identity = jest.fn(() => of(null)); + mockAccountService.getAuthenticationState = jest.fn(() => of(null)); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(mockAccountService.identity).toHaveBeenCalled(); + }); + + it('Should call accountService.isAuthenticated on Init', () => { + // GIVEN + mockAccountService.identity = jest.fn(() => of(null)); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(mockAccountService.isAuthenticated).toHaveBeenCalled(); + }); + + it('should navigate to home page on Init if authenticated=true', () => { + // GIVEN + mockAccountService.identity = jest.fn(() => of(null)); + mockAccountService.getAuthenticationState = jest.fn(() => of(null)); + mockAccountService.isAuthenticated = () => true; + + // WHEN + comp.ngOnInit(); + + // THEN + expect(mockRouter.navigate).toHaveBeenCalledWith(['']); + }); + }); + + describe('ngAfterViewInit', () => { + it('shoult set focus to username input after the view has been initialized', () => { + // GIVEN + const node = { + focus: jest.fn(), + }; + comp.username = new ElementRef(node); + + // WHEN + comp.ngAfterViewInit(); + + // THEN + expect(node.focus).toHaveBeenCalled(); + }); + }); + + describe('login', () => { + it('should authenticate the user and navigate to home page', () => { + // GIVEN + const credentials = { + username: 'admin', + password: 'admin', + rememberMe: true, + }; + + comp.loginForm.patchValue({ + username: 'admin', + password: 'admin', + rememberMe: true, + }); + + // WHEN + comp.login(); + + // THEN + expect(comp.authenticationError).toEqual(false); + expect(mockLoginService.login).toHaveBeenCalledWith(credentials); + expect(mockRouter.navigate).toHaveBeenCalledWith(['']); + }); + + it('should authenticate the user but not navigate to home page if authentication process is already routing to cached url from localstorage', () => { + // GIVEN + jest.spyOn(mockRouter, 'getCurrentNavigation').mockReturnValue({} as Navigation); + + // WHEN + comp.login(); + + // THEN + expect(comp.authenticationError).toEqual(false); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + + it('should stay on login form and show error message on login error', () => { + // GIVEN + mockLoginService.login = jest.fn(() => throwError({})); + + // WHEN + comp.login(); + + // THEN + expect(comp.authenticationError).toEqual(true); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/login/login.component.ts b/src/main/webapp/app/login/login.component.ts new file mode 100644 index 0000000..517e7c7 --- /dev/null +++ b/src/main/webapp/app/login/login.component.ts @@ -0,0 +1,54 @@ +import { Component, ViewChild, OnInit, AfterViewInit, ElementRef } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Router, RouterModule } from '@angular/router'; + +import SharedModule from 'app/shared/shared.module'; +import { LoginService } from 'app/login/login.service'; +import { AccountService } from 'app/core/auth/account.service'; + +@Component({ + selector: 'jhi-login', + standalone: true, + imports: [SharedModule, FormsModule, ReactiveFormsModule, RouterModule], + templateUrl: './login.component.html', +}) +export default class LoginComponent implements OnInit, AfterViewInit { + @ViewChild('username', { static: false }) + username!: ElementRef; + + authenticationError = false; + + loginForm = new FormGroup({ + username: new FormControl('', { nonNullable: true, validators: [Validators.required] }), + password: new FormControl('', { nonNullable: true, validators: [Validators.required] }), + rememberMe: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), + }); + + constructor(private accountService: AccountService, private loginService: LoginService, private router: Router) {} + + ngOnInit(): void { + // if already authenticated then navigate to home page + this.accountService.identity().subscribe(() => { + if (this.accountService.isAuthenticated()) { + this.router.navigate(['']); + } + }); + } + + ngAfterViewInit(): void { + this.username.nativeElement.focus(); + } + + login(): void { + this.loginService.login(this.loginForm.getRawValue()).subscribe({ + next: () => { + this.authenticationError = false; + if (!this.router.getCurrentNavigation()) { + // There were no routing during login (eg from navigationToStoredUrl) + this.router.navigate(['']); + } + }, + error: () => (this.authenticationError = true), + }); + } +} diff --git a/src/main/webapp/app/login/login.model.ts b/src/main/webapp/app/login/login.model.ts new file mode 100644 index 0000000..422fce9 --- /dev/null +++ b/src/main/webapp/app/login/login.model.ts @@ -0,0 +1,3 @@ +export class Login { + constructor(public username: string, public password: string, public rememberMe: boolean) {} +} diff --git a/src/main/webapp/app/login/login.service.ts b/src/main/webapp/app/login/login.service.ts new file mode 100644 index 0000000..c3a4600 --- /dev/null +++ b/src/main/webapp/app/login/login.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { Account } from 'app/core/auth/account.model'; +import { AccountService } from 'app/core/auth/account.service'; +import { AuthServerProvider } from 'app/core/auth/auth-session.service'; +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { Login } from './login.model'; + +@Injectable({ providedIn: 'root' }) +export class LoginService { + constructor( + private applicationConfigService: ApplicationConfigService, + private accountService: AccountService, + private authServerProvider: AuthServerProvider + ) {} + + login(credentials: Login): Observable { + return this.authServerProvider.login(credentials).pipe(mergeMap(() => this.accountService.identity(true))); + } + + logoutUrl(): string { + return this.applicationConfigService.getEndpointFor('api/logout'); + } + + logoutInClient(): void { + this.accountService.authenticate(null); + } + + logout(): void { + this.authServerProvider.logout().subscribe({ complete: () => this.accountService.authenticate(null) }); + } +} diff --git a/src/main/webapp/app/shared/alert/alert-error.component.html b/src/main/webapp/app/shared/alert/alert-error.component.html new file mode 100644 index 0000000..76ff881 --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert-error.component.html @@ -0,0 +1,7 @@ + diff --git a/src/main/webapp/app/shared/alert/alert-error.component.spec.ts b/src/main/webapp/app/shared/alert/alert-error.component.spec.ts new file mode 100644 index 0000000..cb6de0e --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert-error.component.spec.ts @@ -0,0 +1,158 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; + +import { EventManager } from 'app/core/util/event-manager.service'; +import { Alert, AlertService } from 'app/core/util/alert.service'; + +import { AlertErrorComponent } from './alert-error.component'; + +describe('Alert Error Component', () => { + let comp: AlertErrorComponent; + let fixture: ComponentFixture; + let eventManager: EventManager; + let alertService: AlertService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [AlertErrorComponent], + providers: [EventManager, AlertService], + }) + .overrideTemplate(AlertErrorComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AlertErrorComponent); + comp = fixture.componentInstance; + eventManager = TestBed.inject(EventManager); + alertService = TestBed.inject(AlertService); + alertService.addAlert = (alert: Alert, alerts?: Alert[]) => { + if (alerts) { + alerts.push(alert); + } + return alert; + }; + }); + + describe('Error Handling', () => { + it('Should display an alert on status 0', () => { + // GIVEN + eventManager.broadcast({ name: 'isdashboardApp.httpError', content: { status: 0 } }); + // THEN + expect(comp.alerts.length).toBe(1); + expect(comp.alerts[0].message).toBe('Server not reachable'); + }); + + it('Should display an alert on status 404', () => { + // GIVEN + eventManager.broadcast({ name: 'isdashboardApp.httpError', content: { status: 404 } }); + // THEN + expect(comp.alerts.length).toBe(1); + expect(comp.alerts[0].message).toBe('Not found'); + }); + + it('Should display an alert on generic error', () => { + // GIVEN + eventManager.broadcast({ name: 'isdashboardApp.httpError', content: { error: { message: 'Error Message' } } }); + eventManager.broadcast({ name: 'isdashboardApp.httpError', content: { error: 'Second Error Message' } }); + // THEN + expect(comp.alerts.length).toBe(2); + expect(comp.alerts[0].message).toBe('Error Message'); + expect(comp.alerts[1].message).toBe('Second Error Message'); + }); + + it('Should display an alert on status 400 for generic error', () => { + // GIVEN + const response = new HttpErrorResponse({ + url: 'http://localhost:8080/api/foos', + headers: new HttpHeaders(), + status: 400, + statusText: 'Bad Request', + error: { + type: 'https://www.jhipster.tech/problem/constraint-violation', + title: 'Bad Request', + status: 400, + path: '/api/foos', + message: 'error.validation', + }, + }); + eventManager.broadcast({ name: 'isdashboardApp.httpError', content: response }); + // THEN + expect(comp.alerts.length).toBe(1); + expect(comp.alerts[0].message).toBe('error.validation'); + }); + + it('Should display an alert on status 400 for generic error without message', () => { + // GIVEN + const response = new HttpErrorResponse({ + url: 'http://localhost:8080/api/foos', + headers: new HttpHeaders(), + status: 400, + error: 'Bad Request', + }); + eventManager.broadcast({ name: 'isdashboardApp.httpError', content: response }); + // THEN + expect(comp.alerts.length).toBe(1); + expect(comp.alerts[0].message).toBe('Bad Request'); + }); + + it('Should display an alert on status 400 for invalid parameters', () => { + // GIVEN + const response = new HttpErrorResponse({ + url: 'http://localhost:8080/api/foos', + headers: new HttpHeaders(), + status: 400, + statusText: 'Bad Request', + error: { + type: 'https://www.jhipster.tech/problem/constraint-violation', + title: 'Method argument not valid', + status: 400, + path: '/api/foos', + message: 'error.validation', + fieldErrors: [{ objectName: 'foo', field: 'minField', message: 'Min' }], + }, + }); + eventManager.broadcast({ name: 'isdashboardApp.httpError', content: response }); + // THEN + expect(comp.alerts.length).toBe(1); + expect(comp.alerts[0].message).toBe('Error on field "MinField"'); + }); + + it('Should display an alert on status 400 for error headers', () => { + // GIVEN + const response = new HttpErrorResponse({ + url: 'http://localhost:8080/api/foos', + headers: new HttpHeaders().append('app-error', 'Error Message').append('app-params', 'foo'), + status: 400, + statusText: 'Bad Request', + error: { + status: 400, + message: 'error.validation', + }, + }); + eventManager.broadcast({ name: 'isdashboardApp.httpError', content: response }); + // THEN + expect(comp.alerts.length).toBe(1); + expect(comp.alerts[0].message).toBe('Error Message'); + }); + + it('Should display an alert on status 500 with detail', () => { + // GIVEN + const response = new HttpErrorResponse({ + url: 'http://localhost:8080/api/foos', + headers: new HttpHeaders(), + status: 500, + statusText: 'Internal server error', + error: { + status: 500, + message: 'error.http.500', + detail: 'Detailed error message', + }, + }); + eventManager.broadcast({ name: 'isdashboardApp.httpError', content: response }); + // THEN + expect(comp.alerts.length).toBe(1); + expect(comp.alerts[0].message).toBe('Detailed error message'); + }); + }); +}); diff --git a/src/main/webapp/app/shared/alert/alert-error.component.ts b/src/main/webapp/app/shared/alert/alert-error.component.ts new file mode 100644 index 0000000..aad8ac4 --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert-error.component.ts @@ -0,0 +1,99 @@ +import { Component, OnDestroy } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Subscription } from 'rxjs'; +import { CommonModule } from '@angular/common'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { AlertError } from './alert-error.model'; +import { Alert, AlertService } from 'app/core/util/alert.service'; +import { EventManager, EventWithContent } from 'app/core/util/event-manager.service'; + +@Component({ + standalone: true, + selector: 'jhi-alert-error', + templateUrl: './alert-error.component.html', + imports: [CommonModule, NgbModule], +}) +export class AlertErrorComponent implements OnDestroy { + alerts: Alert[] = []; + errorListener: Subscription; + httpErrorListener: Subscription; + + constructor(private alertService: AlertService, private eventManager: EventManager) { + this.errorListener = eventManager.subscribe('isdashboardApp.error', (response: EventWithContent | string) => { + const errorResponse = (response as EventWithContent).content; + this.addErrorAlert(errorResponse.message); + }); + + this.httpErrorListener = eventManager.subscribe('isdashboardApp.httpError', (response: EventWithContent | string) => { + const httpErrorResponse = (response as EventWithContent).content; + switch (httpErrorResponse.status) { + // connection refused, server not reachable + case 0: + this.addErrorAlert('Server not reachable'); + break; + + case 400: { + const arr = httpErrorResponse.headers.keys(); + let errorHeader: string | null = null; + for (const entry of arr) { + if (entry.toLowerCase().endsWith('app-error')) { + errorHeader = httpErrorResponse.headers.get(entry); + } + } + if (errorHeader) { + this.addErrorAlert(errorHeader); + } else if (httpErrorResponse.error !== '' && httpErrorResponse.error.fieldErrors) { + const fieldErrors = httpErrorResponse.error.fieldErrors; + for (const fieldError of fieldErrors) { + if (['Min', 'Max', 'DecimalMin', 'DecimalMax'].includes(fieldError.message)) { + fieldError.message = 'Size'; + } + // convert 'something[14].other[4].id' to 'something[].other[].id' so translations can be written to it + const convertedField: string = fieldError.field.replace(/\[\d*\]/g, '[]'); + const fieldName: string = convertedField.charAt(0).toUpperCase() + convertedField.slice(1); + this.addErrorAlert(`Error on field "${fieldName}"`); + } + } else if (httpErrorResponse.error !== '' && httpErrorResponse.error.message) { + this.addErrorAlert(httpErrorResponse.error.detail ?? httpErrorResponse.error.message); + } else { + this.addErrorAlert(httpErrorResponse.error); + } + break; + } + + case 404: + this.addErrorAlert('Not found'); + break; + + default: + if (httpErrorResponse.error !== '' && httpErrorResponse.error.message) { + this.addErrorAlert(httpErrorResponse.error.detail ?? httpErrorResponse.error.message); + } else { + this.addErrorAlert(httpErrorResponse.error); + } + } + }); + } + + setClasses(alert: Alert): { [key: string]: boolean } { + const classes = { 'jhi-toast': Boolean(alert.toast) }; + if (alert.position) { + return { ...classes, [alert.position]: true }; + } + return classes; + } + + ngOnDestroy(): void { + this.eventManager.destroy(this.errorListener); + this.eventManager.destroy(this.httpErrorListener); + } + + close(alert: Alert): void { + alert.close?.(this.alerts); + } + + private addErrorAlert(message?: string): void { + this.alertService.addAlert({ type: 'danger', message }, this.alerts); + } +} diff --git a/src/main/webapp/app/shared/alert/alert-error.model.ts b/src/main/webapp/app/shared/alert/alert-error.model.ts new file mode 100644 index 0000000..2b8cb8f --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert-error.model.ts @@ -0,0 +1,3 @@ +export class AlertError { + constructor(public message: string) {} +} diff --git a/src/main/webapp/app/shared/alert/alert.component.html b/src/main/webapp/app/shared/alert/alert.component.html new file mode 100644 index 0000000..76ff881 --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert.component.html @@ -0,0 +1,7 @@ + diff --git a/src/main/webapp/app/shared/alert/alert.component.spec.ts b/src/main/webapp/app/shared/alert/alert.component.spec.ts new file mode 100644 index 0000000..79fe41f --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert.component.spec.ts @@ -0,0 +1,44 @@ +jest.mock('app/core/util/alert.service'); + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { AlertService } from 'app/core/util/alert.service'; + +import { AlertComponent } from './alert.component'; + +describe('Alert Component', () => { + let comp: AlertComponent; + let fixture: ComponentFixture; + let mockAlertService: AlertService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [AlertComponent], + providers: [AlertService], + }) + .overrideTemplate(AlertComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AlertComponent); + comp = fixture.componentInstance; + mockAlertService = TestBed.inject(AlertService); + }); + + it('Should call alertService.get on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(mockAlertService.get).toHaveBeenCalled(); + }); + + it('Should call alertService.clear on destroy', () => { + // WHEN + comp.ngOnDestroy(); + + // THEN + expect(mockAlertService.clear).toHaveBeenCalled(); + }); +}); diff --git a/src/main/webapp/app/shared/alert/alert.component.ts b/src/main/webapp/app/shared/alert/alert.component.ts new file mode 100644 index 0000000..098a90f --- /dev/null +++ b/src/main/webapp/app/shared/alert/alert.component.ts @@ -0,0 +1,37 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { AlertService, Alert } from 'app/core/util/alert.service'; + +@Component({ + standalone: true, + selector: 'jhi-alert', + templateUrl: './alert.component.html', + imports: [CommonModule, NgbModule], +}) +export class AlertComponent implements OnInit, OnDestroy { + alerts: Alert[] = []; + + constructor(private alertService: AlertService) {} + + ngOnInit(): void { + this.alerts = this.alertService.get(); + } + + setClasses(alert: Alert): { [key: string]: boolean } { + const classes = { 'jhi-toast': Boolean(alert.toast) }; + if (alert.position) { + return { ...classes, [alert.position]: true }; + } + return classes; + } + + ngOnDestroy(): void { + this.alertService.clear(); + } + + close(alert: Alert): void { + alert.close?.(this.alerts); + } +} diff --git a/src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts b/src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts new file mode 100644 index 0000000..3b8c21a --- /dev/null +++ b/src/main/webapp/app/shared/auth/has-any-authority.directive.spec.ts @@ -0,0 +1,131 @@ +jest.mock('app/core/auth/account.service'); + +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { Subject } from 'rxjs'; + +import { AccountService } from 'app/core/auth/account.service'; +import { Account } from 'app/core/auth/account.model'; + +import HasAnyAuthorityDirective from './has-any-authority.directive'; + +@Component({ + template: `
`, +}) +class TestHasAnyAuthorityDirectiveComponent { + @ViewChild('content', { static: false }) + content?: ElementRef; +} + +describe('HasAnyAuthorityDirective tests', () => { + let mockAccountService: AccountService; + const authenticationState = new Subject(); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HasAnyAuthorityDirective], + declarations: [TestHasAnyAuthorityDirectiveComponent], + providers: [AccountService], + }); + })); + + beforeEach(() => { + mockAccountService = TestBed.inject(AccountService); + mockAccountService.getAuthenticationState = jest.fn(() => authenticationState.asObservable()); + }); + + describe('set jhiHasAnyAuthority', () => { + it('should show restricted content to user if user has required role', () => { + // GIVEN + mockAccountService.hasAnyAuthority = jest.fn(() => true); + const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); + const comp = fixture.componentInstance; + + // WHEN + fixture.detectChanges(); + + // THEN + expect(comp.content).toBeDefined(); + }); + + it('should not show restricted content to user if user has not required role', () => { + // GIVEN + mockAccountService.hasAnyAuthority = jest.fn(() => false); + const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); + const comp = fixture.componentInstance; + + // WHEN + fixture.detectChanges(); + + // THEN + expect(comp.content).toBeUndefined(); + }); + }); + + describe('change authorities', () => { + it('should show or not show restricted content correctly if user authorities are changing', () => { + // GIVEN + mockAccountService.hasAnyAuthority = jest.fn(() => true); + const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); + const comp = fixture.componentInstance; + + // WHEN + fixture.detectChanges(); + + // THEN + expect(comp.content).toBeDefined(); + + // GIVEN + mockAccountService.hasAnyAuthority = jest.fn(() => false); + + // WHEN + authenticationState.next(null); + fixture.detectChanges(); + + // THEN + expect(comp.content).toBeUndefined(); + + // GIVEN + mockAccountService.hasAnyAuthority = jest.fn(() => true); + + // WHEN + authenticationState.next(null); + fixture.detectChanges(); + + // THEN + expect(comp.content).toBeDefined(); + }); + }); + + describe('ngOnDestroy', () => { + it('should destroy authentication state subscription on component destroy', () => { + // GIVEN + mockAccountService.hasAnyAuthority = jest.fn(() => true); + const fixture = TestBed.createComponent(TestHasAnyAuthorityDirectiveComponent); + const div = fixture.debugElement.queryAllNodes(By.directive(HasAnyAuthorityDirective))[0]; + const hasAnyAuthorityDirective = div.injector.get(HasAnyAuthorityDirective); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(mockAccountService.hasAnyAuthority).toHaveBeenCalled(); + + // WHEN + jest.clearAllMocks(); + authenticationState.next(null); + + // THEN + expect(mockAccountService.hasAnyAuthority).toHaveBeenCalled(); + + // WHEN + jest.clearAllMocks(); + hasAnyAuthorityDirective.ngOnDestroy(); + authenticationState.next(null); + + // THEN + expect(mockAccountService.hasAnyAuthority).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main/webapp/app/shared/auth/has-any-authority.directive.ts b/src/main/webapp/app/shared/auth/has-any-authority.directive.ts new file mode 100644 index 0000000..b7be41e --- /dev/null +++ b/src/main/webapp/app/shared/auth/has-any-authority.directive.ts @@ -0,0 +1,54 @@ +import { Directive, Input, TemplateRef, ViewContainerRef, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { AccountService } from 'app/core/auth/account.service'; + +/** + * @whatItDoes Conditionally includes an HTML element if current user has any + * of the authorities passed as the `expression`. + * + * @howToUse + * ``` + * ... + * + * ... + * ``` + */ +@Directive({ + standalone: true, + selector: '[jhiHasAnyAuthority]', +}) +export default class HasAnyAuthorityDirective implements OnDestroy { + private authorities!: string | string[]; + + private readonly destroy$ = new Subject(); + + constructor(private accountService: AccountService, private templateRef: TemplateRef, private viewContainerRef: ViewContainerRef) {} + + @Input() + set jhiHasAnyAuthority(value: string | string[]) { + this.authorities = value; + this.updateView(); + // Get notified each time authentication state changes. + this.accountService + .getAuthenticationState() + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.updateView(); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + private updateView(): void { + const hasAnyAuthority = this.accountService.hasAnyAuthority(this.authorities); + this.viewContainerRef.clear(); + if (hasAnyAuthority) { + this.viewContainerRef.createEmbeddedView(this.templateRef); + } + } +} diff --git a/src/main/webapp/app/shared/date/duration.pipe.ts b/src/main/webapp/app/shared/date/duration.pipe.ts new file mode 100644 index 0000000..fda99e3 --- /dev/null +++ b/src/main/webapp/app/shared/date/duration.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import dayjs from 'dayjs/esm'; + +@Pipe({ + standalone: true, + name: 'duration', +}) +export default class DurationPipe implements PipeTransform { + transform(value: any): string { + if (value) { + return dayjs.duration(value).humanize(); + } + return ''; + } +} diff --git a/src/main/webapp/app/shared/date/format-medium-date.pipe.spec.ts b/src/main/webapp/app/shared/date/format-medium-date.pipe.spec.ts new file mode 100644 index 0000000..bdb618e --- /dev/null +++ b/src/main/webapp/app/shared/date/format-medium-date.pipe.spec.ts @@ -0,0 +1,19 @@ +import dayjs from 'dayjs/esm'; + +import FormatMediumDatePipe from './format-medium-date.pipe'; + +describe('FormatMediumDatePipe', () => { + const formatMediumDatePipe = new FormatMediumDatePipe(); + + it('should return an empty string when receive undefined', () => { + expect(formatMediumDatePipe.transform(undefined)).toBe(''); + }); + + it('should return an empty string when receive null', () => { + expect(formatMediumDatePipe.transform(null)).toBe(''); + }); + + it('should format date like this D MMM YYYY', () => { + expect(formatMediumDatePipe.transform(dayjs('2020-11-16').locale('fr'))).toBe('16 Nov 2020'); + }); +}); diff --git a/src/main/webapp/app/shared/date/format-medium-date.pipe.ts b/src/main/webapp/app/shared/date/format-medium-date.pipe.ts new file mode 100644 index 0000000..96b679b --- /dev/null +++ b/src/main/webapp/app/shared/date/format-medium-date.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import dayjs from 'dayjs/esm'; + +@Pipe({ + standalone: true, + name: 'formatMediumDate', +}) +export default class FormatMediumDatePipe implements PipeTransform { + transform(day: dayjs.Dayjs | null | undefined): string { + return day ? day.format('D MMM YYYY') : ''; + } +} diff --git a/src/main/webapp/app/shared/date/format-medium-datetime.pipe.spec.ts b/src/main/webapp/app/shared/date/format-medium-datetime.pipe.spec.ts new file mode 100644 index 0000000..c08aa47 --- /dev/null +++ b/src/main/webapp/app/shared/date/format-medium-datetime.pipe.spec.ts @@ -0,0 +1,19 @@ +import dayjs from 'dayjs/esm'; + +import FormatMediumDatetimePipe from './format-medium-datetime.pipe'; + +describe('FormatMediumDatePipe', () => { + const formatMediumDatetimePipe = new FormatMediumDatetimePipe(); + + it('should return an empty string when receive undefined', () => { + expect(formatMediumDatetimePipe.transform(undefined)).toBe(''); + }); + + it('should return an empty string when receive null', () => { + expect(formatMediumDatetimePipe.transform(null)).toBe(''); + }); + + it('should format date like this D MMM YYYY', () => { + expect(formatMediumDatetimePipe.transform(dayjs('2020-11-16').locale('fr'))).toBe('16 Nov 2020 00:00:00'); + }); +}); diff --git a/src/main/webapp/app/shared/date/format-medium-datetime.pipe.ts b/src/main/webapp/app/shared/date/format-medium-datetime.pipe.ts new file mode 100644 index 0000000..bd09cfb --- /dev/null +++ b/src/main/webapp/app/shared/date/format-medium-datetime.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import dayjs from 'dayjs/esm'; + +@Pipe({ + standalone: true, + name: 'formatMediumDatetime', +}) +export default class FormatMediumDatetimePipe implements PipeTransform { + transform(day: dayjs.Dayjs | null | undefined): string { + return day ? day.format('D MMM YYYY HH:mm:ss') : ''; + } +} diff --git a/src/main/webapp/app/shared/date/index.ts b/src/main/webapp/app/shared/date/index.ts new file mode 100644 index 0000000..5372ce8 --- /dev/null +++ b/src/main/webapp/app/shared/date/index.ts @@ -0,0 +1,3 @@ +export { default as DurationPipe } from './duration.pipe'; +export { default as FormatMediumDatePipe } from './format-medium-date.pipe'; +export { default as FormatMediumDatetimePipe } from './format-medium-datetime.pipe'; diff --git a/src/main/webapp/app/shared/filter/filter.component.html b/src/main/webapp/app/shared/filter/filter.component.html new file mode 100644 index 0000000..8862d00 --- /dev/null +++ b/src/main/webapp/app/shared/filter/filter.component.html @@ -0,0 +1,12 @@ +
+ Following filters are set + +
    + +
  • + {{ filterOption.name }}: {{ value }} + +
  • +
    +
+
diff --git a/src/main/webapp/app/shared/filter/filter.component.ts b/src/main/webapp/app/shared/filter/filter.component.ts new file mode 100644 index 0000000..e4d76af --- /dev/null +++ b/src/main/webapp/app/shared/filter/filter.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; +import { IFilterOptions } from './filter.model'; +import SharedModule from '../shared.module'; + +@Component({ + selector: 'jhi-filter', + standalone: true, + imports: [SharedModule], + templateUrl: './filter.component.html', +}) +export default class FilterComponent { + @Input() filters!: IFilterOptions; + + clearAllFilters(): void { + this.filters.clear(); + } + + clearFilter(filterName: string, value: string): void { + this.filters.removeFilter(filterName, value); + } +} diff --git a/src/main/webapp/app/shared/filter/filter.model.spec.ts b/src/main/webapp/app/shared/filter/filter.model.spec.ts new file mode 100644 index 0000000..6c42c91 --- /dev/null +++ b/src/main/webapp/app/shared/filter/filter.model.spec.ts @@ -0,0 +1,242 @@ +import { convertToParamMap, ParamMap, Params } from '@angular/router'; +import { FilterOptions, FilterOption } from './filter.model'; + +describe('FilterModel Tests', () => { + describe('FilterOption', () => { + let filterOption: FilterOption; + + beforeEach(() => { + filterOption = new FilterOption('foo', ['bar', 'bar2']); + }); + + it('nameAsQueryParam returns query key', () => { + expect(filterOption.nameAsQueryParam()).toEqual('filter[foo]'); + }); + + describe('addValue', () => { + it('adds multiples unique values and returns true', () => { + const ret = filterOption.addValue('bar2', 'bar3', 'bar4'); + expect(filterOption.values).toMatchObject(['bar', 'bar2', 'bar3', 'bar4']); + expect(ret).toBe(true); + }); + it("doesn't adds duplicated values and return false", () => { + const ret = filterOption.addValue('bar', 'bar2'); + expect(filterOption.values).toMatchObject(['bar', 'bar2']); + expect(ret).toBe(false); + }); + }); + + describe('removeValue', () => { + it('removes the exiting value and return true', () => { + const ret = filterOption.removeValue('bar'); + expect(filterOption.values).toMatchObject(['bar2']); + expect(ret).toBe(true); + }); + it("doesn't removes the value and return false", () => { + const ret = filterOption.removeValue('foo'); + expect(filterOption.values).toMatchObject(['bar', 'bar2']); + expect(ret).toBe(false); + }); + }); + + describe('equals', () => { + it('returns true to matching options', () => { + const otherFilterOption = new FilterOption(filterOption.name, filterOption.values.concat()); + expect(filterOption.equals(otherFilterOption)).toBe(true); + expect(otherFilterOption.equals(filterOption)).toBe(true); + }); + it('returns false to different name', () => { + const otherFilterOption = new FilterOption('bar', filterOption.values.concat()); + expect(filterOption.equals(otherFilterOption)).toBe(false); + expect(otherFilterOption.equals(filterOption)).toBe(false); + }); + it('returns false to different values', () => { + const otherFilterOption = new FilterOption('bar', []); + expect(filterOption.equals(otherFilterOption)).toBe(false); + expect(otherFilterOption.equals(filterOption)).toBe(false); + }); + }); + }); + + describe('FilterOptions', () => { + describe('hasAnyFilterSet', () => { + it('with empty options returns false', () => { + const filters = new FilterOptions(); + expect(filters.hasAnyFilterSet()).toBe(false); + }); + it('with options and empty values returns false', () => { + const filters = new FilterOptions([new FilterOption('foo'), new FilterOption('bar')]); + expect(filters.hasAnyFilterSet()).toBe(false); + }); + it('with option and value returns true', () => { + const filters = new FilterOptions([new FilterOption('foo', ['bar'])]); + expect(filters.hasAnyFilterSet()).toBe(true); + }); + }); + + describe('clear', () => { + it("removes empty filters and dosn't emit next element", () => { + const filters = new FilterOptions([new FilterOption('foo'), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + filters.clear(); + + expect(filters.filterChanges.next).not.toBeCalled(); + expect(filters.filterOptions).toMatchObject([]); + }); + it('removes empty filters and emits next element', () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + filters.clear(); + + expect(filters.filterChanges.next).toHaveBeenCalledTimes(1); + expect(filters.filterOptions).toMatchObject([]); + }); + }); + + describe('addFilter', () => { + it('adds a non existing FilterOption, returns true and emit next element', () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.addFilter('addedFilter', 'addedValue'); + + expect(result).toBe(true); + expect(filters.filterChanges.next).toHaveBeenCalledTimes(1); + expect(filters.filterOptions).toMatchObject([ + { name: 'foo', values: ['existingFoo1', 'existingFoo2'] }, + { name: 'addedFilter', values: ['addedValue'] }, + ]); + }); + it('adds a non existing value to FilterOption, returns true and emit next element', () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.addFilter('foo', 'addedValue1', 'addedValue2'); + + expect(result).toBe(true); + expect(filters.filterChanges.next).toHaveBeenCalledTimes(1); + expect(filters.filterOptions).toMatchObject([ + { name: 'foo', values: ['existingFoo1', 'existingFoo2', 'addedValue1', 'addedValue2'] }, + ]); + }); + it("doesn't add FilterOption values already added, returns false and doesn't emit next element", () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.addFilter('foo', 'existingFoo1', 'existingFoo2'); + + expect(result).toBe(false); + expect(filters.filterChanges.next).not.toBeCalled(); + expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo1', 'existingFoo2'] }]); + }); + }); + + describe('removeFilter', () => { + it('removes an existing FilterOptions and returns true', () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.removeFilter('foo', 'existingFoo1'); + + expect(result).toBe(true); + expect(filters.filterChanges.next).toHaveBeenCalledTimes(1); + expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo2'] }]); + }); + it("doesn't remove a non existing FilterOptions values returns false", () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.removeFilter('foo', 'nonExisting1'); + + expect(result).toBe(false); + expect(filters.filterChanges.next).not.toBeCalled(); + expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo1', 'existingFoo2'] }]); + }); + it("doesn't remove a non existing FilterOptions returns false", () => { + const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]); + jest.spyOn(filters.filterChanges, 'next'); + + const result = filters.removeFilter('nonExisting', 'nonExisting1'); + + expect(result).toBe(false); + expect(filters.filterChanges.next).not.toBeCalled(); + expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo1', 'existingFoo2'] }]); + }); + }); + + describe('initializeFromParams', () => { + const oneValidParam: Params = { + test: 'blub', + 'filter[hello.in]': 'world', + 'filter[invalid': 'invalid', + filter_invalid2: 'invalid', + }; + + const noValidParam: Params = { + test: 'blub', + 'filter[invalid': 'invalid', + filter_invalid2: 'invalid', + }; + + const paramWithTwoValues: Params = { + 'filter[hello.in]': ['world', 'world2'], + }; + + const paramWithTwoKeys: Params = { + 'filter[hello.in]': ['world', 'world2'], + 'filter[hello.notIn]': ['world3', 'world4'], + }; + + it('should parse from Params if there are any and not emit next element', () => { + const filters: FilterOptions = new FilterOptions([new FilterOption('foo', ['bar'])]); + jest.spyOn(filters.filterChanges, 'next'); + const paramMap: ParamMap = convertToParamMap(oneValidParam); + + filters.initializeFromParams(paramMap); + + expect(filters.filterChanges.next).not.toHaveBeenCalled(); + expect(filters.filterOptions).toMatchObject([{ name: 'hello.in', values: ['world'] }]); + }); + + it('should parse from Params and have none if there are none', () => { + const filters: FilterOptions = new FilterOptions(); + const paramMap: ParamMap = convertToParamMap(noValidParam); + jest.spyOn(filters.filterChanges, 'next'); + + filters.initializeFromParams(paramMap); + + expect(filters.filterChanges.next).not.toHaveBeenCalled(); + expect(filters.filterOptions).toMatchObject([]); + }); + + it('should parse from Params and have a parameter with 2 values and one aditional value', () => { + const filters: FilterOptions = new FilterOptions([new FilterOption('hello.in', ['world'])]); + jest.spyOn(filters.filterChanges, 'next'); + + const paramMap: ParamMap = convertToParamMap(paramWithTwoValues); + + filters.initializeFromParams(paramMap); + + expect(filters.filterChanges.next).not.toHaveBeenCalled(); + expect(filters.filterOptions).toMatchObject([{ name: 'hello.in', values: ['world', 'world2'] }]); + }); + + it('should parse from Params and have a parameter with 2 keys', () => { + const filters: FilterOptions = new FilterOptions(); + jest.spyOn(filters.filterChanges, 'next'); + + const paramMap: ParamMap = convertToParamMap(paramWithTwoKeys); + + filters.initializeFromParams(paramMap); + + expect(filters.filterChanges.next).not.toHaveBeenCalled(); + expect(filters.filterOptions).toMatchObject([ + { name: 'hello.in', values: ['world', 'world2'] }, + { name: 'hello.notIn', values: ['world3', 'world4'] }, + ]); + }); + }); + }); +}); diff --git a/src/main/webapp/app/shared/filter/filter.model.ts b/src/main/webapp/app/shared/filter/filter.model.ts new file mode 100644 index 0000000..5afc648 --- /dev/null +++ b/src/main/webapp/app/shared/filter/filter.model.ts @@ -0,0 +1,156 @@ +import { ParamMap } from '@angular/router'; +import { Subject } from 'rxjs'; + +export interface IFilterOptions { + readonly filterChanges: Subject; + get filterOptions(): IFilterOption[]; + hasAnyFilterSet(): boolean; + clear(): boolean; + initializeFromParams(params: ParamMap): boolean; + addFilter(name: string, ...values: string[]): boolean; + removeFilter(name: string, value: string): boolean; +} + +export interface IFilterOption { + name: string; + values: string[]; + nameAsQueryParam(): string; +} + +export class FilterOption implements IFilterOption { + constructor(public name: string, public values: string[] = []) { + this.values = [...new Set(values)]; + } + + nameAsQueryParam(): string { + return 'filter[' + this.name + ']'; + } + + isSet(): boolean { + return this.values.length > 0; + } + + addValue(...values: string[]): boolean { + const missingValues = values.filter(value => value && !this.values.includes(value)); + if (missingValues.length > 0) { + this.values.push(...missingValues); + return true; + } + return false; + } + + removeValue(value: string): boolean { + const indexOf = this.values.indexOf(value); + if (indexOf === -1) { + return false; + } + + this.values.splice(indexOf, 1); + return true; + } + + clone(): FilterOption { + return new FilterOption(this.name, this.values.concat()); + } + + equals(other: IFilterOption): boolean { + return ( + this.name === other.name && + this.values.length === other.values.length && + this.values.every(thisValue => other.values.includes(thisValue)) && + other.values.every(otherValue => this.values.includes(otherValue)) + ); + } +} + +export class FilterOptions implements IFilterOptions { + readonly filterChanges: Subject = new Subject(); + private _filterOptions: FilterOption[]; + + constructor(filterOptions: FilterOption[] = []) { + this._filterOptions = filterOptions; + } + + get filterOptions(): FilterOption[] { + return this._filterOptions.filter(option => option.isSet()); + } + + hasAnyFilterSet(): boolean { + return this._filterOptions.some(e => e.isSet()); + } + + clear(): boolean { + const hasFields = this.hasAnyFilterSet(); + this._filterOptions = []; + if (hasFields) { + this.changed(); + } + return hasFields; + } + + initializeFromParams(params: ParamMap): boolean { + const oldFilters: FilterOptions = this.clone(); + + this._filterOptions = []; + + const filterRegex = /filter\[(.+)\]/; + params.keys + .filter(paramKey => filterRegex.test(paramKey)) + .forEach(matchingParam => { + const matches = filterRegex.exec(matchingParam); + if (matches && matches.length > 1) { + this.getFilterOptionByName(matches[1], true).addValue(...params.getAll(matchingParam)); + } + }); + + if (oldFilters.equals(this)) { + return false; + } + return true; + } + + addFilter(name: string, ...values: string[]): boolean { + if (this.getFilterOptionByName(name, true).addValue(...values)) { + this.changed(); + return true; + } + return false; + } + + removeFilter(name: string, value: string): boolean { + if (this.getFilterOptionByName(name)?.removeValue(value)) { + this.changed(); + return true; + } + return false; + } + + protected changed(): void { + this.filterChanges.next(this.filterOptions.map(option => option.clone())); + } + + protected equals(other: FilterOptions): boolean { + const thisFilters = this.filterOptions; + const otherFilters = other.filterOptions; + if (thisFilters.length !== otherFilters.length) { + return false; + } + return thisFilters.every(option => other.getFilterOptionByName(option.name)?.equals(option)); + } + + protected clone(): FilterOptions { + return new FilterOptions(this.filterOptions.map(option => new FilterOption(option.name, option.values.concat()))); + } + + protected getFilterOptionByName(name: string, add: true): FilterOption; + protected getFilterOptionByName(name: string, add: false): FilterOption | null; + protected getFilterOptionByName(name: string): FilterOption | null; + protected getFilterOptionByName(name: string, add = false): FilterOption | null { + const addOption = (option: FilterOption): FilterOption => { + this._filterOptions.push(option); + return option; + }; + + return this._filterOptions.find(thisOption => thisOption.name === name) ?? (add ? addOption(new FilterOption(name)) : null); + } +} diff --git a/src/main/webapp/app/shared/filter/index.ts b/src/main/webapp/app/shared/filter/index.ts new file mode 100644 index 0000000..ae0af5a --- /dev/null +++ b/src/main/webapp/app/shared/filter/index.ts @@ -0,0 +1,2 @@ +export { default as FilterComponent } from './filter.component'; +export * from './filter.model'; diff --git a/src/main/webapp/app/shared/pagination/index.ts b/src/main/webapp/app/shared/pagination/index.ts new file mode 100644 index 0000000..395ed88 --- /dev/null +++ b/src/main/webapp/app/shared/pagination/index.ts @@ -0,0 +1 @@ +export { default as ItemCountComponent } from './item-count.component'; diff --git a/src/main/webapp/app/shared/pagination/item-count.component.spec.ts b/src/main/webapp/app/shared/pagination/item-count.component.spec.ts new file mode 100644 index 0000000..7a24ba8 --- /dev/null +++ b/src/main/webapp/app/shared/pagination/item-count.component.spec.ts @@ -0,0 +1,64 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; + +import ItemCountComponent from './item-count.component'; + +describe('ItemCountComponent test', () => { + let comp: ItemCountComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ItemCountComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemCountComponent); + comp = fixture.componentInstance; + }); + + describe('UI logic tests', () => { + it('should initialize with undefined', () => { + expect(comp.first).toBeUndefined(); + expect(comp.second).toBeUndefined(); + expect(comp.total).toBeUndefined(); + }); + + it('should set calculated numbers to undefined if the page value is not yet defined', () => { + // GIVEN + comp.params = { page: undefined, totalItems: 0, itemsPerPage: 10 }; + + // THEN + expect(comp.first).toBeUndefined(); + expect(comp.second).toBeUndefined(); + }); + + it('should change the content on page change', () => { + // GIVEN + comp.params = { page: 1, totalItems: 100, itemsPerPage: 10 }; + + // THEN + expect(comp.first).toBe(1); + expect(comp.second).toBe(10); + expect(comp.total).toBe(100); + + // GIVEN + comp.params = { page: 2, totalItems: 100, itemsPerPage: 10 }; + + // THEN + expect(comp.first).toBe(11); + expect(comp.second).toBe(20); + expect(comp.total).toBe(100); + }); + + it('should set the second number to totalItems if this is the last page which contains less than itemsPerPage items', () => { + // GIVEN + comp.params = { page: 2, totalItems: 16, itemsPerPage: 10 }; + + // THEN + expect(comp.first).toBe(11); + expect(comp.second).toBe(16); + expect(comp.total).toBe(16); + }); + }); +}); diff --git a/src/main/webapp/app/shared/pagination/item-count.component.ts b/src/main/webapp/app/shared/pagination/item-count.component.ts new file mode 100644 index 0000000..cab4904 --- /dev/null +++ b/src/main/webapp/app/shared/pagination/item-count.component.ts @@ -0,0 +1,32 @@ +import { Component, Input } from '@angular/core'; + +/** + * A component that will take care of item count statistics of a pagination. + */ +@Component({ + standalone: true, + selector: 'jhi-item-count', + template: `
Showing {{ first }} - {{ second }} of {{ total }} items.
`, +}) +export default class ItemCountComponent { + /** + * @param params Contains parameters for component: + * page Current page number + * totalItems Total number of items + * itemsPerPage Number of items per page + */ + @Input() set params(params: { page?: number; totalItems?: number; itemsPerPage?: number }) { + if (params.page && params.totalItems !== undefined && params.itemsPerPage) { + this.first = (params.page - 1) * params.itemsPerPage + 1; + this.second = params.page * params.itemsPerPage < params.totalItems ? params.page * params.itemsPerPage : params.totalItems; + } else { + this.first = undefined; + this.second = undefined; + } + this.total = params.totalItems; + } + + first?: number; + second?: number; + total?: number; +} diff --git a/src/main/webapp/app/shared/shared.module.ts b/src/main/webapp/app/shared/shared.module.ts new file mode 100644 index 0000000..25590b2 --- /dev/null +++ b/src/main/webapp/app/shared/shared.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { AlertComponent } from './alert/alert.component'; +import { AlertErrorComponent } from './alert/alert-error.component'; + +/** + * Application wide Module + */ +@NgModule({ + imports: [AlertComponent, AlertErrorComponent], + exports: [CommonModule, NgbModule, FontAwesomeModule, AlertComponent, AlertErrorComponent], +}) +export default class SharedModule {} diff --git a/src/main/webapp/app/shared/sort/index.ts b/src/main/webapp/app/shared/sort/index.ts new file mode 100644 index 0000000..1a04bec --- /dev/null +++ b/src/main/webapp/app/shared/sort/index.ts @@ -0,0 +1,2 @@ +export { default as SortDirective } from './sort.directive'; +export { default as SortByDirective } from './sort-by.directive'; diff --git a/src/main/webapp/app/shared/sort/sort-by.directive.spec.ts b/src/main/webapp/app/shared/sort/sort-by.directive.spec.ts new file mode 100644 index 0000000..51fef72 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort-by.directive.spec.ts @@ -0,0 +1,140 @@ +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { FaIconComponent, FaIconLibrary } from '@fortawesome/angular-fontawesome'; +import { fas, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; + +import SortByDirective from './sort-by.directive'; +import SortDirective from './sort.directive'; + +@Component({ + template: ` + + + + + + +
ID
+ `, +}) +class TestSortByDirectiveComponent { + predicate?: string; + ascending?: boolean; + sortAllowed = true; + transition = jest.fn(); + + constructor(library: FaIconLibrary) { + library.addIconPacks(fas); + library.addIcons(faSort, faSortDown, faSortUp); + } +} + +describe('Directive: SortByDirective', () => { + let component: TestSortByDirectiveComponent; + let fixture: ComponentFixture; + let tableHead: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SortDirective, SortByDirective], + declarations: [TestSortByDirectiveComponent, FaIconComponent], + }); + fixture = TestBed.createComponent(TestSortByDirectiveComponent); + component = fixture.componentInstance; + tableHead = fixture.debugElement.query(By.directive(SortByDirective)); + }); + + it('should initialize predicate, order, icon when initial component predicate differs from column predicate', () => { + // GIVEN + component.predicate = 'id'; + const sortByDirective = tableHead.injector.get(SortByDirective); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(sortByDirective.jhiSortBy).toEqual('name'); + expect(component.predicate).toEqual('id'); + expect(sortByDirective.iconComponent?.icon).toEqual('sort'); + expect(component.transition).toHaveBeenCalledTimes(0); + }); + + it('should initialize predicate, order, icon when initial component predicate is same as column predicate', () => { + // GIVEN + component.predicate = 'name'; + component.ascending = true; + const sortByDirective = tableHead.injector.get(SortByDirective); + + // WHEN + fixture.detectChanges(); + + // THEN + expect(sortByDirective.jhiSortBy).toEqual('name'); + expect(component.predicate).toEqual('name'); + expect(component.ascending).toEqual(true); + expect(sortByDirective.iconComponent?.icon).toEqual(faSortUp.iconName); + expect(component.transition).toHaveBeenCalledTimes(0); + }); + + it('should update component predicate, order, icon when user clicks on column header', () => { + // GIVEN + component.predicate = 'name'; + component.ascending = true; + const sortByDirective = tableHead.injector.get(SortByDirective); + + // WHEN + fixture.detectChanges(); + tableHead.triggerEventHandler('click', null); + fixture.detectChanges(); + + // THEN + expect(component.predicate).toEqual('name'); + expect(component.ascending).toEqual(false); + expect(sortByDirective.iconComponent?.icon).toEqual(faSortDown.iconName); + expect(component.transition).toHaveBeenCalledTimes(1); + expect(component.transition).toHaveBeenCalledWith({ predicate: 'name', ascending: false }); + }); + + it('should update component predicate, order, icon when user double clicks on column header', () => { + // GIVEN + component.predicate = 'name'; + component.ascending = true; + const sortByDirective = tableHead.injector.get(SortByDirective); + + // WHEN + fixture.detectChanges(); + + tableHead.triggerEventHandler('click', null); + fixture.detectChanges(); + + tableHead.triggerEventHandler('click', null); + fixture.detectChanges(); + + // THEN + expect(component.predicate).toEqual('name'); + expect(component.ascending).toEqual(true); + expect(sortByDirective.iconComponent?.icon).toEqual(faSortUp.iconName); + expect(component.transition).toHaveBeenCalledTimes(2); + expect(component.transition).toHaveBeenNthCalledWith(1, { predicate: 'name', ascending: false }); + expect(component.transition).toHaveBeenNthCalledWith(2, { predicate: 'name', ascending: true }); + }); + + it('should not run sorting on click if sorting icon is hidden', () => { + // GIVEN + component.predicate = 'id'; + component.ascending = false; + component.sortAllowed = false; + + // WHEN + fixture.detectChanges(); + + tableHead.triggerEventHandler('click', null); + fixture.detectChanges(); + + // THEN + expect(component.predicate).toEqual('id'); + expect(component.ascending).toEqual(false); + expect(component.transition).not.toHaveBeenCalled(); + }); +}); diff --git a/src/main/webapp/app/shared/sort/sort-by.directive.ts b/src/main/webapp/app/shared/sort/sort-by.directive.ts new file mode 100644 index 0000000..1e8eda6 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort-by.directive.ts @@ -0,0 +1,56 @@ +import { AfterContentInit, ContentChild, Directive, Host, HostListener, Input, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { faSort, faSortDown, faSortUp, IconDefinition } from '@fortawesome/free-solid-svg-icons'; + +import SortDirective from './sort.directive'; + +@Directive({ + standalone: true, + selector: '[jhiSortBy]', +}) +export default class SortByDirective implements AfterContentInit, OnDestroy { + @Input() jhiSortBy!: T; + + @ContentChild(FaIconComponent, { static: false }) + iconComponent?: FaIconComponent; + + sortIcon = faSort; + sortAscIcon = faSortUp; + sortDescIcon = faSortDown; + + private readonly destroy$ = new Subject(); + + constructor(@Host() private sort: SortDirective) { + sort.predicateChange.pipe(takeUntil(this.destroy$)).subscribe(() => this.updateIconDefinition()); + sort.ascendingChange.pipe(takeUntil(this.destroy$)).subscribe(() => this.updateIconDefinition()); + } + + @HostListener('click') + onClick(): void { + if (this.iconComponent) { + this.sort.sort(this.jhiSortBy); + } + } + + ngAfterContentInit(): void { + this.updateIconDefinition(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + private updateIconDefinition(): void { + if (this.iconComponent) { + let icon: IconDefinition = this.sortIcon; + if (this.sort.predicate === this.jhiSortBy) { + icon = this.sort.ascending ? this.sortAscIcon : this.sortDescIcon; + } + this.iconComponent.icon = icon.iconName; + this.iconComponent.render(); + } + } +} diff --git a/src/main/webapp/app/shared/sort/sort.directive.spec.ts b/src/main/webapp/app/shared/sort/sort.directive.spec.ts new file mode 100644 index 0000000..5dc7b87 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort.directive.spec.ts @@ -0,0 +1,87 @@ +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import SortDirective from './sort.directive'; + +@Component({ + template: ` + + + + +
+ `, +}) +class TestSortDirectiveComponent { + predicate?: string; + ascending?: boolean; + transition = jest.fn(); +} + +describe('Directive: SortDirective', () => { + let component: TestSortDirectiveComponent; + let fixture: ComponentFixture; + let tableRow: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SortDirective], + declarations: [TestSortDirectiveComponent], + }); + fixture = TestBed.createComponent(TestSortDirectiveComponent); + component = fixture.componentInstance; + tableRow = fixture.debugElement.query(By.directive(SortDirective)); + }); + + it('should update predicate, order and invoke sortChange function', () => { + // GIVEN + const sortDirective = tableRow.injector.get(SortDirective); + + // WHEN + fixture.detectChanges(); + sortDirective.sort('ID'); + + // THEN + expect(component.predicate).toEqual('ID'); + expect(component.ascending).toEqual(true); + expect(component.transition).toHaveBeenCalledTimes(1); + expect(component.transition).toHaveBeenCalledWith({ predicate: 'ID', ascending: true }); + }); + + it('should change sort order to descending when same field is sorted again', () => { + // GIVEN + const sortDirective = tableRow.injector.get(SortDirective); + + // WHEN + fixture.detectChanges(); + sortDirective.sort('ID'); + // sort again + sortDirective.sort('ID'); + + // THEN + expect(component.predicate).toEqual('ID'); + expect(component.ascending).toEqual(false); + expect(component.transition).toHaveBeenCalledTimes(2); + expect(component.transition).toHaveBeenNthCalledWith(1, { predicate: 'ID', ascending: true }); + expect(component.transition).toHaveBeenNthCalledWith(2, { predicate: 'ID', ascending: false }); + }); + + it('should change sort order to ascending when different field is sorted', () => { + // GIVEN + const sortDirective = tableRow.injector.get(SortDirective); + + // WHEN + fixture.detectChanges(); + sortDirective.sort('ID'); + // sort again + sortDirective.sort('NAME'); + + // THEN + expect(component.predicate).toEqual('NAME'); + expect(component.ascending).toEqual(true); + expect(component.transition).toHaveBeenCalledTimes(2); + expect(component.transition).toHaveBeenNthCalledWith(1, { predicate: 'ID', ascending: true }); + expect(component.transition).toHaveBeenNthCalledWith(2, { predicate: 'NAME', ascending: true }); + }); +}); diff --git a/src/main/webapp/app/shared/sort/sort.directive.ts b/src/main/webapp/app/shared/sort/sort.directive.ts new file mode 100644 index 0000000..9bc4117 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort.directive.ts @@ -0,0 +1,40 @@ +import { Directive, EventEmitter, Input, Output } from '@angular/core'; + +@Directive({ + standalone: true, + selector: '[jhiSort]', +}) +export default class SortDirective { + @Input() + get predicate(): T | undefined { + return this._predicate; + } + set predicate(predicate: T | undefined) { + this._predicate = predicate; + this.predicateChange.emit(predicate); + } + + @Input() + get ascending(): boolean | undefined { + return this._ascending; + } + set ascending(ascending: boolean | undefined) { + this._ascending = ascending; + this.ascendingChange.emit(ascending); + } + + @Output() predicateChange = new EventEmitter(); + @Output() ascendingChange = new EventEmitter(); + @Output() sortChange = new EventEmitter<{ predicate: T; ascending: boolean }>(); + + private _predicate?: T; + private _ascending?: boolean; + + sort(field: T): void { + this.ascending = field !== this.predicate ? true : !this.ascending; + this.predicate = field; + this.predicateChange.emit(field); + this.ascendingChange.emit(this.ascending); + this.sortChange.emit({ predicate: this.predicate, ascending: this.ascending }); + } +} diff --git a/src/main/webapp/app/shared/sort/sort.service.ts b/src/main/webapp/app/shared/sort/sort.service.ts new file mode 100644 index 0000000..d276059 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class SortService { + private collator = new Intl.Collator(undefined, { + numeric: true, + sensitivity: 'base', + }); + + public startSort(property: string, order: number): (a: any, b: any) => number { + return (a: any, b: any) => this.collator.compare(a[property], b[property]) * order; + } +} diff --git a/src/main/webapp/bootstrap.ts b/src/main/webapp/bootstrap.ts new file mode 100644 index 0000000..e5038d5 --- /dev/null +++ b/src/main/webapp/bootstrap.ts @@ -0,0 +1,16 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { DEBUG_INFO_ENABLED } from './app/app.constants'; +import { AppModule } from './app/app.module'; + +// disable debug data on prod profile to improve performance +if (!DEBUG_INFO_ENABLED) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule, { preserveWhitespaces: true }) + // eslint-disable-next-line no-console + .then(() => console.log('Application started')) + .catch(err => console.error(err)); diff --git a/src/main/webapp/content/css/loading.css b/src/main/webapp/content/css/loading.css new file mode 100644 index 0000000..678e7b6 --- /dev/null +++ b/src/main/webapp/content/css/loading.css @@ -0,0 +1,152 @@ +@keyframes lds-pacman-1 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + } + 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } +} +@-webkit-keyframes lds-pacman-1 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 50% { + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + } + 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } +} +@keyframes lds-pacman-2 { + 0% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } + 50% { + -webkit-transform: rotate(225deg); + transform: rotate(225deg); + } + 100% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } +} +@-webkit-keyframes lds-pacman-2 { + 0% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } + 50% { + -webkit-transform: rotate(225deg); + transform: rotate(225deg); + } + 100% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } +} +@keyframes lds-pacman-3 { + 0% { + -webkit-transform: translate(190px, 0); + transform: translate(190px, 0); + opacity: 0; + } + 20% { + opacity: 1; + } + 100% { + -webkit-transform: translate(70px, 0); + transform: translate(70px, 0); + opacity: 1; + } +} +@-webkit-keyframes lds-pacman-3 { + 0% { + -webkit-transform: translate(190px, 0); + transform: translate(190px, 0); + opacity: 0; + } + 20% { + opacity: 1; + } + 100% { + -webkit-transform: translate(70px, 0); + transform: translate(70px, 0); + opacity: 1; + } +} + +.app-loading { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + top: 10em; +} +.app-loading p { + display: block; + font-size: 1.17em; + margin-inline-start: 0px; + margin-inline-end: 0px; + font-weight: normal; +} + +.app-loading .lds-pacman { + position: relative; + margin: auto; + width: 200px !important; + height: 200px !important; + -webkit-transform: translate(-100px, -100px) scale(1) translate(100px, 100px); + transform: translate(-100px, -100px) scale(1) translate(100px, 100px); +} +.app-loading .lds-pacman > div:nth-child(2) div { + position: absolute; + top: 40px; + left: 40px; + width: 120px; + height: 60px; + border-radius: 120px 120px 0 0; + background: #bbcedd; + -webkit-animation: lds-pacman-1 1s linear infinite; + animation: lds-pacman-1 1s linear infinite; + -webkit-transform-origin: 60px 60px; + transform-origin: 60px 60px; +} +.app-loading .lds-pacman > div:nth-child(2) div:nth-child(2) { + -webkit-animation: lds-pacman-2 1s linear infinite; + animation: lds-pacman-2 1s linear infinite; +} +.app-loading .lds-pacman > div:nth-child(1) div { + position: absolute; + top: 97px; + left: -8px; + width: 24px; + height: 10px; + background-image: url('/content/images/logo-jhipster.png'); + background-size: contain; + -webkit-animation: lds-pacman-3 1s linear infinite; + animation: lds-pacman-3 1.5s linear infinite; +} +.app-loading .lds-pacman > div:nth-child(1) div:nth-child(1) { + -webkit-animation-delay: -0.67s; + animation-delay: -1s; +} +.app-loading .lds-pacman > div:nth-child(1) div:nth-child(2) { + -webkit-animation-delay: -0.33s; + animation-delay: -0.5s; +} +.app-loading .lds-pacman > div:nth-child(1) div:nth-child(3) { + -webkit-animation-delay: 0s; + animation-delay: 0s; +} diff --git a/src/main/webapp/content/images/jhipster_family_member_0.svg b/src/main/webapp/content/images/jhipster_family_member_0.svg new file mode 100644 index 0000000..d6df83c --- /dev/null +++ b/src/main/webapp/content/images/jhipster_family_member_0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/webapp/content/images/jhipster_family_member_0_head-192.png b/src/main/webapp/content/images/jhipster_family_member_0_head-192.png new file mode 100644 index 0000000000000000000000000000000000000000..6d90ab36d37914257d5f3d0356e7895ccc0596f2 GIT binary patch literal 13439 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 literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_0_head-256.png b/src/main/webapp/content/images/jhipster_family_member_0_head-256.png new file mode 100644 index 0000000000000000000000000000000000000000..8e99bfe66d5a0e4831fcdc4031c1a57ba9b9cb1a GIT binary patch literal 7037 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 literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_0_head-384.png b/src/main/webapp/content/images/jhipster_family_member_0_head-384.png new file mode 100644 index 0000000000000000000000000000000000000000..c7ca46022548477085d9c10966c94131e7290df6 GIT binary patch literal 10350 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 literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_1.svg b/src/main/webapp/content/images/jhipster_family_member_1.svg new file mode 100644 index 0000000..e3a0f3d --- /dev/null +++ b/src/main/webapp/content/images/jhipster_family_member_1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/webapp/content/images/jhipster_family_member_1_head-192.png b/src/main/webapp/content/images/jhipster_family_member_1_head-192.png new file mode 100644 index 0000000000000000000000000000000000000000..ac5f2a00ac809809f7e48628d79e04849ebad98b GIT binary patch literal 7046 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 S!t<8`Kvhvo0VZc1_WuB}nB+SE literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_1_head-256.png b/src/main/webapp/content/images/jhipster_family_member_1_head-256.png new file mode 100644 index 0000000000000000000000000000000000000000..443822e8ea1f8db01adc11f33e8685fb14b997dc GIT binary patch literal 9505 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 literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_1_head-384.png b/src/main/webapp/content/images/jhipster_family_member_1_head-384.png new file mode 100644 index 0000000000000000000000000000000000000000..4a5e9fe4775b79347fcf33c97a7ad10e3f693b2c GIT binary patch literal 15054 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 O0c0c;#cM>30{;*Cash(? literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_1_head-512.png b/src/main/webapp/content/images/jhipster_family_member_1_head-512.png new file mode 100644 index 0000000000000000000000000000000000000000..66c625c10626011b5e142ce32ca009cbaea5f0bb GIT binary patch literal 16456 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 literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_2.svg b/src/main/webapp/content/images/jhipster_family_member_2.svg new file mode 100644 index 0000000..51c6a5a --- /dev/null +++ b/src/main/webapp/content/images/jhipster_family_member_2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/webapp/content/images/jhipster_family_member_2_head-192.png b/src/main/webapp/content/images/jhipster_family_member_2_head-192.png new file mode 100644 index 0000000000000000000000000000000000000000..24baf78ce8326c3c2756f10ebed90829d0bddf30 GIT binary patch literal 5423 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 X%hVz6s@WHxK!C2Mp+=25A@Y9!nF9~R literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_2_head-256.png b/src/main/webapp/content/images/jhipster_family_member_2_head-256.png new file mode 100644 index 0000000000000000000000000000000000000000..7b25f52f93d109da2df18f8442ef2104a6e14b8f GIT binary patch literal 6687 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 y!w`2PZFekY^= literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_2_head-384.png b/src/main/webapp/content/images/jhipster_family_member_2_head-384.png new file mode 100644 index 0000000000000000000000000000000000000000..e89e120c2f015ccb140678ffff8ccbacb1ae1f20 GIT binary patch literal 9682 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 V^p!DYUi_f~=xZBm)oD1z{4Y|2cgz3) literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_2_head-512.png b/src/main/webapp/content/images/jhipster_family_member_2_head-512.png new file mode 100644 index 0000000000000000000000000000000000000000..3c0e6cb9af31e2e46d9e9b6c8cec99c0d4c5e5c6 GIT binary patch literal 10514 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 literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_3.svg b/src/main/webapp/content/images/jhipster_family_member_3.svg new file mode 100644 index 0000000..cc0d01f --- /dev/null +++ b/src/main/webapp/content/images/jhipster_family_member_3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/webapp/content/images/jhipster_family_member_3_head-192.png b/src/main/webapp/content/images/jhipster_family_member_3_head-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b1e4fb3c8654570012950a3c888f11cb92c23bdd GIT binary patch literal 6148 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 B<5&Oy literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_3_head-256.png b/src/main/webapp/content/images/jhipster_family_member_3_head-256.png new file mode 100644 index 0000000000000000000000000000000000000000..aa058c7a0aba654a4991c8bbd5d3e2d32517acd1 GIT binary patch literal 8028 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 I)*|@-0J^?0w*UYD literal 0 HcmV?d00001 diff --git a/src/main/webapp/content/images/jhipster_family_member_3_head-384.png b/src/main/webapp/content/images/jhipster_family_member_3_head-384.png new file mode 100644 index 0000000000000000000000000000000000000000..1d10bd5da3d4d417aedfd3bf2e8515eff092c506 GIT binary patch literal 11998 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 element. + +$body-bg: #ffffff; + +// Typography: +// Font, line-height, and color for body text, headings, and more. + +$font-size-base: 1rem; + +$dropdown-link-hover-color: white; +$dropdown-link-hover-bg: #343a40; diff --git a/src/main/webapp/content/scss/global.scss b/src/main/webapp/content/scss/global.scss new file mode 100644 index 0000000..f939174 --- /dev/null +++ b/src/main/webapp/content/scss/global.scss @@ -0,0 +1,239 @@ +@import 'bootstrap-variables'; +@import 'bootstrap/scss/functions'; +@import 'bootstrap/scss/variables'; + +/* ============================================================== +Bootstrap tweaks +===============================================================*/ + +body, +h1, +h2, +h3, +h4 { + font-weight: 300; +} + +/* Increase contrast of links to get 100% on Lighthouse Accessability Audit. Override this color if you want to change the link color, or use a Bootswatch theme */ +a { + color: #533f03; + font-weight: bold; +} + +a:hover { + color: #533f03; +} + +/* override hover color for dropdown-item forced by bootstrap to all a:not([href]):not([tabindex]) elements in _reboot.scss */ +a:not([href]):not([tabindex]):hover.dropdown-item { + color: $dropdown-link-hover-color; +} + +/* override .dropdown-item.active background-color on hover */ +.dropdown-item.active:hover { + background-color: mix($dropdown-link-hover-bg, $dropdown-link-active-bg, 50%); +} + +a:hover { + /* make sure browsers use the pointer cursor for anchors, even with no href */ + cursor: pointer; +} + +.dropdown-item:hover { + color: $dropdown-link-hover-color; +} + +/* ========================================================================== +Browser Upgrade Prompt +========================================================================== */ +.browserupgrade { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + +/* ========================================================================== +Generic styles +========================================================================== */ + +/* Error highlight on input fields */ +.ng-valid[required], +.ng-valid.required { + border-left: 5px solid green; +} + +.ng-invalid:not(form) { + border-left: 5px solid red; +} + +/* other generic styles */ + +.jh-card { + padding: 1.5%; + margin-top: 20px; + border: none; +} + +.error { + color: white; + background-color: red; +} + +.pad { + padding: 10px; +} + +.w-40 { + width: 40% !important; +} + +.w-60 { + width: 60% !important; +} + +.break { + white-space: normal; + word-break: break-all; +} + +.form-control { + background-color: #fff; +} + +.readonly { + background-color: #eee; + opacity: 1; +} + +.footer { + border-top: 1px solid rgba(0, 0, 0, 0.125); +} + +.hand, +[jhisortby] { + cursor: pointer; +} + +/* ========================================================================== +Custom alerts for notification +========================================================================== */ +.alerts { + .alert { + text-overflow: ellipsis; + pre { + background: none; + border: none; + font: inherit; + color: inherit; + padding: 0; + margin: 0; + } + .popover pre { + font-size: 10px; + } + } + .jhi-toast { + position: fixed; + width: 100%; + &.left { + left: 5px; + } + &.right { + right: 5px; + } + &.top { + top: 55px; + } + &.bottom { + bottom: 55px; + } + } +} + +@media screen and (min-width: 480px) { + .alerts .jhi-toast { + width: 50%; + } +} + +/* ========================================================================== +entity list page css +========================================================================== */ + +.table-entities thead th .d-flex > * { + margin: auto 0; +} + +/* ========================================================================== +entity detail page css +========================================================================== */ +.row-md.jh-entity-details { + display: grid; + grid-template-columns: auto 1fr; + column-gap: 10px; + line-height: 1.5; +} + +@media screen and (min-width: 768px) { + .row-md.jh-entity-details > { + dt { + float: left; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0.5em 0; + } + dd { + border-bottom: 1px solid #eee; + padding: 0.5em 0; + margin-left: 0; + } + } +} + +/* ========================================================================== +ui bootstrap tweaks +========================================================================== */ +.nav, +.pagination, +.carousel, +.panel-title a { + cursor: pointer; +} + +.thread-dump-modal-lock { + max-width: 450px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dropdown-menu { + padding-left: 0px; +} + +/* ========================================================================== +angular-cli removes postcss-rtl processed inline css, processed rules must be added here instead +========================================================================== */ +/* page-ribbon.component.scss */ +.ribbon { + left: -3.5em; + -moz-transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + -o-transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); +} + +/* navbar.component.scss */ +.navbar { + ul.navbar-nav { + .nav-item { + margin-left: 0.5em; + } + } +} +/* jhipster-needle-scss-add-main JHipster will add new css style */ diff --git a/src/main/webapp/content/scss/vendor.scss b/src/main/webapp/content/scss/vendor.scss new file mode 100644 index 0000000..acf2df2 --- /dev/null +++ b/src/main/webapp/content/scss/vendor.scss @@ -0,0 +1,12 @@ +/* after changing this file run 'npm run webapp:build' */ + +/*************************** +put Sass variables here: +eg $input-color: red; +****************************/ +// Override Bootstrap variables +@import 'bootstrap-variables'; +// Import Bootstrap source files from node_modules +@import 'bootstrap/scss/bootstrap'; + +/* jhipster-needle-scss-add-vendor JHipster will add new css style */ diff --git a/src/main/webapp/declarations.d.ts b/src/main/webapp/declarations.d.ts new file mode 100644 index 0000000..dfaf5b5 --- /dev/null +++ b/src/main/webapp/declarations.d.ts @@ -0,0 +1 @@ +declare const SERVER_API_URL: string; diff --git a/src/main/webapp/favicon.ico b/src/main/webapp/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4179874f53b3c3b0ba9e2a401412f814ab6296bd GIT binary patch literal 1574 zcmV+>2HE+EP)000H;Nkl9r4a2J+cS@yzSX6E!^*k!$-D-4;C?DsI4Z2te8^PT@c z=RfD)VMK(b3{GnU7K#)Bt&t+2HBtn$Mv8#eNDUS>&~)z8>N1c(q8rNd{sPjn1MM2QHDA^sG2W;HKsEVpi{%G+8~m}54A zpq~8zXet!#9Gbj-*V8>RHQ@_Oa=ck5fDynmyijTRbXS$xsGFssxLtW3`my8G-!>%7 zn;n<%&U37xG-qc+$&NL}Ic8(r8P8{L>@pz`5wF~FxA(hl3{Q#@0mJ|T!>orWW&$y= zwZ)lV?q9=mOj&=001@Fo=j2<5|CsUovve~87<4?ht)^g4)54xHIM_fH6g9vTH~{$6e3n@@!>>0A((rb8tLK5s#V0 zMm+wn&))p*T>q|hCGR#@SL9}ZK#Tw|V#*5$4}y`?UCz_p4!1wNkO1l#@e&9G#+VYs zEG&vW!|wTsW3iPFM8p#vgQq%eFRm{7prxk1*aAS&G~ti@^w-GQ9%mpd0W^=8Nrc@q z?FmG(O?n~{0D#CgKIQg@lG#5`2LTaLZtu09`*&npbwOKeU4B;jv8(n!ddI?|{BT~F zzml*h^*el9D=mnp(SK|%RkC6{JP5dS>;C22j@;ZLQI=y-twP>a1a%F2w^mvhW1N!C zItRzq%;^AP-WFw5y#6R|j(8Qh9Dt}K_u4&+p(d6Y7r5u2fMIuVYB~yqz^KPR<_)T> zVUZCQ2K>q>IWj4>kGx1p%HD(9OEwu=KUlGU-EF%U2| z6%r-`VTl%YwkK@x(j4)7%q9E||Yz*Vu z-KgXDY^wP1l~c9KAAp;kIku<%ZR4V3H)gcci^L zxuYQ7iwPXJy}sz+_S%}ltQn(|jw6aU+5iCCMBw-}`=x=2s3a#J7eur?P5(n%lfW3; zzojxs0t_(d_}%ME4>VV=%*&kl@mY?4RLIPDrr1%QWBTmX>U-{zUphzI`^LkPoTQkY z^?7=MW3nvEM4$hB{Zyw7M3hi@DgLIIc}3Z#J)0`_Hm$Unjj=qofc>keP`dJ1mj{ks?4R(3kPwGF$MQ4Nw$&8u zY$#b@Zsp>+MfD;l;gbiMsB74J{+6r5=JEI=N`S;Tl0o2i)aJImRADmkV6kfzMM5Yl zb`4GR+C9Ed)T9?ySkhM)WtCdZJg310p5oRacks5uH}YWG9~KQdzS3&iP?nXGu8&`< zB@h73l>ryEsGJ*5{SM`E0!tK2{&F`(Kx?E3XpIyBt&t+2HBtn$Mv8#eND + + + + + isdashboard + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + + + + diff --git a/src/main/webapp/main.ts b/src/main/webapp/main.ts new file mode 100644 index 0000000..73c3a0e --- /dev/null +++ b/src/main/webapp/main.ts @@ -0,0 +1 @@ +import('./bootstrap').catch(err => console.error(err)); diff --git a/src/main/webapp/manifest.webapp b/src/main/webapp/manifest.webapp new file mode 100644 index 0000000..5312c45 --- /dev/null +++ b/src/main/webapp/manifest.webapp @@ -0,0 +1,31 @@ +{ + "name": "Isdashboard", + "short_name": "Isdashboard", + "icons": [ + { + "src": "./content/images/jhipster_family_member_2_head-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "./content/images/jhipster_family_member_2_head-256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "./content/images/jhipster_family_member_2_head-384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "./content/images/jhipster_family_member_2_head-512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#000000", + "background_color": "#e0e0e0", + "start_url": ".", + "display": "standalone", + "orientation": "portrait" +} diff --git a/src/main/webapp/robots.txt b/src/main/webapp/robots.txt new file mode 100644 index 0000000..04f669b --- /dev/null +++ b/src/main/webapp/robots.txt @@ -0,0 +1,8 @@ +# robotstxt.org/ + +User-agent: * +Disallow: /api/account +Disallow: /api/logs/ +Disallow: /api/users/ +Disallow: /management/ +Disallow: /v3/api-docs/ diff --git a/src/main/webapp/swagger-ui/dist/images/throbber.gif b/src/main/webapp/swagger-ui/dist/images/throbber.gif new file mode 100644 index 0000000000000000000000000000000000000000..06393889242fb3ea9e0205fa84369ec7bb66d15a GIT binary patch literal 9257 zcmd^^X;@R|x`tQg5wbE8AV3mAn1TjmQ&en2CK8~ENEH<+P_)pZ24y2E+7O0>K^a6u zQ3;5MiU^7p6*M3qDk!2=YEcHMQ>nzEYP;R`e2C@r+U+?#XaC*&gKPcB#k$`o&;7mu zYNhYYXe|Uo84#4ZIko#rcU5K8*yFL{qT47O&^5fZH$ zVZ@%(l~vVHjnm;H@KL8@r%yUHoo;rbHI_4lIH(_nsTT>S2`DFOD~uCb9_dF4`#QgI zy7ldMcLs+A_s%|e1pRPrbX-tpeNP!9(IpMFTce`t_5U%lP99z%&i6`1d~ zWeM!Rxc50<+d$e^9LT`?B+aMK~apR zHm?q;p<7{wN2g|I^aGlSws;VP84j(z%aQwvAWv83Z$}p(% zZ^?2;gxg(ey_`V5J7{;!o;o;KslW@z5EP~JGs|U)J7dF&(ff#A=6vU?cGQ$-4+;Jf z-ggJEa!yStn`_EWvl)#yhm6XVs}UUbsi;+agri;mCfjH^Uy;lH+Zw^h)4N?oZgZz4 zJk(fTZ|Bi^;+s_M=~+d#vyoxEPzTlOS=mX@sbl*uRj>=MaMr}cFIY8i?UM61>86uB zV$DlOUCiUJwbzJMP@D$urzK|lL2-PC!p1l47V-ZG<5Ev0Z5h~Kx?`KOp7gkAjV93A z-Gc7MrlxTf?wF;CbNc@tCHJH{TB3c;#{SVu%97}tyAM2n&|9W_?qv}$*Jt*%7Yxb# zV0;d;7|lDEltJYS+U)#aiJO};?_Jyy_4%syQ(uy?-J-Yx-9O5nKRk@@XSS~X<(2u~ zV-LamWm~!iqtH9wkpf8mAXZhOD&L#aA_%)4h2M;1M5jt zIR>Us+%W-GXa_f^opKg=DSrAs)AXeRa;Hp0aC1OgbxQ%Qr_QvTleM1jkR!2mkcX$3 ztsR8~G9iqh(-FJ@F_rQBIYDXV_6s7G9SxaVF^laZqcx$!D97m|7t16j6@Jt6UdDRy49Qyvs|c>RuA|@b%}`*wU}2^7q;&Vtc6@lb zcXl)T!6nYDzmMJ~%n$KNXyNlCG)GkJ4!82;v6@d3>s5r~E+3!O?049JDr14Y^PeMI02R`0lJ^=oJ zYd|*u9|SU(j7hY?+<=(?fP*mtV*zFhOrz6%{VA?ozdm&(Jf^V zMfPZ?>l`mS3{Uq8IM;e!+1YjJy2!mzK$O|wPeU{*QSbs9m+@`f5KxO3PBnQ=%RsZg%go*fJ`*w9TL{-WgZVIA$!YV}3BRcfeXaR$x#b zW)Tpd#8E4)^MyYdkH;4_;ChJuw%n+Be7Ko4;w-nHvyo$d_0e-YiF78Df&)_)(}fcr_r0mPH(4RRYWIu+d@t0&Ss@O^s! zOKyX&13)%N@83r^;QsgN{rl(!0|RF1FA)b1{CRXAy&1ySz@>olPiR4r$aMdq&_=nK zq|cFs8phWJ1@%dZ-gXd{zDbTILD>)qEvH-NU*Rf1b2J1Ri79`rBFl@ z8E^0I)OqEi{pH(a24b9YPG;Kz@t-qZW;3Mpe`MRlmYx{7bH-XZ&`RQ7Rb^%}gc&X| zd}Q-FZf|RWxHU?PR!(C?80zu(^l>*h{#ulSiid(O!J(8P-41bNM3tnX@U6NS5yo0? zdcF)~xFE&+&|gZ$23dV5t~?$$&ymZ;F8j7GGMncGSsDo%>J`26=&l=X#rSKv_64;0 zr;k6no@=gV`P)K!=kaHl>q?!`X>(A;84tg^Md<`zA%qbRLby1Z=fn*ZRdNqs%Tq|3 zOt}lZu0q9oKJhgz&+^7PCt$=UFW=R*w?a1)ePoL*`R$Gxj?TU@12tTHsT$giHQU+sqf;fS0FpT!< z z#UR4L_rT;lfRLVo8|3$7cmuxwjY5rmYs&kR6z_LRhf9-=4QalKQYEWw^4-EBI3j$& zA>$Im_{ZA>0`)E_&m%x6a)BThkx=e|aMkOrK9zb1YzqpQ&WZ^$)2T>CwTCuYRn5y) z3fVXg-@R5&Bf4?WUTyD|hBDe2>xEh|o-y}o5Se~+Ob!5xN>CaAN!<4)F zwNh!Y7B?@AigokFYNJL`0Vz&-ekrY95-n3M<%GR<;SzXRmO7(zd+gf|$Thb%;pby2 zyd{5TJ?|JYUgpSlJ0=LB@k6#d&opuPGq^qJAIumfhigC2qAX0OEnYnT@O;bA?X1O5 zpLe9|%_H+Yki!Rv$7Kvjv8r7Z?$<>G)g*%D*V#s&kz>Z3V1 z3!ZKh9H8Nl9IdhEW_rY#oYdDCLTe+nQ{(d2pBX8%CmxL+1`|b#Vb!?IY!kT7$PDWAP9$FY=e9KSK{DEH|408! zl-$lv)U8$EB{~es&j>rYg%{{JRvIl8@NK}L=xDAEVv(o#W@3LUDc*m?yKSPR0O|nY zAh;*QuBdpja8HzP8Uw`ce-r*LrUA47ZvZ)ff3k4^>;dFcof}9eXeeM<0OVj&CKDVK zpUKKIF%hSmry!pwK68UX>zOF@dv}B4Gg)^2GQmN7@A?zG!xO6dT*Cq0+r{eY6}AfU zf`|~y!?^R*nB0!iTcg|CgM}ou^H*s~5)%h;Xh;PYOM!|Yhfk$w;@`1Dx1y!EZrM&^zMat!^Wz# z=Z{;Pa0w21oA1X3*9=`*c7o3ePa^k%Vzu>2C_7DaZJ8FW5GJv|t>`Ym;_S>7g_3XI zdRb!Ppd`ErK`pUDHRsJd9@)bu>}s1)nKsyAR7h21<1u{DX1gd_Vf;^zdUpFPeSHHR z7AMgw^{FlFlK91CGMafKt`$FLhq#^=->@Uok7pqW6&#Zs4*E(i5-jog43A*qC@!(8 z8&F}pofRcMVmcJd=f;fvlfAR!ZqeaTE?#TQ^jQM0ioaJf8m^!Kdv^`f5kEsD0=gX#4={QE1$3A4K~V$ITKEd){XVLx?i6K*D>JF6E=i znqF^X#&UX}rfB|#A9%y|sR5i6B5gyk>8@Q+xHg|^5iz7C2}YkGF)nuP4LX#k2tRBP z=!VnWnXea(K#Wvg2&0f{!mXuuWaPpsoZ)3TSaEp;i|_)CvP=4wjI; zH%7tcLM8dQXsHW*#|}%TG9yiGpyjBltpcpXkpl8zg~x zD{QG)2Z8x$vfjgDc(J6i|OHoLX&!<+m^<$S3DtA8Mf!{ z7;g1}0uqJ0Mxuy%=#BFX5;Xh9JkrA$d}neS9T;$F$kXn}ss zF{Jn}9EDk=>h)sMy$YXfhKIDxr7U@3xl+uI|N5y!>?{aVn703L1Qgb$ql%JT^lsGD%)~)(H?Spj$zNt)h)Raob z@KyVB@&ngE0rtMW4!UTqGX>{&KHJAWqb)oYq9O)e)nmN0jVa;LNbKXx04a+8&O;q) zHBzGejrqt7Dk$Z2VR%%K#`!((pXE*MR{jGtv|q$p5#v9N0f^6B9IB!Q6(y$TmHRLM zsYXm2jn3f{9T)KVVzotDx=Ng8q0Z*VDZOkd5C!p0PRoFt>NyVEc9*%YR&2>Nq~$AI zXOQfjJ&wpGMe~I8y=cC(QR4=W2GWccFK(3`d&gN+)qWtW-`*}mZI%KDRl4@rUv1%d zxFO82lhW$xQyYxJg8tOZyXm1As%kEFNn)eW{R61M>af@wr(YW{R@+eL2 zx?SovK+867$F%T;Dfeajw|kiQ81GcOnS$Y4+hp8g_w1P8_~79d9p$*M1_Ei81$H$Ti6oi?ZW)&tmsJa7RV1LKddm7R*qL54L7j zvCr1Mrb;l!=m^TbJun-C_6$7w81E1eAQC^6s4>rZ4&I5+yyu$kha%Z&d+|S7Ki#{2 zy}%Giz|eR|G?ychX%%=eL`W(aLarb(L4jd>J+wlX;xMV9H8J!l&i?~Mw7)jlIuLD% zyq+AK92j#kC`ycv$SJ|E7!FBParx#v<3_rZ-DLQ@>`#sdl5}immok8&`{YgF|+< z`tB>e%6G{=B4?V-be>`&*}0d*f?$yBX@w+rJht@O+=^zttqB2p=IiA17#YD$4-fih z@$gJ95mGmFhN!d;3Ag4#>3o`>%L{G=9<}qOJ$wDN)%)MN6bVsAPG4oKB3+8r6!Qf9 z3m8?jIpWcEJbt6|f?Y4nMXK(--YZ|GA2_aRS!do%J9S7?Q&4FYL@sPilq}e4tlYa& z?f+we^=FH^Z9|dnXZghblW!IYGIAT{``58&7vZBybh+GuIPP{h*J?&vf7i8rv6qgx zab9~l+K`tvC7pWtlS!5lt(n#Yl}PAR(v01oXjc0F?T0w>+*p#PtE?Tf_hMrEaZ!^V zbv_>=4xibc0TUxg^I>TS?HR4fdiWl`@6{7|WU9G68l7tOz2p>oIe~NNr!>Q&PHm`4 z98R?g(IT*nl#{_|*WO_h0X78;WwMp?A^Zi)W@BX5q==TdOl?~J6HK(0b(xD6?m3e3 z#+zMaSJb(W$h5+d+6vujSjyi_R80c9>7h;0YlUFDvN`iNGu&5HQ5^e>6x?&JSc4V$6_I1jJ4vnCVbkU`Gz=Uy#~OI( zlL-$UAE$pVCsD_rICM#Q!ltzcqDphp5L|ZrqUm>=H%x!RjMrF#*?BN2shvUg=H;)& zy~_xWl*k$~9Hl6PIq({dELPE-r4*YNs7?5{>dlC`EcK~lPKB_8V)G@H)UZFF8$tXT z@^raW#Hq4OJGFL2Aye|HU&_NL%dYans6?ltqEBz`Q|m=@Zh4=-p2r;}q(Nbsk$fUI zP|(Ns2>MDvZi1H7<55frlQn#%?`WY3g`+fRuC#UJx%#d!zxEu3=}zF514S=6f@?~$ zeuSB=6E7r3ya|; z@K7M3VBrls6c{M*M_{AB_fVjgQ|F(FuK(@=1eWeVMSpLglllqV6Rg-L_46;?^IskS z)x6|SR1^gGl6amWjkb1dX}^8DumNXNmhsfxKA#;bBBIZE@0gma5yQY(FX>|N~Y^mgq`xc zdxOf6r{9u#_e0gV3(fdBTdV2Sc4SN5ZmP?cB4?KR + + + + isdashboard - Swagger UI + + + + + + +
+ + + + + + + + diff --git a/src/test/java/org/gcube/isdashboard/IntegrationTest.java b/src/test/java/org/gcube/isdashboard/IntegrationTest.java new file mode 100644 index 0000000..eaee318 --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/IntegrationTest.java @@ -0,0 +1,20 @@ +package org.gcube.isdashboard; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.gcube.isdashboard.IsdashboardApp; +import org.gcube.isdashboard.config.AsyncSyncConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +/** + * Base composite annotation for integration tests. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@SpringBootTest(classes = { IsdashboardApp.class, AsyncSyncConfiguration.class }) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public @interface IntegrationTest { +} diff --git a/src/test/java/org/gcube/isdashboard/TechnicalStructureTest.java b/src/test/java/org/gcube/isdashboard/TechnicalStructureTest.java new file mode 100644 index 0000000..c7adad4 --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/TechnicalStructureTest.java @@ -0,0 +1,33 @@ +package org.gcube.isdashboard; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf; +import static com.tngtech.archunit.library.Architectures.layeredArchitecture; + +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +@AnalyzeClasses(packagesOf = IsdashboardApp.class, importOptions = DoNotIncludeTests.class) +class TechnicalStructureTest { + + // prettier-ignore + @ArchTest + static final ArchRule respectsTechnicalArchitectureLayers = layeredArchitecture() + .consideringAllDependencies() + .layer("Config").definedBy("..config..") + .layer("Web").definedBy("..web..") + .optionalLayer("Service").definedBy("..service..") + .layer("Security").definedBy("..security..") + + .whereLayer("Config").mayNotBeAccessedByAnyLayer() + .whereLayer("Web").mayOnlyBeAccessedByLayers("Config") + .whereLayer("Service").mayOnlyBeAccessedByLayers("Web", "Config") + .whereLayer("Security").mayOnlyBeAccessedByLayers("Config", "Service", "Web") + + .ignoreDependency(belongToAnyOf(IsdashboardApp.class), alwaysTrue()) + .ignoreDependency(alwaysTrue(), belongToAnyOf( + org.gcube.isdashboard.config.ApplicationProperties.class + )); +} diff --git a/src/test/java/org/gcube/isdashboard/config/AsyncSyncConfiguration.java b/src/test/java/org/gcube/isdashboard/config/AsyncSyncConfiguration.java new file mode 100644 index 0000000..9a74900 --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/config/AsyncSyncConfiguration.java @@ -0,0 +1,16 @@ +package org.gcube.isdashboard.config; + +import java.util.concurrent.Executor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.task.SyncTaskExecutor; + +@Configuration +public class AsyncSyncConfiguration { + + @Bean(name = "taskExecutor") + public Executor taskExecutor() { + return new SyncTaskExecutor(); + } +} diff --git a/src/test/java/org/gcube/isdashboard/config/SpringBootTestClassOrderer.java b/src/test/java/org/gcube/isdashboard/config/SpringBootTestClassOrderer.java new file mode 100644 index 0000000..1282215 --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/config/SpringBootTestClassOrderer.java @@ -0,0 +1,22 @@ +package org.gcube.isdashboard.config; + +import java.util.Comparator; +import org.gcube.isdashboard.IntegrationTest; +import org.junit.jupiter.api.ClassDescriptor; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.ClassOrdererContext; + +public class SpringBootTestClassOrderer implements ClassOrderer { + + @Override + public void orderClasses(ClassOrdererContext context) { + context.getClassDescriptors().sort(Comparator.comparingInt(SpringBootTestClassOrderer::getOrder)); + } + + private static int getOrder(ClassDescriptor classDescriptor) { + if (classDescriptor.findAnnotation(IntegrationTest.class).isPresent()) { + return 2; + } + return 1; + } +} diff --git a/src/test/java/org/gcube/isdashboard/config/StaticResourcesWebConfigurerTest.java b/src/test/java/org/gcube/isdashboard/config/StaticResourcesWebConfigurerTest.java new file mode 100644 index 0000000..9983578 --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/config/StaticResourcesWebConfigurerTest.java @@ -0,0 +1,76 @@ +package org.gcube.isdashboard.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.gcube.isdashboard.config.StaticResourcesWebConfiguration.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.CacheControl; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import tech.jhipster.config.JHipsterDefaults; +import tech.jhipster.config.JHipsterProperties; + +class StaticResourcesWebConfigurerTest { + + public static final int MAX_AGE_TEST = 5; + public StaticResourcesWebConfiguration staticResourcesWebConfiguration; + private ResourceHandlerRegistry resourceHandlerRegistry; + private MockServletContext servletContext; + private WebApplicationContext applicationContext; + private JHipsterProperties props; + + @BeforeEach + void setUp() { + servletContext = spy(new MockServletContext()); + applicationContext = mock(WebApplicationContext.class); + resourceHandlerRegistry = spy(new ResourceHandlerRegistry(applicationContext, servletContext)); + props = new JHipsterProperties(); + staticResourcesWebConfiguration = spy(new StaticResourcesWebConfiguration(props)); + } + + @Test + void shouldAppendResourceHandlerAndInitializeIt() { + staticResourcesWebConfiguration.addResourceHandlers(resourceHandlerRegistry); + + verify(resourceHandlerRegistry, times(1)).addResourceHandler(RESOURCE_PATHS); + verify(staticResourcesWebConfiguration, times(1)).initializeResourceHandler(any(ResourceHandlerRegistration.class)); + for (String testingPath : RESOURCE_PATHS) { + assertThat(resourceHandlerRegistry.hasMappingForPattern(testingPath)).isTrue(); + } + } + + @Test + void shouldInitializeResourceHandlerWithCacheControlAndLocations() { + CacheControl ccExpected = CacheControl.maxAge(5, TimeUnit.DAYS).cachePublic(); + when(staticResourcesWebConfiguration.getCacheControl()).thenReturn(ccExpected); + ResourceHandlerRegistration resourceHandlerRegistration = spy(new ResourceHandlerRegistration(RESOURCE_PATHS)); + + staticResourcesWebConfiguration.initializeResourceHandler(resourceHandlerRegistration); + + verify(staticResourcesWebConfiguration, times(1)).getCacheControl(); + verify(resourceHandlerRegistration, times(1)).setCacheControl(ccExpected); + verify(resourceHandlerRegistration, times(1)).addResourceLocations(RESOURCE_LOCATIONS); + } + + @Test + void shouldCreateCacheControlBasedOnJhipsterDefaultProperties() { + CacheControl cacheExpected = CacheControl.maxAge(JHipsterDefaults.Http.Cache.timeToLiveInDays, TimeUnit.DAYS).cachePublic(); + assertThat(staticResourcesWebConfiguration.getCacheControl()) + .extracting(CacheControl::getHeaderValue) + .isEqualTo(cacheExpected.getHeaderValue()); + } + + @Test + void shouldCreateCacheControlWithSpecificConfigurationInProperties() { + props.getHttp().getCache().setTimeToLiveInDays(MAX_AGE_TEST); + CacheControl cacheExpected = CacheControl.maxAge(MAX_AGE_TEST, TimeUnit.DAYS).cachePublic(); + assertThat(staticResourcesWebConfiguration.getCacheControl()) + .extracting(CacheControl::getHeaderValue) + .isEqualTo(cacheExpected.getHeaderValue()); + } +} diff --git a/src/test/java/org/gcube/isdashboard/config/WebConfigurerTest.java b/src/test/java/org/gcube/isdashboard/config/WebConfigurerTest.java new file mode 100644 index 0000000..2bac427 --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/config/WebConfigurerTest.java @@ -0,0 +1,133 @@ +package org.gcube.isdashboard.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import jakarta.servlet.*; +import java.io.File; +import java.util.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.JHipsterProperties; + +/** + * Unit tests for the {@link WebConfigurer} class. + */ +class WebConfigurerTest { + + private WebConfigurer webConfigurer; + + private MockServletContext servletContext; + + private MockEnvironment env; + + private JHipsterProperties props; + + @BeforeEach + public void setup() { + servletContext = spy(new MockServletContext()); + doReturn(mock(FilterRegistration.Dynamic.class)).when(servletContext).addFilter(anyString(), any(Filter.class)); + doReturn(mock(ServletRegistration.Dynamic.class)).when(servletContext).addServlet(anyString(), any(Servlet.class)); + + env = new MockEnvironment(); + props = new JHipsterProperties(); + + webConfigurer = new WebConfigurer(env, props); + } + + @Test + void shouldCustomizeServletContainer() { + env.setActiveProfiles(JHipsterConstants.SPRING_PROFILE_PRODUCTION); + UndertowServletWebServerFactory container = new UndertowServletWebServerFactory(); + webConfigurer.customize(container); + assertThat(container.getMimeMappings().get("abs")).isEqualTo("audio/x-mpeg"); + assertThat(container.getMimeMappings().get("html")).isEqualTo("text/html"); + assertThat(container.getMimeMappings().get("json")).isEqualTo("application/json"); + if (container.getDocumentRoot() != null) { + assertThat(container.getDocumentRoot()).isEqualTo(new File("target/classes/static/")); + } + } + + @Test + void shouldCorsFilterOnApiPath() throws Exception { + props.getCors().setAllowedOrigins(Collections.singletonList("other.domain.com")); + props.getCors().setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE")); + props.getCors().setAllowedHeaders(Collections.singletonList("*")); + props.getCors().setMaxAge(1800L); + props.getCors().setAllowCredentials(true); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new WebConfigurerTestController()).addFilters(webConfigurer.corsFilter()).build(); + + mockMvc + .perform( + options("/api/test-cors") + .header(HttpHeaders.ORIGIN, "other.domain.com") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST") + ) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "other.domain.com")) + .andExpect(header().string(HttpHeaders.VARY, "Origin")) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,PUT,DELETE")) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "1800")); + + mockMvc + .perform(get("/api/test-cors").header(HttpHeaders.ORIGIN, "other.domain.com")) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "other.domain.com")); + } + + @Test + void shouldCorsFilterOnOtherPath() throws Exception { + props.getCors().setAllowedOrigins(Collections.singletonList("*")); + props.getCors().setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE")); + props.getCors().setAllowedHeaders(Collections.singletonList("*")); + props.getCors().setMaxAge(1800L); + props.getCors().setAllowCredentials(true); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new WebConfigurerTestController()).addFilters(webConfigurer.corsFilter()).build(); + + mockMvc + .perform(get("/test/test-cors").header(HttpHeaders.ORIGIN, "other.domain.com")) + .andExpect(status().isOk()) + .andExpect(header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + + @Test + void shouldCorsFilterDeactivatedForNullAllowedOrigins() throws Exception { + props.getCors().setAllowedOrigins(null); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new WebConfigurerTestController()).addFilters(webConfigurer.corsFilter()).build(); + + mockMvc + .perform(get("/api/test-cors").header(HttpHeaders.ORIGIN, "other.domain.com")) + .andExpect(status().isOk()) + .andExpect(header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + + @Test + void shouldCorsFilterDeactivatedForEmptyAllowedOrigins() throws Exception { + props.getCors().setAllowedOrigins(new ArrayList<>()); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new WebConfigurerTestController()).addFilters(webConfigurer.corsFilter()).build(); + + mockMvc + .perform(get("/api/test-cors").header(HttpHeaders.ORIGIN, "other.domain.com")) + .andExpect(status().isOk()) + .andExpect(header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } +} diff --git a/src/test/java/org/gcube/isdashboard/config/WebConfigurerTestController.java b/src/test/java/org/gcube/isdashboard/config/WebConfigurerTestController.java new file mode 100644 index 0000000..1825647 --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/config/WebConfigurerTestController.java @@ -0,0 +1,14 @@ +package org.gcube.isdashboard.config; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class WebConfigurerTestController { + + @GetMapping("/api/test-cors") + public void testCorsOnApiPath() {} + + @GetMapping("/test/test-cors") + public void testCorsOnOtherPath() {} +} diff --git a/src/test/java/org/gcube/isdashboard/security/SecurityUtilsUnitTest.java b/src/test/java/org/gcube/isdashboard/security/SecurityUtilsUnitTest.java new file mode 100644 index 0000000..6669445 --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/security/SecurityUtilsUnitTest.java @@ -0,0 +1,92 @@ +package org.gcube.isdashboard.security; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Test class for the {@link SecurityUtils} utility class. + */ +class SecurityUtilsUnitTest { + + @BeforeEach + @AfterEach + void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + void testGetCurrentUserLogin() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("admin", "admin")); + SecurityContextHolder.setContext(securityContext); + Optional login = SecurityUtils.getCurrentUserLogin(); + assertThat(login).contains("admin"); + } + + @Test + void testIsAuthenticated() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("admin", "admin")); + SecurityContextHolder.setContext(securityContext); + boolean isAuthenticated = SecurityUtils.isAuthenticated(); + assertThat(isAuthenticated).isTrue(); + } + + @Test + void testAnonymousIsNotAuthenticated() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.ANONYMOUS)); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("anonymous", "anonymous", authorities)); + SecurityContextHolder.setContext(securityContext); + boolean isAuthenticated = SecurityUtils.isAuthenticated(); + assertThat(isAuthenticated).isFalse(); + } + + @Test + void testHasCurrentUserThisAuthority() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.USER)); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("user", "user", authorities)); + SecurityContextHolder.setContext(securityContext); + + assertThat(SecurityUtils.hasCurrentUserThisAuthority(AuthoritiesConstants.USER)).isTrue(); + assertThat(SecurityUtils.hasCurrentUserThisAuthority(AuthoritiesConstants.ADMIN)).isFalse(); + } + + @Test + void testHasCurrentUserAnyOfAuthorities() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.USER)); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("user", "user", authorities)); + SecurityContextHolder.setContext(securityContext); + + assertThat(SecurityUtils.hasCurrentUserAnyOfAuthorities(AuthoritiesConstants.USER, AuthoritiesConstants.ADMIN)).isTrue(); + assertThat(SecurityUtils.hasCurrentUserAnyOfAuthorities(AuthoritiesConstants.ANONYMOUS, AuthoritiesConstants.ADMIN)).isFalse(); + } + + @Test + void testHasCurrentUserNoneOfAuthorities() { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.USER)); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("user", "user", authorities)); + SecurityContextHolder.setContext(securityContext); + + assertThat(SecurityUtils.hasCurrentUserNoneOfAuthorities(AuthoritiesConstants.USER, AuthoritiesConstants.ADMIN)).isFalse(); + assertThat(SecurityUtils.hasCurrentUserNoneOfAuthorities(AuthoritiesConstants.ANONYMOUS, AuthoritiesConstants.ADMIN)).isTrue(); + } +} diff --git a/src/test/java/org/gcube/isdashboard/web/filter/SpaWebFilterIT.java b/src/test/java/org/gcube/isdashboard/web/filter/SpaWebFilterIT.java new file mode 100644 index 0000000..94d0b86 --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/web/filter/SpaWebFilterIT.java @@ -0,0 +1,83 @@ +package org.gcube.isdashboard.web.filter; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.gcube.isdashboard.IntegrationTest; +import org.gcube.isdashboard.security.AuthoritiesConstants; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +@AutoConfigureMockMvc +@WithMockUser +@IntegrationTest +class SpaWebFilterIT { + + @Autowired + private MockMvc mockMvc; + + @Test + void testFilterForwardsToIndex() throws Exception { + mockMvc.perform(get("/")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + } + + @Test + @WithMockUser(authorities = AuthoritiesConstants.ADMIN) + void testFilterDoesNotForwardToIndexForV3ApiDocs() throws Exception { + mockMvc.perform(get("/v3/api-docs")).andExpect(status().isOk()).andExpect(forwardedUrl(null)); + } + + @Test + void testFilterDoesNotForwardToIndexForDotFile() throws Exception { + mockMvc.perform(get("/file.js")).andExpect(status().isNotFound()); + } + + @Test + void getBackendEndpoint() throws Exception { + mockMvc.perform(get("/test")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedFirstLevelMapping() throws Exception { + mockMvc.perform(get("/first-level")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedSecondLevelMapping() throws Exception { + mockMvc.perform(get("/first-level/second-level")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedThirdLevelMapping() throws Exception { + mockMvc.perform(get("/first-level/second-level/third-level")).andExpect(status().isOk()).andExpect(forwardedUrl("/index.html")); + } + + @Test + void forwardUnmappedDeepMapping() throws Exception { + mockMvc.perform(get("/1/2/3/4/5/6/7/8/9/10")).andExpect(forwardedUrl("/index.html")); + } + + @Test + void getUnmappedFirstLevelFile() throws Exception { + mockMvc.perform(get("/foo.js")).andExpect(status().isNotFound()); + } + + /** + * This test verifies that any files that aren't permitted by Spring Security will be forbidden. + * If you want to change this to return isNotFound(), you need to add a request mapping that + * allows this file in SecurityConfiguration. + */ + @Test + void getUnmappedSecondLevelFile() throws Exception { + mockMvc.perform(get("/foo/bar.js")).andExpect(status().isForbidden()); + } + + @Test + void getUnmappedThirdLevelFile() throws Exception { + mockMvc.perform(get("/foo/another/bar.js")).andExpect(status().isForbidden()); + } +} diff --git a/src/test/java/org/gcube/isdashboard/web/rest/AccountResourceIT.java b/src/test/java/org/gcube/isdashboard/web/rest/AccountResourceIT.java new file mode 100644 index 0000000..f90dd00 --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/web/rest/AccountResourceIT.java @@ -0,0 +1,63 @@ +package org.gcube.isdashboard.web.rest; + +import static org.gcube.isdashboard.web.rest.AccountResourceIT.TEST_USER_LOGIN; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.gcube.isdashboard.IntegrationTest; +import org.gcube.isdashboard.security.AuthoritiesConstants; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +/** + * Integration tests for the {@link AccountResource} REST controller. + */ +@AutoConfigureMockMvc +@WithMockUser(value = TEST_USER_LOGIN) +@IntegrationTest +class AccountResourceIT { + + static final String TEST_USER_LOGIN = "test"; + + @Autowired + private MockMvc restAccountMockMvc; + + @Test + @WithMockUser(username = TEST_USER_LOGIN, authorities = AuthoritiesConstants.ADMIN) + void testGetExistingAccount() throws Exception { + restAccountMockMvc + .perform(get("/api/account").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.login").value(TEST_USER_LOGIN)) + .andExpect(jsonPath("$.authorities").value(AuthoritiesConstants.ADMIN)); + } + + @Test + @WithUnauthenticatedMockUser + void testNonAuthenticatedUser() throws Exception { + restAccountMockMvc + .perform(get("/api/authenticate").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string("")); + } + + @Test + void testAuthenticatedUser() throws Exception { + restAccountMockMvc + .perform( + get("/api/authenticate") + .with(request -> { + request.setRemoteUser(TEST_USER_LOGIN); + return request; + }) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().string(TEST_USER_LOGIN)); + } +} diff --git a/src/test/java/org/gcube/isdashboard/web/rest/TestUtil.java b/src/test/java/org/gcube/isdashboard/web/rest/TestUtil.java new file mode 100644 index 0000000..1b3261f --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/web/rest/TestUtil.java @@ -0,0 +1,182 @@ +package org.gcube.isdashboard.web.rest; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.hamcrest.TypeSafeMatcher; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; + +/** + * Utility class for testing REST controllers. + */ +public final class TestUtil { + + private static final ObjectMapper mapper = createObjectMapper(); + + private static ObjectMapper createObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false); + mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + mapper.registerModule(new JavaTimeModule()); + return mapper; + } + + /** + * Convert an object to JSON byte array. + * + * @param object the object to convert. + * @return the JSON byte array. + * @throws IOException + */ + public static byte[] convertObjectToJsonBytes(Object object) throws IOException { + return mapper.writeValueAsBytes(object); + } + + /** + * Create a byte array with a specific size filled with specified data. + * + * @param size the size of the byte array. + * @param data the data to put in the byte array. + * @return the JSON byte array. + */ + public static byte[] createByteArray(int size, String data) { + byte[] byteArray = new byte[size]; + for (int i = 0; i < size; i++) { + byteArray[i] = Byte.parseByte(data, 2); + } + return byteArray; + } + + /** + * A matcher that tests that the examined string represents the same instant as the reference datetime. + */ + public static class ZonedDateTimeMatcher extends TypeSafeDiagnosingMatcher { + + private final ZonedDateTime date; + + public ZonedDateTimeMatcher(ZonedDateTime date) { + this.date = date; + } + + @Override + protected boolean matchesSafely(String item, Description mismatchDescription) { + try { + if (!date.isEqual(ZonedDateTime.parse(item))) { + mismatchDescription.appendText("was ").appendValue(item); + return false; + } + return true; + } catch (DateTimeParseException e) { + mismatchDescription.appendText("was ").appendValue(item).appendText(", which could not be parsed as a ZonedDateTime"); + return false; + } + } + + @Override + public void describeTo(Description description) { + description.appendText("a String representing the same Instant as ").appendValue(date); + } + } + + /** + * Creates a matcher that matches when the examined string represents the same instant as the reference datetime. + * + * @param date the reference datetime against which the examined string is checked. + */ + public static ZonedDateTimeMatcher sameInstant(ZonedDateTime date) { + return new ZonedDateTimeMatcher(date); + } + + /** + * A matcher that tests that the examined number represents the same value - it can be Long, Double, etc - as the reference BigDecimal. + */ + public static class NumberMatcher extends TypeSafeMatcher { + + final BigDecimal value; + + public NumberMatcher(BigDecimal value) { + this.value = value; + } + + @Override + public void describeTo(Description description) { + description.appendText("a numeric value is ").appendValue(value); + } + + @Override + protected boolean matchesSafely(Number item) { + BigDecimal bigDecimal = asDecimal(item); + return bigDecimal != null && value.compareTo(bigDecimal) == 0; + } + + private static BigDecimal asDecimal(Number item) { + if (item == null) { + return null; + } + if (item instanceof BigDecimal) { + return (BigDecimal) item; + } else if (item instanceof Long) { + return BigDecimal.valueOf((Long) item); + } else if (item instanceof Integer) { + return BigDecimal.valueOf((Integer) item); + } else if (item instanceof Double) { + return BigDecimal.valueOf((Double) item); + } else if (item instanceof Float) { + return BigDecimal.valueOf((Float) item); + } else { + return BigDecimal.valueOf(item.doubleValue()); + } + } + } + + /** + * Creates a matcher that matches when the examined number represents the same value as the reference BigDecimal. + * + * @param number the reference BigDecimal against which the examined number is checked. + */ + public static NumberMatcher sameNumber(BigDecimal number) { + return new NumberMatcher(number); + } + + /** + * Verifies the equals/hashcode contract on the domain object. + */ + public static void equalsVerifier(Class clazz) throws Exception { + T domainObject1 = clazz.getConstructor().newInstance(); + assertThat(domainObject1.toString()).isNotNull(); + assertThat(domainObject1).isEqualTo(domainObject1); + assertThat(domainObject1).hasSameHashCodeAs(domainObject1); + // Test with an instance of another class + Object testOtherObject = new Object(); + assertThat(domainObject1).isNotEqualTo(testOtherObject); + assertThat(domainObject1).isNotEqualTo(null); + // Test with an instance of the same class + T domainObject2 = clazz.getConstructor().newInstance(); + assertThat(domainObject1).isNotEqualTo(domainObject2); + } + + /** + * Create a {@link FormattingConversionService} which use ISO date format, instead of the localized one. + * @return the {@link FormattingConversionService}. + */ + public static FormattingConversionService createFormattingConversionService() { + DefaultFormattingConversionService dfcs = new DefaultFormattingConversionService(); + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + registrar.registerFormatters(dfcs); + return dfcs; + } + + private TestUtil() {} +} diff --git a/src/test/java/org/gcube/isdashboard/web/rest/WithUnauthenticatedMockUser.java b/src/test/java/org/gcube/isdashboard/web/rest/WithUnauthenticatedMockUser.java new file mode 100644 index 0000000..1c3774f --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/web/rest/WithUnauthenticatedMockUser.java @@ -0,0 +1,23 @@ +package org.gcube.isdashboard.web.rest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContext; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithUnauthenticatedMockUser.Factory.class) +public @interface WithUnauthenticatedMockUser { + class Factory implements WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(WithUnauthenticatedMockUser annotation) { + return SecurityContextHolder.createEmptyContext(); + } + } +} diff --git a/src/test/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslatorIT.java b/src/test/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslatorIT.java new file mode 100644 index 0000000..0dd30dd --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslatorIT.java @@ -0,0 +1,112 @@ +package org.gcube.isdashboard.web.rest.errors; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.gcube.isdashboard.IntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +/** + * Integration tests {@link ExceptionTranslator} controller advice. + */ +@WithMockUser +@AutoConfigureMockMvc +@IntegrationTest +class ExceptionTranslatorIT { + + @Autowired + private MockMvc mockMvc; + + @Test + void testMethodArgumentNotValid() throws Exception { + mockMvc + .perform( + post("/api/exception-translator-test/method-argument").content("{}").contentType(MediaType.APPLICATION_JSON).with(csrf()) + ) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value(ErrorConstants.ERR_VALIDATION)) + .andExpect(jsonPath("$.fieldErrors.[0].objectName").value("test")) + .andExpect(jsonPath("$.fieldErrors.[0].field").value("test")) + .andExpect(jsonPath("$.fieldErrors.[0].message").value("must not be null")); + } + + @Test + void testMissingServletRequestPartException() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/missing-servlet-request-part").with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.400")); + } + + @Test + void testMissingServletRequestParameterException() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/missing-servlet-request-parameter").with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.400")); + } + + @Test + void testAccessDenied() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/access-denied").with(csrf())) + .andExpect(status().isForbidden()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.403")) + .andExpect(jsonPath("$.detail").value("test access denied!")); + } + + @Test + void testUnauthorized() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/unauthorized").with(csrf())) + .andExpect(status().isUnauthorized()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.401")) + .andExpect(jsonPath("$.path").value("/api/exception-translator-test/unauthorized")) + .andExpect(jsonPath("$.detail").value("test authentication failed!")); + } + + @Test + void testMethodNotSupported() throws Exception { + mockMvc + .perform(post("/api/exception-translator-test/access-denied").with(csrf())) + .andExpect(status().isMethodNotAllowed()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.405")) + .andExpect(jsonPath("$.detail").value("Request method 'POST' is not supported")); + } + + @Test + void testExceptionWithResponseStatus() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/response-status").with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.400")) + .andExpect(jsonPath("$.title").value("test response status")); + } + + @Test + void testInternalServerError() throws Exception { + mockMvc + .perform(get("/api/exception-translator-test/internal-server-error").with(csrf())) + .andExpect(status().isInternalServerError()) + .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) + .andExpect(jsonPath("$.message").value("error.http.500")) + .andExpect(jsonPath("$.title").value("Internal Server Error")); + } +} diff --git a/src/test/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslatorTestController.java b/src/test/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslatorTestController.java new file mode 100644 index 0000000..92e7e9b --- /dev/null +++ b/src/test/java/org/gcube/isdashboard/web/rest/errors/ExceptionTranslatorTestController.java @@ -0,0 +1,60 @@ +package org.gcube.isdashboard.web.rest.errors; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/exception-translator-test") +public class ExceptionTranslatorTestController { + + @PostMapping("/method-argument") + public void methodArgument(@Valid @RequestBody TestDTO testDTO) {} + + @GetMapping("/missing-servlet-request-part") + public void missingServletRequestPartException(@RequestPart String part) {} + + @GetMapping("/missing-servlet-request-parameter") + public void missingServletRequestParameterException(@RequestParam String param) {} + + @GetMapping("/access-denied") + public void accessdenied() { + throw new AccessDeniedException("test access denied!"); + } + + @GetMapping("/unauthorized") + public void unauthorized() { + throw new BadCredentialsException("test authentication failed!"); + } + + @GetMapping("/response-status") + public void exceptionWithResponseStatus() { + throw new TestResponseStatusException(); + } + + @GetMapping("/internal-server-error") + public void internalServerError() { + throw new RuntimeException(); + } + + public static class TestDTO { + + @NotNull + private String test; + + public String getTest() { + return test; + } + + public void setTest(String test) { + this.test = test; + } + } + + @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "test response status") + @SuppressWarnings("serial") + public static class TestResponseStatusException extends RuntimeException {} +} diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml new file mode 100644 index 0000000..ab664ee --- /dev/null +++ b/src/test/resources/config/application.yml @@ -0,0 +1,89 @@ +# =================================================================== +# Spring Boot configuration. +# +# This configuration is used for unit/integration tests. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +spring: + application: + name: isdashboard + jackson: + serialization: + write-durations-as-timestamps: false + mail: + host: localhost + main: + allow-bean-definition-overriding: true + messages: + basename: i18n/messages + security: + user: + name: test + password: test + roles: + - USER + task: + execution: + thread-name-prefix: isdashboard-task- + pool: + core-size: 1 + max-size: 50 + queue-capacity: 10000 + scheduling: + thread-name-prefix: isdashboard-scheduling- + pool: + size: 20 + thymeleaf: + mode: HTML + +server: + port: 10344 + address: localhost + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== +jhipster: + clientApp: + name: 'isdashboardApp' + mail: + from: isdashboard@localhost.com + base-url: http://127.0.0.1:8080 + logging: + # To test json console appender + use-json-format: false + logstash: + enabled: false + host: localhost + port: 5000 + ring-buffer-size: 512 + security: + remember-me: + # security key (this key should be unique for your application, and kept secret) + key: 6038bcb93c3f8cc61c163e71aa55573477e46c3535cf0624212567f396b89e7be83d90b03885a818d14b1b7f4928ab548561 + +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: +management: + health: + mail: + enabled: false diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..1f9a495 --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1,4 @@ +junit.jupiter.execution.timeout.default = 15 s +junit.jupiter.execution.timeout.testable.method.default = 15 s +junit.jupiter.execution.timeout.beforeall.method.default = 60 s +junit.jupiter.testclass.order.default=org.gcube.isdashboard.config.SpringBootTestClassOrderer diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000..c18cca4 --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..85b7334 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./target/classes/static/app", + "types": ["@angular/localize"] + }, + "files": ["src/main/webapp/main.ts"], + "include": ["src/main/webapp/**/*.d.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b12774a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "baseUrl": "src/main/webapp/", + "outDir": "./target/classes/static/", + "forceConsistentCasingInFileNames": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "useDefineForClassFields": false, + "target": "es2022", + "module": "es2020", + "types": [], + "lib": ["es2018", "dom"] + }, + "references": [ + { + "path": "tsconfig.spec.json" + } + ], + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + "preserveWhitespaces": true + } +} diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 0000000..d00c687 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/main/webapp/**/*.ts"], + "compilerOptions": { + "composite": true, + "outDir": "target/out-tsc/spec", + "types": ["jest", "node"] + } +} diff --git a/webpack/environment.js b/webpack/environment.js new file mode 100644 index 0000000..d021177 --- /dev/null +++ b/webpack/environment.js @@ -0,0 +1,6 @@ +module.exports = { + I18N_HASH: 'generated_hash', + SERVER_API_URL: '', + __VERSION__: process.env.hasOwnProperty('APP_VERSION') ? process.env.APP_VERSION : 'DEV', + __DEBUG_INFO_ENABLED__: false, +}; diff --git a/webpack/logo-jhipster.png b/webpack/logo-jhipster.png new file mode 100644 index 0000000000000000000000000000000000000000..e301aa90f75e3ef8b5c5bf053186f3a02e7605d2 GIT binary patch literal 3326 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 EKQ4Pe { + // PLUGINS + if (config.mode === 'development') { + config.plugins.push( + new ESLintPlugin({ + baseConfig: { + parserOptions: { + project: ['../tsconfig.app.json'], + }, + }, + }), + new WebpackNotifierPlugin({ + title: 'Isdashboard', + contentImage: path.join(__dirname, 'logo-jhipster.png'), + }) + ); + } + + // configuring proxy for back end service + const tls = Boolean(config.devServer && config.devServer.https); + if (config.devServer) { + config.devServer.proxy = proxyConfig({ tls }); + } + + if (targetOptions.target === 'serve' || config.watch) { + config.plugins.push( + new BrowserSyncPlugin( + { + host: 'localhost', + port: 9000, + https: tls, + proxy: { + target: `http${tls ? 's' : ''}://localhost:${targetOptions.target === 'serve' ? '4200' : '8080'}`, + ws: true, + proxyOptions: { + changeOrigin: false, //pass the Host header to the backend unchanged https://github.com/Browsersync/browser-sync/issues/430 + }, + }, + socket: { + clients: { + heartbeatTimeout: 60000, + }, + }, + /* + ghostMode: { // uncomment this part to disable BrowserSync ghostMode; https://github.com/jhipster/generator-jhipster/issues/11116 + clicks: false, + location: false, + forms: false, + scroll: false, + }, + */ + }, + { + reload: targetOptions.target === 'build', // enabled for build --watch + } + ) + ); + } + + if (config.mode === 'production') { + config.plugins.push( + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + openAnalyzer: false, + // Webpack statistics in target folder + reportFilename: '../stats.html', + }) + ); + } + + const patterns = [ + { + // https://github.com/swagger-api/swagger-ui/blob/v4.6.1/swagger-ui-dist-package/README.md + context: require('swagger-ui-dist').getAbsoluteFSPath(), + from: '*.{js,css,html,png}', + to: 'swagger-ui/', + globOptions: { ignore: ['**/index.html'] }, + }, + { + from: path.join(path.dirname(require.resolve('axios/package.json')), 'dist/axios.min.js'), + to: 'swagger-ui/', + }, + { from: './src/main/webapp/swagger-ui/', to: 'swagger-ui/' }, + // jhipster-needle-add-assets-to-webpack - JHipster will add/remove third-party resources in this array + ]; + + if (patterns.length > 0) { + config.plugins.push(new CopyWebpackPlugin({ patterns })); + } + + config.plugins.push( + new webpack.DefinePlugin({ + // APP_VERSION is passed as an environment variable from the Gradle / Maven build tasks. + __VERSION__: JSON.stringify(environment.__VERSION__), + __DEBUG_INFO_ENABLED__: environment.__DEBUG_INFO_ENABLED__ || config.mode === 'development', + // The root URL for API calls, ending with a '/' - for example: `"https://www.jhipster.tech:8081/myservice/"`. + // If this URL is left empty (""), then it will be relative to the current context. + // If you use an API server, in `prod` mode, you will need to enable CORS + // (see the `jhipster.cors` common JHipster property in the `application-*.yml` configurations) + SERVER_API_URL: JSON.stringify(environment.SERVER_API_URL), + }) + ); + + config = merge( + config + // jhipster-needle-add-webpack-config - JHipster will add custom config + ); + + return config; +};