diff --git a/package.json b/package.json index 5cf240f859..a579c7d16a 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@nicky-lenaers/ngx-scroll-to": "^9.0.0", "angular-idle-preload": "3.0.0", "angulartics2": "^12.0.0", + "axios": "^0.27.2", "bootstrap": "4.3.1", "caniuse-lite": "^1.0.30001165", "cerialize": "0.1.18", @@ -212,4 +213,4 @@ "webpack-cli": "^4.2.0", "webpack-dev-server": "^4.5.0" } -} \ No newline at end of file +} diff --git a/server.ts b/server.ts index a4d90270ef..9fe03fe5b5 100644 --- a/server.ts +++ b/server.ts @@ -19,6 +19,7 @@ import 'zone.js/node'; import 'reflect-metadata'; import 'rxjs'; +import axios from 'axios'; import * as pem from 'pem'; import * as https from 'https'; import * as morgan from 'morgan'; @@ -38,14 +39,14 @@ import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import { hasValue, hasNoValue } from './src/app/shared/empty.util'; +import { hasNoValue, hasValue } from './src/app/shared/empty.util'; import { UIServerConfig } from './src/config/ui-server-config.interface'; import { ServerAppModule } from './src/main.server'; import { buildAppConfig } from './src/config/config.server'; -import { AppConfig, APP_CONFIG } from './src/config/app-config.interface'; +import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; import { extendEnvironmentWithAppConfig } from './src/config/config.util'; /* @@ -174,6 +175,11 @@ export function app() { */ router.use('/iiif', express.static(IIIF_VIEWER, { index: false })); + /** + * Checking server status + */ + server.get('/app/health', healthCheck); + // Register the ngApp callback function to handle incoming requests router.get('*', ngApp); @@ -319,6 +325,21 @@ function start() { } } +/* + * The callback function to serve health check requests + */ +function healthCheck(req, res) { + const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`; + axios.get(baseUrl) + .then((response) => { + res.status(response.status).send(response.data); + }) + .catch((error) => { + res.status(error.response.status).send({ + error: error.message + }); + }); +} // Webpack will replace 'require' with '__webpack_require__' // '__non_webpack_require__' is a proxy to Node 'require' // The below code is to ensure that the server is run only when not requiring the bundle. diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 6524edef77..b54036cf5a 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -122,3 +122,5 @@ export const REQUEST_COPY_MODULE_PATH = 'request-a-copy'; export function getRequestCopyModulePath() { return `/${REQUEST_COPY_MODULE_PATH}`; } + +export const HEALTH_PAGE_PATH = 'health'; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index f0869d9fb6..d00e1d7b0a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,13 +3,16 @@ import { RouterModule, NoPreloading } from '@angular/router'; import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; import { AuthenticatedGuard } from './core/auth/authenticated.guard'; -import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; +import { + SiteAdministratorGuard +} from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import { ACCESS_CONTROL_MODULE_PATH, ADMIN_MODULE_PATH, BITSTREAM_MODULE_PATH, FORBIDDEN_PATH, FORGOT_PASSWORD_PATH, + HEALTH_PAGE_PATH, INFO_MODULE_PATH, INTERNAL_SERVER_ERROR, LEGACY_BITSTREAM_MODULE_PATH, @@ -27,8 +30,12 @@ import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end- import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard'; import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component'; import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; -import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; -import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; +import { + GroupAdministratorGuard +} from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; +import { + ThemedPageInternalServerErrorComponent +} from './page-internal-server-error/themed-page-internal-server-error.component'; import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { MenuResolver } from './menu.resolver'; @@ -210,6 +217,11 @@ import { MenuResolver } from './menu.resolver'; loadChildren: () => import('./statistics-page/statistics-page-routing.module') .then((m) => m.StatisticsPageRoutingModule) }, + { + path: HEALTH_PAGE_PATH, + loadChildren: () => import('./health-page/health-page.module') + .then((m) => m.HealthPageModule) + }, { path: ACCESS_CONTROL_MODULE_PATH, loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule), diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.html b/src/app/health-page/health-info/health-info-component/health-info-component.component.html new file mode 100644 index 0000000000..dbaaa7a6b6 --- /dev/null +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.html @@ -0,0 +1,27 @@ +
+
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+ +

{{ getPropertyLabel(entry.key) | titlecase }} : {{entry.value}}

