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==