diff --git a/package.json b/package.json index 99ab1d2e07..95f659e9eb 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@angular/core": "~11.2.14", "@angular/forms": "~11.2.14", "@angular/localize": "11.2.14", + "@angular/material": "9.2.0", "@angular/platform-browser": "~11.2.14", "@angular/platform-browser-dynamic": "~11.2.14", "@angular/platform-server": "~11.2.14", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 6456547355..80310774c8 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -210,8 +210,8 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard'; }, { path: 'health', - loadChildren: () => import('./health-page/health.routing.module') - .then((m) => m.HealthPageRoutingModule) + loadChildren: () => import('./health-page/health.module') + .then((m) => m.HealthModule) }, { path: ACCESS_CONTROL_MODULE_PATH, diff --git a/src/app/health-page/health-data.service.ts b/src/app/health-page/health-data.service.ts new file mode 100644 index 0000000000..bd905006aa --- /dev/null +++ b/src/app/health-page/health-data.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 HealthDataService { + 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/health.module.ts b/src/app/health-page/health.module.ts index 46a6642168..8731b77f77 100644 --- a/src/app/health-page/health.module.ts +++ b/src/app/health-page/health.module.ts @@ -1,12 +1,23 @@ +import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { HealthPageRoutingModule } from './health.routing.module'; import { HealthComponent } from './health/health.component'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; @NgModule({ + imports: [ + CommonModule, + HealthPageRoutingModule, + MatExpansionModule, + NgbModule, + TranslateModule + ], declarations: [ HealthComponent ] }) export class HealthModule { - - } \ No newline at end of file + } diff --git a/src/app/health-page/health.routing.module.ts b/src/app/health-page/health.routing.module.ts index 70ed2a0e43..a8d94d9d1f 100644 --- a/src/app/health-page/health.routing.module.ts +++ b/src/app/health-page/health.routing.module.ts @@ -2,12 +2,15 @@ import { RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { HealthComponent } from './health/health.component'; +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; @NgModule({ imports: [ RouterModule.forChild([ { path: '', + resolve: { breadcrumb: I18nBreadcrumbResolver }, + data: { breadcrumbKey: 'health' }, canActivate: [AuthenticatedGuard], children: [ { diff --git a/src/app/health-page/health/health.component.html b/src/app/health-page/health/health.component.html index f96dc0a28f..05f77225fb 100644 --- a/src/app/health-page/health/health.component.html +++ b/src/app/health-page/health/health.component.html @@ -1 +1,39 @@ -

health works!