+
+
diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.scss b/src/app/health-page/health-info/health-info-component/health-info-component.component.scss new file mode 100644 index 0000000000..a6f0e73413 --- /dev/null +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts b/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts new file mode 100644 index 0000000000..b4532415b8 --- /dev/null +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts @@ -0,0 +1,82 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; + +import { HealthInfoComponentComponent } from './health-info-component.component'; +import { HealthInfoComponentOne, HealthInfoComponentTwo } from '../../../shared/mocks/health-endpoint.mocks'; +import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; + +describe('HealthInfoComponentComponent', () => { + let component: HealthInfoComponentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + NgbCollapseModule, + NoopAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ + HealthInfoComponentComponent, + ObjNgFor + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthInfoComponentComponent); + component = fixture.componentInstance; + }); + + describe('when has nested components', () => { + beforeEach(() => { + component.healthInfoComponentName = 'App'; + component.healthInfoComponent = HealthInfoComponentOne; + component.isCollapsed = false; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display property', () => { + const properties = fixture.debugElement.queryAll(By.css('[data-test="property"]')); + expect(properties.length).toBe(14); + const components = fixture.debugElement.queryAll(By.css('[data-test="info-component"]')); + expect(components.length).toBe(4); + }); + + }); + + describe('when has plain properties', () => { + beforeEach(() => { + component.healthInfoComponentName = 'Java'; + component.healthInfoComponent = HealthInfoComponentTwo; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display property', () => { + const property = fixture.debugElement.queryAll(By.css('[data-test="property"]')); + expect(property.length).toBe(1); + }); + + }); +}); diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.ts b/src/app/health-page/health-info/health-info-component/health-info-component.component.ts new file mode 100644 index 0000000000..d2cb393f09 --- /dev/null +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.ts @@ -0,0 +1,46 @@ +import { Component, Input } from '@angular/core'; + +import { HealthInfoComponent } from '../../models/health-component.model'; +import { HealthComponentComponent } from '../../health-panel/health-component/health-component.component'; + +/** + * Shows a health info object + */ +@Component({ + selector: 'ds-health-info-component', + templateUrl: './health-info-component.component.html', + styleUrls: ['./health-info-component.component.scss'] +}) +export class HealthInfoComponentComponent extends HealthComponentComponent { + + /** + * The HealthInfoComponent object to display + */ + @Input() healthInfoComponent: HealthInfoComponent|string; + + /** + * The HealthInfoComponent object name + */ + @Input() healthInfoComponentName: string; + + /** + * A boolean representing if div should start collapsed + */ + @Input() isNested = false; + + /** + * A boolean representing if div should start collapsed + */ + public isCollapsed = false; + + /** + * Check if the HealthInfoComponent is has only string property or contains object + * + * @param entry The HealthInfoComponent to check + * @return boolean + */ + isPlainProperty(entry: HealthInfoComponent | string): boolean { + return typeof entry === 'string'; + } + +} diff --git a/src/app/health-page/health-info/health-info.component.html b/src/app/health-page/health-info/health-info.component.html new file mode 100644 index 0000000000..4bafcaa2d8 --- /dev/null +++ b/src/app/health-page/health-info/health-info.component.html @@ -0,0 +1,25 @@ + + + + +
+ +
+ +
+ + +
+
+
+
+ + + +
+
+
diff --git a/src/app/health-page/health-info/health-info.component.scss b/src/app/health-page/health-info/health-info.component.scss new file mode 100644 index 0000000000..a6f0e73413 --- /dev/null +++ b/src/app/health-page/health-info/health-info.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-info/health-info.component.spec.ts b/src/app/health-page/health-info/health-info.component.spec.ts new file mode 100644 index 0000000000..5a9b8bf0aa --- /dev/null +++ b/src/app/health-page/health-info/health-info.component.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HealthInfoComponent } from './health-info.component'; +import { HealthInfoResponseObj } from '../../shared/mocks/health-endpoint.mocks'; +import { ObjNgFor } from '../../shared/utils/object-ngfor.pipe'; +import { By } from '@angular/platform-browser'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; + +describe('HealthInfoComponent', () => { + let component: HealthInfoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + NgbAccordionModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ + HealthInfoComponent, + ObjNgFor + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthInfoComponent); + component = fixture.componentInstance; + component.healthInfoResponse = HealthInfoResponseObj; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create info component properly', () => { + const components = fixture.debugElement.queryAll(By.css('[data-test="info-component"]')); + expect(components.length).toBe(3); + }); +}); diff --git a/src/app/health-page/health-info/health-info.component.ts b/src/app/health-page/health-info/health-info.component.ts new file mode 100644 index 0000000000..186d00299c --- /dev/null +++ b/src/app/health-page/health-info/health-info.component.ts @@ -0,0 +1,46 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { HealthInfoResponse } from '../models/health-component.model'; + +/** + * A component to render a "health-info component" object. + * + * Note that the word "component" in "health-info component" doesn't refer to Angular use of the term + * but rather to the components used in the response of the health endpoint of Spring's Actuator + * API. + */ +@Component({ + selector: 'ds-health-info', + templateUrl: './health-info.component.html', + styleUrls: ['./health-info.component.scss'] +}) +export class HealthInfoComponent implements OnInit { + + @Input() healthInfoResponse: HealthInfoResponse; + + /** + * The first active panel id + */ + activeId: string; + + constructor(private translate: TranslateService) { + } + + ngOnInit(): void { + this.activeId = Object.keys(this.healthInfoResponse)[0]; + } + + /** + * Return translated label if exist for the given property + * + * @param panelKey + */ + public getPanelLabel(panelKey: string): string { + const translationKey = `health-page.section-info.${panelKey}.title`; + const translation = this.translate.instant(translationKey); + + return (translation === translationKey) ? panelKey : translation; + } +} diff --git a/src/app/health-page/health-page.component.html b/src/app/health-page/health-page.component.html new file mode 100644 index 0000000000..8083389e1b --- /dev/null +++ b/src/app/health-page/health-page.component.html @@ -0,0 +1,27 @@ +
+

{{'health-page.heading' | translate}}

+
+ +
+
+ +
+ + diff --git a/src/app/health-page/health-page.component.scss b/src/app/health-page/health-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/health-page/health-page.component.spec.ts b/src/app/health-page/health-page.component.spec.ts new file mode 100644 index 0000000000..f3847ab092 --- /dev/null +++ b/src/app/health-page/health-page.component.spec.ts @@ -0,0 +1,72 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { By } from '@angular/platform-browser'; + +import { of } from 'rxjs'; +import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { HealthPageComponent } from './health-page.component'; +import { HealthService } from './health.service'; +import { HealthInfoResponseObj, HealthResponseObj } from '../shared/mocks/health-endpoint.mocks'; +import { RawRestResponse } from '../core/dspace-rest/raw-rest-response.model'; +import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; + +describe('HealthPageComponent', () => { + let component: HealthPageComponent; + let fixture: ComponentFixture; + + const healthService = jasmine.createSpyObj('healthDataService', { + getHealth: jasmine.createSpy('getHealth'), + getInfo: jasmine.createSpy('getInfo'), + }); + + const healthRestResponse$ = of({ + payload: HealthResponseObj, + statusCode: 200, + statusText: 'OK' + } as RawRestResponse); + + const healthInfoRestResponse$ = of({ + payload: HealthInfoResponseObj, + statusCode: 200, + statusText: 'OK' + } as RawRestResponse); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + NgbNavModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ HealthPageComponent ], + providers: [ + { provide: HealthService, useValue: healthService } + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthPageComponent); + component = fixture.componentInstance; + healthService.getHealth.and.returnValue(healthRestResponse$); + healthService.getInfo.and.returnValue(healthInfoRestResponse$); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create nav items properly', () => { + const navItems = fixture.debugElement.queryAll(By.css('li.nav-item')); + expect(navItems.length).toBe(2); + }); +}); diff --git a/src/app/health-page/health-page.component.ts b/src/app/health-page/health-page.component.ts new file mode 100644 index 0000000000..aa7bd7cba4 --- /dev/null +++ b/src/app/health-page/health-page.component.ts @@ -0,0 +1,66 @@ +import { Component, OnInit } from '@angular/core'; + +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { HealthService } from './health.service'; +import { HealthInfoResponse, HealthResponse } from './models/health-component.model'; + +@Component({ + selector: 'ds-health-page', + templateUrl: './health-page.component.html', + styleUrls: ['./health-page.component.scss'] +}) +export class HealthPageComponent implements OnInit { + + /** + * Health info endpoint response + */ + healthInfoResponse: BehaviorSubject = new BehaviorSubject(null); + + /** + * Health endpoint response + */ + healthResponse: BehaviorSubject = new BehaviorSubject(null); + + /** + * Represent if the response from health status endpoint is already retrieved or not + */ + healthResponseInitialised: BehaviorSubject = new BehaviorSubject(false); + + /** + * Represent if the response from health info endpoint is already retrieved or not + */ + healthInfoResponseInitialised: BehaviorSubject = new BehaviorSubject(false); + + constructor(private healthDataService: HealthService) { + } + + /** + * Retrieve responses from rest + */ + ngOnInit(): void { + this.healthDataService.getHealth().pipe(take(1)).subscribe({ + next: (data: any) => { + this.healthResponse.next(data.payload); + this.healthResponseInitialised.next(true); + }, + error: () => { + this.healthResponse.next(null); + this.healthResponseInitialised.next(true); + } + }); + + this.healthDataService.getInfo().pipe(take(1)).subscribe({ + next: (data: any) => { + this.healthInfoResponse.next(data.payload); + this.healthInfoResponseInitialised.next(true); + }, + error: () => { + this.healthInfoResponse.next(null); + this.healthInfoResponseInitialised.next(true); + } + }); + + } +} diff --git a/src/app/health-page/health-page.module.ts b/src/app/health-page/health-page.module.ts new file mode 100644 index 0000000000..02a6a91a5f --- /dev/null +++ b/src/app/health-page/health-page.module.ts @@ -0,0 +1,35 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; + +import { HealthPageRoutingModule } from './health-page.routing.module'; +import { HealthPanelComponent } from './health-panel/health-panel.component'; +import { HealthStatusComponent } from './health-panel/health-status/health-status.component'; +import { SharedModule } from '../shared/shared.module'; +import { HealthPageComponent } from './health-page.component'; +import { HealthComponentComponent } from './health-panel/health-component/health-component.component'; +import { HealthInfoComponent } from './health-info/health-info.component'; +import { HealthInfoComponentComponent } from './health-info/health-info-component/health-info-component.component'; + + +@NgModule({ + imports: [ + CommonModule, + HealthPageRoutingModule, + NgbModule, + SharedModule, + TranslateModule + ], + declarations: [ + HealthPageComponent, + HealthPanelComponent, + HealthStatusComponent, + HealthComponentComponent, + HealthInfoComponent, + HealthInfoComponentComponent, + ] +}) +export class HealthPageModule { +} diff --git a/src/app/health-page/health-page.routing.module.ts b/src/app/health-page/health-page.routing.module.ts new file mode 100644 index 0000000000..82d541dc31 --- /dev/null +++ b/src/app/health-page/health-page.routing.module.ts @@ -0,0 +1,28 @@ +import { RouterModule } from '@angular/router'; +import { NgModule } from '@angular/core'; + +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { HealthPageComponent } from './health-page.component'; +import { + SiteAdministratorGuard +} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + resolve: { breadcrumb: I18nBreadcrumbResolver }, + data: { + breadcrumbKey: 'health', + title: 'health-page.title', + }, + canActivate: [SiteAdministratorGuard], + component: HealthPageComponent + } + ]) + ] +}) +export class HealthPageRoutingModule { + +} diff --git a/src/app/health-page/health-panel/health-component/health-component.component.html b/src/app/health-page/health-panel/health-component/health-component.component.html new file mode 100644 index 0000000000..1f29c8c9fc --- /dev/null +++ b/src/app/health-page/health-panel/health-component/health-component.component.html @@ -0,0 +1,30 @@ + +
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+
+ +
+

{{ getPropertyLabel(item.key) | titlecase }} : {{item.value}}

+
+
+ + + diff --git a/src/app/health-page/health-panel/health-component/health-component.component.scss b/src/app/health-page/health-panel/health-component/health-component.component.scss new file mode 100644 index 0000000000..a6f0e73413 --- /dev/null +++ b/src/app/health-page/health-panel/health-component/health-component.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-panel/health-component/health-component.component.spec.ts b/src/app/health-page/health-panel/health-component/health-component.component.spec.ts new file mode 100644 index 0000000000..a8ec2b65e0 --- /dev/null +++ b/src/app/health-page/health-panel/health-component/health-component.component.spec.ts @@ -0,0 +1,85 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; + +import { HealthComponentComponent } from './health-component.component'; +import { HealthComponentOne, HealthComponentTwo } from '../../../shared/mocks/health-endpoint.mocks'; +import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; + +describe('HealthComponentComponent', () => { + let component: HealthComponentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + NgbCollapseModule, + NoopAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ + HealthComponentComponent, + ObjNgFor + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthComponentComponent); + component = fixture.componentInstance; + }); + + describe('when has nested components', () => { + beforeEach(() => { + component.healthComponentName = 'db'; + component.healthComponent = HealthComponentOne; + component.isCollapsed = false; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create collapsible divs properly', () => { + const collapseDivs = fixture.debugElement.queryAll(By.css('[data-test="collapse"]')); + expect(collapseDivs.length).toBe(2); + const detailsDivs = fixture.debugElement.queryAll(By.css('[data-test="details"]')); + expect(detailsDivs.length).toBe(6); + }); + }); + + describe('when has details', () => { + beforeEach(() => { + component.healthComponentName = 'geoIp'; + component.healthComponent = HealthComponentTwo; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create detail divs properly', () => { + const detailsDivs = fixture.debugElement.queryAll(By.css('[data-test="details"]')); + expect(detailsDivs.length).toBe(1); + const collapseDivs = fixture.debugElement.queryAll(By.css('[data-test="collapse"]')); + expect(collapseDivs.length).toBe(0); + }); + }); +}); diff --git a/src/app/health-page/health-panel/health-component/health-component.component.ts b/src/app/health-page/health-panel/health-component/health-component.component.ts new file mode 100644 index 0000000000..e212a07289 --- /dev/null +++ b/src/app/health-page/health-panel/health-component/health-component.component.ts @@ -0,0 +1,53 @@ +import { Component, Input } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { HealthComponent } from '../../models/health-component.model'; +import { AlertType } from '../../../shared/alert/aletr-type'; + +/** + * A component to render a "health component" object. + * + * Note that the word "component" in "health component" doesn't refer to Angular use of the term + * but rather to the components used in the response of the health endpoint of Spring's Actuator + * API. + */ +@Component({ + selector: 'ds-health-component', + templateUrl: './health-component.component.html', + styleUrls: ['./health-component.component.scss'] +}) +export class HealthComponentComponent { + + /** + * The HealthComponent object to display + */ + @Input() healthComponent: HealthComponent; + + /** + * The HealthComponent object name + */ + @Input() healthComponentName: string; + + public AlertTypeEnum = AlertType; + + /** + * A boolean representing if div should start collapsed + */ + public isCollapsed = false; + + constructor(private translate: TranslateService) { + } + + /** + * Return translated label if exist for the given property + * + * @param property + */ + public getPropertyLabel(property: string): string { + const translationKey = `health-page.property.${property}`; + const translation = this.translate.instant(translationKey); + + return (translation === translationKey) ? property : translation; + } +} diff --git a/src/app/health-page/health-panel/health-panel.component.html b/src/app/health-page/health-panel/health-panel.component.html new file mode 100644 index 0000000000..2d67fa537b --- /dev/null +++ b/src/app/health-page/health-panel/health-panel.component.html @@ -0,0 +1,25 @@ +

{{'health-page.status' | translate}} :

+ + + +
+ +
+ +
+ + +
+
+
+
+ + + +
+
+ + diff --git a/src/app/health-page/health-panel/health-panel.component.scss b/src/app/health-page/health-panel/health-panel.component.scss new file mode 100644 index 0000000000..a6f0e73413 --- /dev/null +++ b/src/app/health-page/health-panel/health-panel.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-panel/health-panel.component.spec.ts b/src/app/health-page/health-panel/health-panel.component.spec.ts new file mode 100644 index 0000000000..1d9c856ddb --- /dev/null +++ b/src/app/health-page/health-panel/health-panel.component.spec.ts @@ -0,0 +1,57 @@ +import { CommonModule } from '@angular/common'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; + +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { HealthPanelComponent } from './health-panel.component'; +import { HealthResponseObj } from '../../shared/mocks/health-endpoint.mocks'; +import { ObjNgFor } from '../../shared/utils/object-ngfor.pipe'; + +describe('HealthPanelComponent', () => { + let component: HealthPanelComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + NgbNavModule, + NgbAccordionModule, + CommonModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [ + HealthPanelComponent, + ObjNgFor + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthPanelComponent); + component = fixture.componentInstance; + component.healthResponse = HealthResponseObj; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render a panel for each component', () => { + const components = fixture.debugElement.queryAll(By.css('[data-test="component"]')); + expect(components.length).toBe(5); + }); + +}); diff --git a/src/app/health-page/health-panel/health-panel.component.ts b/src/app/health-page/health-panel/health-panel.component.ts new file mode 100644 index 0000000000..1c056daf20 --- /dev/null +++ b/src/app/health-page/health-panel/health-panel.component.ts @@ -0,0 +1,45 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { HealthResponse } from '../models/health-component.model'; + +/** + * Show the health panel + */ +@Component({ + selector: 'ds-health-panel', + templateUrl: './health-panel.component.html', + styleUrls: ['./health-panel.component.scss'] +}) +export class HealthPanelComponent implements OnInit { + + /** + * Health endpoint response + */ + @Input() healthResponse: HealthResponse; + + /** + * The first active panel id + */ + activeId: string; + + constructor(private translate: TranslateService) { + } + + ngOnInit(): void { + this.activeId = Object.keys(this.healthResponse.components)[0]; + } + + /** + * Return translated label if exist for the given property + * + * @param panelKey + */ + public getPanelLabel(panelKey: string): string { + const translationKey = `health-page.section.${panelKey}.title`; + const translation = this.translate.instant(translationKey); + + return (translation === translationKey) ? panelKey : translation; + } +} diff --git a/src/app/health-page/health-panel/health-status/health-status.component.html b/src/app/health-page/health-panel/health-status/health-status.component.html new file mode 100644 index 0000000000..38a6f72601 --- /dev/null +++ b/src/app/health-page/health-panel/health-status/health-status.component.html @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/app/health-page/health-panel/health-status/health-status.component.scss b/src/app/health-page/health-panel/health-status/health-status.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/health-page/health-panel/health-status/health-status.component.spec.ts b/src/app/health-page/health-panel/health-status/health-status.component.spec.ts new file mode 100644 index 0000000000..f0f61ebdbb --- /dev/null +++ b/src/app/health-page/health-panel/health-status/health-status.component.spec.ts @@ -0,0 +1,60 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { HealthStatusComponent } from './health-status.component'; +import { HealthStatus } from '../../models/health-component.model'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; + +describe('HealthStatusComponent', () => { + let component: HealthStatusComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + NgbTooltipModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ HealthStatusComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthStatusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create success icon', () => { + component.status = HealthStatus.UP; + fixture.detectChanges(); + const icon = fixture.debugElement.query(By.css('i.text-success')); + expect(icon).toBeTruthy(); + }); + + it('should create warning icon', () => { + component.status = HealthStatus.UP_WITH_ISSUES; + fixture.detectChanges(); + const icon = fixture.debugElement.query(By.css('i.text-warning')); + expect(icon).toBeTruthy(); + }); + + it('should create success icon', () => { + component.status = HealthStatus.DOWN; + fixture.detectChanges(); + const icon = fixture.debugElement.query(By.css('i.text-danger')); + expect(icon).toBeTruthy(); + }); +}); diff --git a/src/app/health-page/health-panel/health-status/health-status.component.ts b/src/app/health-page/health-panel/health-status/health-status.component.ts new file mode 100644 index 0000000000..19f83713fc --- /dev/null +++ b/src/app/health-page/health-panel/health-status/health-status.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core'; +import { HealthStatus } from '../../models/health-component.model'; + +/** + * Show a health status object + */ +@Component({ + selector: 'ds-health-status', + templateUrl: './health-status.component.html', + styleUrls: ['./health-status.component.scss'] +}) +export class HealthStatusComponent { + /** + * The current status to show + */ + @Input() status: HealthStatus; + + /** + * He + */ + HealthStatus = HealthStatus; + +} diff --git a/src/app/health-page/health.service.ts b/src/app/health-page/health.service.ts new file mode 100644 index 0000000000..7c238769a1 --- /dev/null +++ b/src/app/health-page/health.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { DspaceRestService } from '../core/dspace-rest/dspace-rest.service'; +import { RawRestResponse } from '../core/dspace-rest/raw-rest-response.model'; +import { HALEndpointService } from '../core/shared/hal-endpoint.service'; + +@Injectable({ + providedIn: 'root' +}) +export class HealthService { + constructor(protected halService: HALEndpointService, + protected restService: DspaceRestService) { + } + /** + * @returns health data + */ + getHealth(): Observable { + return this.halService.getEndpoint('/actuator').pipe( + map((restURL: string) => restURL + '/health'), + switchMap((endpoint: string) => this.restService.get(endpoint))); + } + + /** + * @returns information of server + */ + getInfo(): Observable { + return this.halService.getEndpoint('/actuator').pipe( + map((restURL: string) => restURL + '/info'), + switchMap((endpoint: string) => this.restService.get(endpoint))); + } +} diff --git a/src/app/health-page/models/health-component.model.ts b/src/app/health-page/models/health-component.model.ts new file mode 100644 index 0000000000..8461d4d967 --- /dev/null +++ b/src/app/health-page/models/health-component.model.ts @@ -0,0 +1,48 @@ +/** + * Interface for Health Status + */ +export enum HealthStatus { + UP = 'UP', + UP_WITH_ISSUES = 'UP_WITH_ISSUES', + DOWN = 'DOWN' +} + +/** + * Interface describing the Health endpoint response + */ +export interface HealthResponse { + status: HealthStatus; + components: { + [name: string]: HealthComponent; + }; +} + +/** + * Interface describing a single component retrieved from the Health endpoint response + */ +export interface HealthComponent { + status: HealthStatus; + details?: { + [name: string]: number|string; + }; + components?: { + [name: string]: HealthComponent; + }; +} + +/** + * Interface describing the Health info endpoint response + */ +export interface HealthInfoResponse { + [name: string]: HealthInfoComponent|string; +} + +/** + * Interface describing a single component retrieved from the Health info endpoint response + */ +export interface HealthInfoComponent { + [property: string]: HealthInfoComponent|string; +} + + + diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index 34e0f8ad0e..4c97d3d1b3 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -15,18 +15,34 @@ import { MenuService } from './shared/menu/menu.service'; import { filter, find, map, take } from 'rxjs/operators'; import { hasValue } from './shared/empty.util'; import { FeatureID } from './core/data/feature-authorization/feature-id'; -import { CreateCommunityParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; +import { + CreateCommunityParentSelectorComponent +} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model'; -import { CreateCollectionParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; -import { CreateItemParentSelectorComponent } from './shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; -import { EditCommunitySelectorComponent } from './shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; -import { EditCollectionSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; -import { EditItemSelectorComponent } from './shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; -import { ExportMetadataSelectorComponent } from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; +import { + CreateCollectionParentSelectorComponent +} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; +import { + CreateItemParentSelectorComponent +} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +import { + EditCommunitySelectorComponent +} from './shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; +import { + EditCollectionSelectorComponent +} from './shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; +import { + EditItemSelectorComponent +} from './shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; +import { + ExportMetadataSelectorComponent +} from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { - METADATA_EXPORT_SCRIPT_NAME, METADATA_IMPORT_SCRIPT_NAME, ScriptDataService + METADATA_EXPORT_SCRIPT_NAME, + METADATA_IMPORT_SCRIPT_NAME, + ScriptDataService } from './core/data/processes/script-data.service'; /** @@ -321,6 +337,18 @@ export class MenuResolver implements Resolve { icon: 'terminal', index: 10 }, + { + id: 'health', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.health', + link: '/health' + } as LinkMenuItemModel, + icon: 'heartbeat', + index: 11 + }, ]; menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { shouldPersistOnRouteChange: true diff --git a/src/app/shared/mocks/health-endpoint.mocks.ts b/src/app/shared/mocks/health-endpoint.mocks.ts new file mode 100644 index 0000000000..a9246d91a1 --- /dev/null +++ b/src/app/shared/mocks/health-endpoint.mocks.ts @@ -0,0 +1,160 @@ +import { + HealthComponent, + HealthInfoComponent, + HealthInfoResponse, + HealthResponse, + HealthStatus +} from '../../health-page/models/health-component.model'; + +export const HealthResponseObj: HealthResponse = { + 'status': HealthStatus.UP_WITH_ISSUES, + 'components': { + 'db': { + 'status': HealthStatus.UP, + 'components': { + 'dataSource': { + 'status': HealthStatus.UP, + 'details': { + 'database': 'PostgreSQL', + 'result': 1, + 'validationQuery': 'SELECT 1' + } + }, + 'dspaceDataSource': { + 'status': HealthStatus.UP, + 'details': { + 'database': 'PostgreSQL', + 'result': 1, + 'validationQuery': 'SELECT 1' + } + } + } + }, + 'geoIp': { + 'status': HealthStatus.UP_WITH_ISSUES, + 'details': { + 'reason': 'The GeoLite Database file is missing (/var/lib/GeoIP/GeoLite2-City.mmdb)! Solr Statistics cannot generate location based reports! Please see the DSpace installation instructions for instructions to install this file.' + } + }, + 'solrOaiCore': { + 'status': HealthStatus.UP, + 'details': { + 'status': 0, + 'detectedPathType': 'particular core' + } + }, + 'solrSearchCore': { + 'status': HealthStatus.UP, + 'details': { + 'status': 0, + 'detectedPathType': 'particular core' + } + }, + 'solrStatisticsCore': { + 'status': HealthStatus.UP, + 'details': { + 'status': 0, + 'detectedPathType': 'particular core' + } + } + } +}; + +export const HealthComponentOne: HealthComponent = { + 'status': HealthStatus.UP, + 'components': { + 'dataSource': { + 'status': HealthStatus.UP, + 'details': { + 'database': 'PostgreSQL', + 'result': 1, + 'validationQuery': 'SELECT 1' + } + }, + 'dspaceDataSource': { + 'status': HealthStatus.UP, + 'details': { + 'database': 'PostgreSQL', + 'result': 1, + 'validationQuery': 'SELECT 1' + } + } + } +}; + +export const HealthComponentTwo: HealthComponent = { + 'status': HealthStatus.UP_WITH_ISSUES, + 'details': { + 'reason': 'The GeoLite Database file is missing (/var/lib/GeoIP/GeoLite2-City.mmdb)! Solr Statistics cannot generate location based reports! Please see the DSpace installation instructions for instructions to install this file.' + } +}; + +export const HealthInfoResponseObj: HealthInfoResponse = { + 'app': { + 'name': 'DSpace at My University', + 'dir': '/home/giuseppe/development/java/install/dspace7-review', + 'url': 'http://localhost:8080/server', + 'db': 'jdbc:postgresql://localhost:5432/dspace7', + 'solr': { + 'server': 'http://localhost:8983/solr', + 'prefix': '' + }, + 'mail': { + 'server': 'smtp.example.com', + 'from-address': 'dspace-noreply@myu.edu', + 'feedback-recipient': 'dspace-help@myu.edu', + 'mail-admin': 'dspace-help@myu.edu', + 'mail-helpdesk': 'dspace-help@myu.edu', + 'alert-recipient': 'dspace-help@myu.edu' + }, + 'cors': { + 'allowed-origins': 'http://localhost:4000' + }, + 'ui': { + 'url': 'http://localhost:4000' + } + }, + 'java': { + 'vendor': 'Private Build', + 'version': '11.0.15', + 'runtime': { + 'name': 'OpenJDK Runtime Environment', + 'version': '11.0.15+10-Ubuntu-0ubuntu0.20.04.1' + }, + 'jvm': { + 'name': 'OpenJDK 64-Bit Server VM', + 'vendor': 'Private Build', + 'version': '11.0.15+10-Ubuntu-0ubuntu0.20.04.1' + } + }, + 'version': '7.3-SNAPSHOT' +}; + +export const HealthInfoComponentOne: HealthInfoComponent = { + 'name': 'DSpace at My University', + 'dir': '/home/giuseppe/development/java/install/dspace7-review', + 'url': 'http://localhost:8080/server', + 'db': 'jdbc:postgresql://localhost:5432/dspace7', + 'solr': { + 'server': 'http://localhost:8983/solr', + 'prefix': '' + }, + 'mail': { + 'server': 'smtp.example.com', + 'from-address': 'dspace-noreply@myu.edu', + 'feedback-recipient': 'dspace-help@myu.edu', + 'mail-admin': 'dspace-help@myu.edu', + 'mail-helpdesk': 'dspace-help@myu.edu', + 'alert-recipient': 'dspace-help@myu.edu' + }, + 'cors': { + 'allowed-origins': 'http://localhost:4000' + }, + 'ui': { + 'url': 'http://localhost:4000' + } +}; + +export const HealthInfoComponentTwo = { + 'version': '7.3-SNAPSHOT' +}; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 8b162b2adb..b412f9cee5 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1587,6 +1587,46 @@ "grant-request-copy.success": "Successfully granted item request", + "health.breadcrumbs": "Health", + + "health-page.heading" : "Health", + + "health-page.info-tab" : "Info", + + "health-page.status-tab" : "Status", + + "health-page.error.msg": "The health check service is temporarily unavailable", + + "health-page.property.status": "Status code", + + "health-page.section.db.title": "Database", + + "health-page.section.geoIp.title": "GeoIp", + + "health-page.section.solrAuthorityCore.title": "Sor: authority core", + + "health-page.section.solrOaiCore.title": "Sor: oai core", + + "health-page.section.solrSearchCore.title": "Sor: search core", + + "health-page.section.solrStatisticsCore.title": "Sor: statistics core", + + "health-page.section-info.app.title": "Application Backend", + + "health-page.section-info.java.title": "Java", + + "health-page.status": "Status", + + "health-page.status.ok.info": "Operational", + + "health-page.status.error.info": "Problems detected", + + "health-page.status.warning.info": "Possible issues detected", + + "health-page.title": "Health", + + "health-page.section.no-issues": "No issues detected", + "home.description": "", @@ -2540,13 +2580,15 @@ "menu.section.icon.find": "Find menu section", + "menu.section.icon.health": "Health check menu section", + "menu.section.icon.import": "Import menu section", "menu.section.icon.new": "New menu section", "menu.section.icon.pin": "Pin sidebar", - "menu.section.icon.processes": "Processes menu section", + "menu.section.icon.processes": "Processes Health", "menu.section.icon.registries": "Registries menu section", @@ -2588,6 +2630,8 @@ "menu.section.processes": "Processes", + "menu.section.health": "Health", + "menu.section.registries": "Registries", @@ -2954,8 +2998,6 @@ "profile.title": "Update Profile", - - "project.listelement.badge": "Research Project", "project.page.contributor": "Contributors", diff --git a/src/config/actuators.config.ts b/src/config/actuators.config.ts new file mode 100644 index 0000000000..8f59a13c98 --- /dev/null +++ b/src/config/actuators.config.ts @@ -0,0 +1,11 @@ +import { Config } from './config.interface'; + +/** + * Config that determines the spring Actuators options + */ +export class ActuatorsConfig implements Config { + /** + * The endpoint path + */ + public endpointPath: string; +} diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 8bfa1f66f1..649efacb7b 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -15,6 +15,7 @@ import { UIServerConfig } from './ui-server-config.interface'; import { MediaViewerConfig } from './media-viewer-config.interface'; import { BrowseByConfig } from './browse-by-config.interface'; import { BundleConfig } from './bundle-config.interface'; +import { ActuatorsConfig } from './actuators.config'; interface AppConfig extends Config { ui: UIServerConfig; @@ -34,6 +35,7 @@ interface AppConfig extends Config { themes: ThemeConfig[]; mediaViewer: MediaViewerConfig; bundle: BundleConfig; + actuators: ActuatorsConfig } const APP_CONFIG = new InjectionToken('APP_CONFIG'); diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 476351b403..383b92cf73 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -15,6 +15,7 @@ import { SubmissionConfig } from './submission-config.interface'; import { ThemeConfig } from './theme.model'; import { UIServerConfig } from './ui-server-config.interface'; import { BundleConfig } from './bundle-config.interface'; +import { ActuatorsConfig } from './actuators.config'; export class DefaultAppConfig implements AppConfig { production = false; @@ -48,6 +49,10 @@ export class DefaultAppConfig implements AppConfig { nameSpace: '/', }; + actuators: ActuatorsConfig = { + endpointPath: '/actuator/health' + }; + // Caching settings cache: CacheConfig = { // NOTE: how long should objects be cached for by default diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 4fb68521fc..8ad842c33e 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -38,6 +38,10 @@ export const environment: BuildConfig = { baseUrl: 'https://rest.com/api' }, + actuators: { + endpointPath: '/actuator/health' + }, + // Caching settings cache: { // NOTE: how long should objects be cached for by default diff --git a/yarn.lock b/yarn.lock index e7dc5201e3..82075602cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3221,6 +3221,14 @@ axios@0.21.4: dependencies: follow-redirects "^1.14.0" +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -6118,7 +6126,7 @@ flatted@^3.1.0, flatted@^3.2.5: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== -follow-redirects@^1.0.0, follow-redirects@^1.14.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.9: version "1.14.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==