+
+ +
+
+ + + + diff --git a/src/app/health-page/health/health.component.scss b/src/app/health-page/health/health.component.scss index e69de29bb2..c085111079 100644 --- a/src/app/health-page/health/health.component.scss +++ b/src/app/health-page/health/health.component.scss @@ -0,0 +1,8 @@ +.mat-expansion-panel-header { + padding-left: 0px; +} + +.circle-red { + color:red; + align-items: center; +} \ No newline at end of file diff --git a/src/app/health-page/health/health.component.spec.ts b/src/app/health-page/health/health.component.spec.ts index 9423481b32..845360f059 100644 --- a/src/app/health-page/health/health.component.spec.ts +++ b/src/app/health-page/health/health.component.spec.ts @@ -1,14 +1,138 @@ +import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; - +import { MatExpansionModule } from '@angular/material/expansion'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { HealthDataService } from '../health-data.service'; +import { HealthPageRoutingModule } from '../health.routing.module'; import { HealthComponent } from './health.component'; -describe('HealthComponent', () => { + function getHealth() { + return of({ + 'payload':{ + 'status':'UP_WITH_ISSUES', + 'components':{ + 'db':{ + 'status':'UP', + 'components':{ + 'dataSource':{ + 'status':'UP', + 'details':{ + 'database':'PostgreSQL', + 'result':1, + 'validationQuery':'SELECT 1' + } + }, + 'dspaceDataSource':{ + 'status':'UP', + 'details':{ + 'database':'PostgreSQL', + 'result':1, + 'validationQuery':'SELECT 1' + } + } + } + }, + 'geoIp':{ + 'status':'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':'UP', + 'details':{ + 'status':0, + 'detectedPathType':'particular core' + } + }, + 'solrSearchCore':{ + 'status':'UP', + 'details':{ + 'status':0, + 'detectedPathType':'particular core' + } + }, + 'solrStatisticsCore':{ + 'status':'UP', + 'details':{ + 'status':0, + 'detectedPathType':'particular core' + } + } + } + }, + 'statusCode':200, + 'statusText':'OK' + }); + } + + function getInfo() { + return of({ + 'payload':{ + 'app':{ + 'name':'DSpace at My University', + 'version':'7.3', + 'dir':'/Users/pratikrajkotiya/Documents/Project/FrontEnd/dspace-cris-install', + 'url':'http://localhost:8080/server', + 'db':'jdbc:postgresql://localhost:5432/4science', + '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' + } + } + }, + 'statusCode':200, + 'statusText':'OK' + }); + } + +function getMockHealthDataService() { + return jasmine.createSpyObj('healthDataService', { + getHealth: getHealth(), + getInfo: getInfo() + }); +} + +fdescribe('HealthComponent', () => { let component: HealthComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ HealthComponent ] + imports: [ + NgbNavModule, + CommonModule, + HealthPageRoutingModule, + MatExpansionModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [ HealthComponent ], + providers:[{ provide: HealthDataService, useValue: getMockHealthDataService() }] }) .compileComponents(); }); @@ -22,4 +146,15 @@ describe('HealthComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should render health tab.', () => { + const healthTab = fixture.debugElement.query(By.css('#health')); + expect(healthTab).toBeTruthy(); + }); + + it('should render info tab.', () => { + const infoTab = fixture.debugElement.query(By.css('#info')); + expect(infoTab).toBeFalsy(); + }); + }); diff --git a/src/app/health-page/health/health.component.ts b/src/app/health-page/health/health.component.ts index 46ff73cc09..89aa6611f6 100644 --- a/src/app/health-page/health/health.component.ts +++ b/src/app/health-page/health/health.component.ts @@ -1,26 +1,100 @@ import { Component, OnInit } from '@angular/core'; -import { DspaceRestService } from '../../core/dspace-rest/dspace-rest.service'; -import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; +import { HealthDataService } from '../health-data.service'; + +enum HealthStatus { + UP = 'UP', + UP_WITH_ISSUES = 'UP_WITH_ISSUES', + DOWN = 'DOWN' +} @Component({ selector: 'ds-health', templateUrl: './health.component.html', styleUrls: ['./health.component.scss'] }) export class HealthComponent implements OnInit { - - constructor(protected halService: HALEndpointService, - protected restService: DspaceRestService) { - } + healthArr: string[]; + serverInfoArr: string[]; + healthGlobalStatus: string; + activeId ='Health'; + constructor(private healthDataService: HealthDataService) { } ngOnInit(): void { - this.halService.getRootHref(); - console.log('this.halService.getRootHref()',); - this.restService.get(this.halService.getRootHref() + '/actuator' + '/health').subscribe((data)=>{ - console.log(data); - - }) - + this.healthDataService.getHealth().subscribe((data) => { + this.healthArr = this.getHealth(data.payload.components); + this.healthGlobalStatus = data.payload.status; + }); + + this.healthDataService.getInfo().subscribe((data) => { + this.serverInfoArr = this.getInfo(data.payload, null, []); + }); + } + + /** + * @param obj represents a info + * @param key represents a nested key of info + * @param arr represents a key value pair or only key + * @returns {{arr}} of key value pair or only key + */ + getInfo(obj, key, arr) { + if (typeof obj === 'object' && key !== null) { + arr.push({style: {'font-weight': 'bold' ,'font-size.px': key === 'app' ? '30' : '20' }, value: key}); + } + if (typeof obj !== 'object') { + arr.push({style: {'font-size.px': '15'}, value: `${key} = ${obj}`}); + return obj; + } + // tslint:disable-next-line: forin + for (const objKey in obj) { + this.getInfo(obj[objKey], objKey, arr); + } + return arr; + } + + /** + * @param subCompObj represent nested sub component + * @param superCompkey represents a key of super component + * @returns linear components array + */ + getHealthSubComponents(subCompObj, superCompkey) { + const subCompArr = []; + // tslint:disable-next-line: forin + for (const key in subCompObj) { + subCompArr.push({ ...subCompObj[key], components: superCompkey + '.' + key }); + } + return subCompArr; + } + + /** + * @param componentsObj represent health data + * @returns linear components array + */ + getHealth(componentsObj) { + let componentsArr = []; + for (const key in componentsObj) { + if (componentsObj[key].hasOwnProperty('components')) { + componentsArr.push({ ...componentsObj[key], components: key }); + // tslint:disable-next-line: no-string-literal + componentsArr = [...componentsArr, ...this.getHealthSubComponents(componentsObj[key]['components'], key)]; + } else { + componentsArr.push({ ...componentsObj[key], components: key }); + } + } + return componentsArr; + } + + /** + * @param status of perticular block + * @returns {{ string }} class respective status + */ + getHealthIconClass(status: string): string { + if (status === HealthStatus.UP) { + return 'fa fa-check-circle text-success ml-2 mt-1'; + } else if (status === HealthStatus.UP_WITH_ISSUES) { + return 'fa fa-exclamation-triangle text-warning ml-2 mt-1'; + } else { + return 'fa fa-times-circle circle-red ml-2 mt-1'; + } } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index ba0508bdb5..5326c2f84e 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2909,7 +2909,13 @@ "profile.title": "Update Profile", + "health.breadcrumbs": "Health", + "health-page.health" : "Health", + + "health-page.info" : "Info", + + "health-page.status" : "Status", "project.listelement.badge": "Research Project", diff --git a/yarn.lock b/yarn.lock index 420ff76478..593dd53d36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -397,6 +397,11 @@ glob "7.1.2" yargs "^16.2.0" +"@angular/material@9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-9.2.0.tgz#1b6f0a2e115f93885d7fc2dc4b258d8c9cf6821f" + integrity sha512-KKzEIVh6/m56m+Ao8p4PK0SyEr0574l3VP2swj1qPag3u+FYgemmXCGTaChrKdDsez+zeTCPXImBGXzE6NQ80Q== + "@angular/platform-browser-dynamic@~11.2.14": version "11.2.14" resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.14.tgz#3c7fff1a1daacba5390acf033d28c377ec281166"