From 7d2b5008abf4d144ed6437d3de03637ca0f147fd Mon Sep 17 00:00:00 2001 From: Tiago Melo Date: Thu, 1 Feb 2018 11:17:43 +0000 Subject: [PATCH] mgr/dashboard_v2: add health page and its dependencies Signed-off-by: Tiago Melo --- .../mgr/dashboard_v2/controllers/dashboard.py | 176 ++++++++++++++ .../dashboard_v2/frontend/.angular-cli.json | 4 +- .../mgr/dashboard_v2/frontend/package.json | 4 + .../frontend/src/app/app-routing.module.ts | 12 +- .../frontend/src/app/app.component.spec.ts | 40 ++-- .../frontend/src/app/app.module.ts | 4 +- .../frontend/src/app/ceph/ceph.module.ts | 4 +- .../app/ceph/dashboard/dashboard.module.ts | 34 +++ .../ceph/dashboard/dashboard.service.spec.ts | 23 ++ .../app/ceph/dashboard/dashboard.service.ts | 11 + .../dashboard/dashboard.component.html | 10 + .../dashboard/dashboard.component.scss | 3 + .../dashboard/dashboard.component.spec.ts | 25 ++ .../dashboard/dashboard.component.ts | 16 ++ .../dashboard/health/health.component.html | 219 ++++++++++++++++++ .../dashboard/health/health.component.scss | 62 +++++ .../dashboard/health/health.component.spec.ts | 43 ++++ .../ceph/dashboard/health/health.component.ts | 210 +++++++++++++++++ .../app/ceph/dashboard/log-color.pipe.spec.ts | 8 + .../src/app/ceph/dashboard/log-color.pipe.ts | 21 ++ .../ceph/dashboard/mds-summary.pipe.spec.ts | 8 + .../app/ceph/dashboard/mds-summary.pipe.ts | 38 +++ .../ceph/dashboard/mgr-summary.pipe.spec.ts | 8 + .../app/ceph/dashboard/mgr-summary.pipe.ts | 22 ++ .../ceph/dashboard/mon-summary.pipe.spec.ts | 8 + .../app/ceph/dashboard/mon-summary.pipe.ts | 18 ++ .../ceph/dashboard/osd-summary.pipe.spec.ts | 8 + .../app/ceph/dashboard/osd-summary.pipe.ts | 26 +++ .../dashboard/pg-status-style.pipe.spec.ts | 8 + .../ceph/dashboard/pg-status-style.pipe.ts | 40 ++++ .../app/ceph/dashboard/pg-status.pipe.spec.ts | 8 + .../src/app/ceph/dashboard/pg-status.pipe.ts | 16 ++ .../app/core/auth/login/login.component.ts | 2 +- .../navigation/navigation.component.html | 10 +- .../navigation/navigation.component.ts | 10 +- .../{credentials.model.ts => credentials.ts} | 0 .../shared/pipes/dimless-binary.pipe.spec.ts | 10 + .../app/shared/pipes/dimless-binary.pipe.ts | 20 ++ .../src/app/shared/pipes/dimless.pipe.spec.ts | 10 + .../src/app/shared/pipes/dimless.pipe.ts | 20 ++ .../shared/pipes/health-color.pipe.spec.ts | 8 + .../src/app/shared/pipes/health-color.pipe.ts | 18 ++ .../src/app/shared/pipes/pipes.module.ts | 19 +- .../src/app/shared/services/auth.service.ts | 2 +- .../shared/services/formatter.service.spec.ts | 15 ++ .../app/shared/services/formatter.service.ts | 51 ++++ .../app/shared/services/services.module.ts | 14 ++ .../shared/services/top-level.service.spec.ts | 21 ++ .../app/shared/services/top-level.service.ts | 32 +++ .../frontend/src/app/shared/shared.module.ts | 10 +- .../mgr/dashboard_v2/tests/test_dashboard.py | 40 ++++ 51 files changed, 1401 insertions(+), 48 deletions(-) create mode 100644 src/pybind/mgr/dashboard_v2/controllers/dashboard.py create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.ts rename src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/{credentials.model.ts => credentials.ts} (100%) create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/services.module.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/top-level.service.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/top-level.service.ts create mode 100644 src/pybind/mgr/dashboard_v2/tests/test_dashboard.py diff --git a/src/pybind/mgr/dashboard_v2/controllers/dashboard.py b/src/pybind/mgr/dashboard_v2/controllers/dashboard.py new file mode 100644 index 00000000000..ea1c06d98f4 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/controllers/dashboard.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import collections +from collections import defaultdict +import json +import time + +import cherrypy +from mgr_module import CommandResult + +from ..tools import ApiController, AuthRequired, BaseController, NotificationQueue + + +LOG_BUFFER_SIZE = 30 + + +@ApiController('dashboard') +@AuthRequired() +class Dashboard(BaseController): + def __init__(self): + super(Dashboard, self).__init__() + + self._log_initialized = False + + self.log_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE) + self.audit_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE) + + def append_log(self, log_struct): + if log_struct['channel'] == "audit": + self.audit_buffer.appendleft(log_struct) + else: + self.log_buffer.appendleft(log_struct) + + def load_buffer(self, buf, channel_name): + result = CommandResult("") + self.mgr.send_command(result, "mon", "", json.dumps({ + "prefix": "log last", + "format": "json", + "channel": channel_name, + "num": LOG_BUFFER_SIZE + }), "") + r, outb, outs = result.wait() + if r != 0: + # Oh well. We won't let this stop us though. + self.log.error("Error fetching log history (r={0}, \"{1}\")".format( + r, outs)) + else: + try: + lines = json.loads(outb) + except ValueError: + self.log.error("Error decoding log history") + else: + for l in lines: + buf.appendleft(l) + + @cherrypy.expose + @cherrypy.tools.json_out() + def toplevel(self): + fsmap = self.mgr.get("fs_map") + + filesystems = [ + { + "id": f['id'], + "name": f['mdsmap']['fs_name'] + } + for f in fsmap['filesystems'] + ] + + return { + 'health_status': self.health_data()['status'], + 'filesystems': filesystems, + } + + # pylint: disable=R0914 + @cherrypy.expose + @cherrypy.tools.json_out() + def health(self): + if not self._log_initialized: + self._log_initialized = True + + self.load_buffer(self.log_buffer, "cluster") + self.load_buffer(self.audit_buffer, "audit") + + NotificationQueue.register(self.append_log, 'clog') + + # Fuse osdmap with pg_summary to get description of pools + # including their PG states + + osd_map = self.osd_map() + + pg_summary = self.mgr.get("pg_summary") + + pools = [] + + pool_stats = defaultdict(lambda: defaultdict( + lambda: collections.deque(maxlen=10))) + + df = self.mgr.get("df") + pool_stats_dict = dict([(p['id'], p['stats']) for p in df['pools']]) + now = time.time() + for pool_id, stats in pool_stats_dict.items(): + for stat_name, stat_val in stats.items(): + pool_stats[pool_id][stat_name].appendleft((now, stat_val)) + + for pool in osd_map['pools']: + pool['pg_status'] = pg_summary['by_pool'][pool['pool'].__str__()] + stats = pool_stats[pool['pool']] + s = {} + + def get_rate(series): + if len(series) >= 2: + return (float(series[0][1]) - float(series[1][1])) / \ + (float(series[0][0]) - float(series[1][0])) + return 0 + + for stat_name, stat_series in stats.items(): + s[stat_name] = { + 'latest': stat_series[0][1], + 'rate': get_rate(stat_series), + 'series': [i for i in stat_series] + } + pool['stats'] = s + pools.append(pool) + + # Not needed, skip the effort of transmitting this + # to UI + del osd_map['pg_temp'] + + df['stats']['total_objects'] = sum( + [p['stats']['objects'] for p in df['pools']]) + + return { + "health": self.health_data(), + "mon_status": self.mon_status(), + "fs_map": self.mgr.get('fs_map'), + "osd_map": osd_map, + "clog": list(self.log_buffer), + "audit_log": list(self.audit_buffer), + "pools": pools, + "mgr_map": self.mgr.get("mgr_map"), + "df": df + } + + def mon_status(self): + mon_status_data = self.mgr.get("mon_status") + return json.loads(mon_status_data['json']) + + def osd_map(self): + osd_map = self.mgr.get("osd_map") + + assert osd_map is not None + + osd_map['tree'] = self.mgr.get("osd_map_tree") + osd_map['crush'] = self.mgr.get("osd_map_crush") + osd_map['crush_map_text'] = self.mgr.get("osd_map_crush_map_text") + osd_map['osd_metadata'] = self.mgr.get("osd_metadata") + + return osd_map + + def health_data(self): + health_data = self.mgr.get("health") + health = json.loads(health_data['json']) + + # Transform the `checks` dict into a list for the convenience + # of rendering from javascript. + checks = [] + for k, v in health['checks'].items(): + v['type'] = k + checks.append(v) + + checks = sorted(checks, key=lambda c: c['severity']) + + health['checks'] = checks + + return health diff --git a/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json b/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json index c623375b8c6..6ffd7b7d6e8 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json +++ b/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json @@ -25,7 +25,9 @@ "../node_modules/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css", "styles.scss" ], - "scripts": [], + "scripts": [ + "../node_modules/chart.js/dist/Chart.bundle.js" + ], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", diff --git a/src/pybind/mgr/dashboard_v2/frontend/package.json b/src/pybind/mgr/dashboard_v2/frontend/package.json index 2e0d90b1758..c622bfba44b 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/package.json +++ b/src/pybind/mgr/dashboard_v2/frontend/package.json @@ -21,11 +21,15 @@ "@angular/platform-browser": "^5.0.0", "@angular/platform-browser-dynamic": "^5.0.0", "@angular/router": "^5.0.0", + "@types/lodash": "^4.14.95", "awesome-bootstrap-checkbox": "0.3.7", "@swimlane/ngx-datatable": "^11.1.7", "bootstrap": "^3.3.7", + "chart.js": "^2.7.1", "core-js": "^2.4.1", "font-awesome": "4.7.0", + "lodash": "^4.17.4", + "ng2-charts": "^1.6.0", "ng2-toastr": "4.1.2", "ngx-bootstrap": "^2.0.1", "rxjs": "^5.5.2", diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts index 628d5b54c2d..237867c83ec 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts @@ -2,17 +2,23 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HostsComponent } from './ceph/cluster/hosts/hosts.component'; +import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component'; import { LoginComponent } from './core/auth/login/login.component'; import { AuthGuardService } from './shared/services/auth-guard.service'; const routes: Routes = [ - { path: '', redirectTo: 'hosts', pathMatch: 'full' }, + { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, + { + path: 'dashboard', + component: DashboardComponent, + canActivate: [AuthGuardService] + }, { path: 'login', component: LoginComponent }, { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] } ]; @NgModule({ - imports: [RouterModule.forRoot(routes, {useHash: true})], + imports: [RouterModule.forRoot(routes, { useHash: true })], exports: [RouterModule] }) -export class AppRoutingModule { } +export class AppRoutingModule {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts index 88ddd4562d5..e7925a0743f 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts @@ -9,30 +9,18 @@ import { CoreModule } from './core/core.module'; import { SharedModule } from './shared/shared.module'; describe('AppComponent', () => { - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - RouterTestingModule, - CoreModule, - SharedModule, - ToastModule.forRoot(), - ClusterModule - ], - declarations: [ - AppComponent - ], - }).compileComponents(); - })); - - it('should create the app', async(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - })); - - it(`should have as title 'oa'`, async(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app.title).toEqual('cd'); - })); + beforeEach( + async(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + CoreModule, + SharedModule, + ToastModule.forRoot(), + ClusterModule + ], + declarations: [AppComponent] + }).compileComponents(); + }) + ); }); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts index 99186e5394f..3211acdc618 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts @@ -31,7 +31,9 @@ export class CustomOption extends ToastOptions { HttpClientModule, CoreModule, SharedModule, - CephModule + CephModule, + HttpClientModule, + BrowserAnimationsModule ], exports: [SharedModule], providers: [ diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts index af7e4662721..00f9548728c 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts @@ -2,11 +2,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ClusterModule } from './cluster/cluster.module'; +import { DashboardModule } from './dashboard/dashboard.module'; @NgModule({ imports: [ CommonModule, - ClusterModule + ClusterModule, + DashboardModule ], declarations: [] }) diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts new file mode 100644 index 00000000000..d2f70a80a76 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts @@ -0,0 +1,34 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { ChartsModule } from 'ng2-charts'; +import { TabsModule } from 'ngx-bootstrap/tabs'; + +import { SharedModule } from '../../shared/shared.module'; +import { DashboardService } from './dashboard.service'; +import { DashboardComponent } from './dashboard/dashboard.component'; +import { HealthComponent } from './health/health.component'; +import { LogColorPipe } from './log-color.pipe'; +import { MdsSummaryPipe } from './mds-summary.pipe'; +import { MgrSummaryPipe } from './mgr-summary.pipe'; +import { MonSummaryPipe } from './mon-summary.pipe'; +import { OsdSummaryPipe } from './osd-summary.pipe'; +import { PgStatusStylePipe } from './pg-status-style.pipe'; +import { PgStatusPipe } from './pg-status.pipe'; + +@NgModule({ + imports: [CommonModule, TabsModule.forRoot(), SharedModule, ChartsModule], + declarations: [ + HealthComponent, + DashboardComponent, + MonSummaryPipe, + OsdSummaryPipe, + LogColorPipe, + MgrSummaryPipe, + PgStatusPipe, + MdsSummaryPipe, + PgStatusStylePipe + ], + providers: [DashboardService] +}) +export class DashboardModule {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts new file mode 100644 index 00000000000..bf061e9f645 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts @@ -0,0 +1,23 @@ +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; + +import { appendFile } from 'fs'; + +import { DashboardService } from './dashboard.service'; + +describe('DashboardService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DashboardService], + imports: [HttpClientTestingModule, HttpClientModule] + }); + }); + + it( + 'should be created', + inject([DashboardService], (service: DashboardService) => { + expect(service).toBeTruthy(); + }) + ); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.ts new file mode 100644 index 00000000000..807983cb25e --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.ts @@ -0,0 +1,11 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class DashboardService { + constructor(private http: HttpClient) {} + + getHealth() { + return this.http.get('/api/dashboard/health'); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html new file mode 100644 index 00000000000..10b37138abb --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html @@ -0,0 +1,10 @@ +
+ + + + + + + + +
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss new file mode 100644 index 00000000000..04eee2d6f25 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss @@ -0,0 +1,3 @@ +div { + padding-top: 20px; +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts new file mode 100644 index 00000000000..80500c0b9ae --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DashboardComponent } from './dashboard.component'; + +describe('DashboardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DashboardComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts new file mode 100644 index 00000000000..fc676c74c6d --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts @@ -0,0 +1,16 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'cd-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'] +}) +export class DashboardComponent implements OnInit { + hasGrafana = false; // TODO: Temporary var, remove when grafana is implemented + + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html new file mode 100644 index 00000000000..533d493176a --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html @@ -0,0 +1,219 @@ +
+
+ +
+
+
+ Health + Overall status: + {{ contentData.health.status }} +
    +
  • + {{ check.type }}: {{ check.summary.message }} +
  • +
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+ Monitors + {{ contentData.mon_status | monSummary }} +
+
+
+
+
+
+
+
+ +
+
+ OSDs + {{ contentData.osd_map | osdSummary }} +
+
+
+
+
+
+
+
+
+
+ +
+
+ Metadata servers + {{ contentData.fs_map | mdsSummary }} +
+
+
+
+
+
+
+
+ +
+
+ Manager daemons + {{ contentData.mgr_map | mgrSummary }} +
+
+
+
+
+
+
+ +
+ +
+
+
+ Usage + + + + + + + + + + + + +
+ {{ contentData.df.stats.total_objects | dimless }} + +
+ +
+
+
+ +
+
Objects + Raw capacity +
+ ({{ contentData.df.stats.total_used_bytes | dimlessBinary }} used) +
Usage by pool
+
+
+
+ +
+
+
+ Pools + + + + + + + + + + + + + + + + + + + + + +
NamePG statusUsageReadWrite
{{ pool.pool_name }} + {{ pool.pg_status | pgStatus }} + + {{ pool.stats.bytes_used.latest | dimlessBinary }} / {{ pool.stats.max_avail.latest | dimlessBinary }} + + {{ pool.stats.rd_bytes.rate | dimless }} + + {{ pool.stats.rd.rate | dimless }} ops + + {{ pool.stats.wr_bytes.rate | dimless }} + + {{ pool.stats.wr.rate | dimless }} ops +
+
+
+
+
+ +
+
+ +
+
+ Logs + + + + + {{ line.stamp }} {{ line.priority }}  + + {{ line.message }} +
+
+
+
+ + + {{ line.stamp }} {{ line.priority }}  + + + {{ line.message }} + +
+
+
+
+
+
+
+
+
+
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.scss new file mode 100644 index 00000000000..919b41d10fc --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.scss @@ -0,0 +1,62 @@ +table.ceph-chartbox { + width: 100%; + + td { + text-align: center; + font-weight: bold; + } +} + +.center-block { + width: 120px; +} + +.pie { + height: 120px; + width: 120px; +} + +.media { + display: block; + min-height: 60px; + width: 100%; + + .media-left { + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; + display: block; + float: left; + height: 60px; + width: 60px; + text-align: center; + font-size: 40px; + line-height: 60px; + padding-right: 0; + + .fa { + font-size: 45px; + } + } + + .media-body { + padding: 5px 10px; + margin-left: 60px; + + .media-heading { + text-transform: uppercase; + display: block; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .media-text { + display: block; + font-weight: bold; + font-size: 18px; + } + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts new file mode 100644 index 00000000000..cac806a0f77 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts @@ -0,0 +1,43 @@ +import { HttpClientModule } from '@angular/common/http'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChartsModule } from 'ng2-charts'; +import { TabsModule } from 'ngx-bootstrap/tabs'; + +import { AppModule } from '../../../app.module'; +import { SharedModule } from '../../../shared/shared.module'; +import { DashboardService } from '../dashboard.service'; +import { LogColorPipe } from '../log-color.pipe'; +import { MdsSummaryPipe } from '../mds-summary.pipe'; +import { MgrSummaryPipe } from '../mgr-summary.pipe'; +import { MonSummaryPipe } from '../mon-summary.pipe'; +import { OsdSummaryPipe } from '../osd-summary.pipe'; +import { PgStatusStylePipe } from '../pg-status-style.pipe'; +import { PgStatusPipe } from '../pg-status.pipe'; +import { HealthComponent } from './health.component'; + +describe('HealthComponent', () => { + let component: HealthComponent; + let fixture: ComponentFixture; + const dashboardServiceStub = { + getHealth() { + return {}; + } + }; + beforeEach( + async(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: DashboardService, useValue: dashboardServiceStub } + ], + imports: [AppModule] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts new file mode 100644 index 00000000000..0a065c10bd3 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts @@ -0,0 +1,210 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; + +import * as Chart from 'chart.js'; +import * as _ from 'lodash'; + +import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; +import { DashboardService } from '../dashboard.service'; + +@Component({ + selector: 'cd-health', + templateUrl: './health.component.html', + styleUrls: ['./health.component.scss'] +}) +export class HealthComponent implements OnInit, OnDestroy { + contentData: any; + interval: any; + poolUsage: any = { + chartType: 'doughnut' + }; + rawUsage: any = { + chartType: 'doughnut', + center_text: 0 + }; + + constructor( + private dimlessBinary: DimlessBinaryPipe, + private dashboardService: DashboardService + ) {} + + ngOnInit() { + // An extension to Chart.js to enable rendering some + // text in the middle of a doughnut + Chart.pluginService.register({ + beforeDraw: function(chart) { + if (!chart.options.center_text) { + return; + } + const width = chart.chart.width, + height = chart.chart.height, + ctx = chart.chart.ctx; + + ctx.restore(); + const fontSize = (height / 114).toFixed(2); + ctx.font = fontSize + 'em sans-serif'; + ctx.textBaseline = 'middle'; + + const text = chart.options.center_text, + textX = Math.round((width - ctx.measureText(text).width) / 2), + textY = height / 2; + + ctx.fillText(text, textX, textY); + ctx.save(); + } + }); + + this.getInfo(); + this.interval = setInterval(() => { + this.getInfo(); + }, 5000); + } + + ngOnDestroy() { + clearInterval(this.interval); + } + + getInfo() { + this.dashboardService.getHealth().subscribe((data: any) => { + this.contentData = data; + this.draw_usage_charts(); + }); + } + + draw_usage_charts() { + let rawUsageChartColor; + const rawUsageText = + Math.round( + 100 * + (this.contentData.df.stats.total_used_bytes / + this.contentData.df.stats.total_bytes) + ) + '%'; + if ( + this.contentData.df.stats.total_used_bytes / + this.contentData.df.stats.total_bytes >= + this.contentData.osd_map.full_ratio + ) { + rawUsageChartColor = '#ff0000'; + } else if ( + this.contentData.df.stats.total_used_bytes / + this.contentData.df.stats.total_bytes >= + this.contentData.osd_map.backfillfull_ratio + ) { + rawUsageChartColor = '#ff6600'; + } else if ( + this.contentData.df.stats.total_used_bytes / + this.contentData.df.stats.total_bytes >= + this.contentData.osd_map.nearfull_ratio + ) { + rawUsageChartColor = '#ffc200'; + } else { + rawUsageChartColor = '#00bb00'; + } + + this.rawUsage = { + chartType: 'doughnut', + dataset: [ + { + label: null, + borderWidth: 0, + data: [ + this.contentData.df.stats.total_used_bytes, + this.contentData.df.stats.total_avail_bytes + ] + } + ], + options: { + center_text: rawUsageText, + responsive: true, + legend: { display: false }, + animation: { duration: 0 }, + tooltips: { + callbacks: { + label: (tooltipItem, chart) => { + return ( + chart.labels[tooltipItem.index] + + ': ' + + this.dimlessBinary.transform( + chart.datasets[0].data[tooltipItem.index] + ) + ); + } + } + } + }, + colors: [ + { + backgroundColor: [rawUsageChartColor, '#424d52'], + borderColor: 'transparent' + } + ], + labels: ['Raw Used', 'Raw Available'] + }; + + const colors = [ + '#3366CC', + '#109618', + '#990099', + '#3B3EAC', + '#0099C6', + '#DD4477', + '#66AA00', + '#B82E2E', + '#316395', + '#994499', + '#22AA99', + '#AAAA11', + '#6633CC', + '#E67300', + '#8B0707', + '#329262', + '#5574A6', + '#FF9900', + '#DC3912', + '#3B3EAC' + ]; + + const poolLabels = []; + const poolData = []; + + _.each(this.contentData.df.pools, function(pool, i) { + poolLabels.push(pool['name']); + poolData.push(pool['stats']['bytes_used']); + }); + + this.poolUsage = { + chartType: 'doughnut', + dataset: [ + { + label: null, + borderWidth: 0, + data: poolData + } + ], + options: { + responsive: true, + legend: { display: false }, + animation: { duration: 0 }, + tooltips: { + callbacks: { + label: (tooltipItem, chart) => { + return ( + chart.labels[tooltipItem.index] + + ': ' + + this.dimlessBinary.transform( + chart.datasets[0].data[tooltipItem.index] + ) + ); + } + } + } + }, + colors: [ + { + backgroundColor: colors, + borderColor: 'transparent' + } + ], + labels: poolLabels + }; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts new file mode 100644 index 00000000000..43af68d08af --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts @@ -0,0 +1,8 @@ +import { LogColorPipe } from './log-color.pipe'; + +describe('LogColorPipe', () => { + it('create an instance', () => { + const pipe = new LogColorPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.ts new file mode 100644 index 00000000000..eb60ddba4c3 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.ts @@ -0,0 +1,21 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'logColor' +}) +export class LogColorPipe implements PipeTransform { + transform(value: any, args?: any): any { + if (value.priority === '[INF]') { + return ''; // Inherit + } else if (value.priority === '[WRN]') { + return { + color: '#ffa500', + 'font-weight': 'bold' + }; + } else if (value.priority === '[ERR]') { + return { color: '#FF2222' }; + } else { + return ''; + } + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts new file mode 100644 index 00000000000..37883a82a98 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts @@ -0,0 +1,8 @@ +import { MdsSummaryPipe } from './mds-summary.pipe'; + +describe('MdsSummaryPipe', () => { + it('create an instance', () => { + const pipe = new MdsSummaryPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts new file mode 100644 index 00000000000..9e6eeca6e8a --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts @@ -0,0 +1,38 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import * as _ from 'lodash'; + +@Pipe({ + name: 'mdsSummary' +}) +export class MdsSummaryPipe implements PipeTransform { + transform(value: any, args?: any): any { + if (!value) { + return ''; + } + + let standbys = 0; + let active = 0; + let standbyReplay = 0; + _.each(value.standbys, (s, i) => { + standbys += 1; + }); + + if (value.standbys && !value.filesystems) { + return standbys + ', no filesystems'; + } else if (value.filesystems.length === 0) { + return 'no filesystems'; + } else { + _.each(value.filesystems, (fs, i) => { + _.each(fs.mdsmap.info, (mds, j) => { + if (mds.state === 'up:standby-replay') { + standbyReplay += 1; + } else { + active += 1; + } + }); + }); + + return active + ' active, ' + (standbys + standbyReplay) + ' standby'; + } + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts new file mode 100644 index 00000000000..fdab76c4808 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts @@ -0,0 +1,8 @@ +import { MgrSummaryPipe } from './mgr-summary.pipe'; + +describe('MgrSummaryPipe', () => { + it('create an instance', () => { + const pipe = new MgrSummaryPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts new file mode 100644 index 00000000000..cf793e66e47 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts @@ -0,0 +1,22 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import * as _ from 'lodash'; + +@Pipe({ + name: 'mgrSummary' +}) +export class MgrSummaryPipe implements PipeTransform { + transform(value: any, args?: any): any { + if (!value) { + return ''; + } + + let result = 'active: '; + result += _.isUndefined(value.active_name) ? 'n/a' : value.active_name; + + if (value.standbys.length) { + result += ', ' + value.standbys.length + ' standbys'; + } + + return result; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts new file mode 100644 index 00000000000..49526cf3fcd --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts @@ -0,0 +1,8 @@ +import { MonSummaryPipe } from './mon-summary.pipe'; + +describe('MonSummaryPipe', () => { + it('create an instance', () => { + const pipe = new MonSummaryPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts new file mode 100644 index 00000000000..6877e2247c7 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'monSummary' +}) +export class MonSummaryPipe implements PipeTransform { + transform(value: any, args?: any): any { + if (!value) { + return ''; + } + + let result = value.monmap.mons.length.toString() + ' (quorum '; + result += value.quorum.join(', '); + result += ')'; + + return result; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts new file mode 100644 index 00000000000..466eec1ac30 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts @@ -0,0 +1,8 @@ +import { OsdSummaryPipe } from './osd-summary.pipe'; + +describe('OsdSummaryPipe', () => { + it('create an instance', () => { + const pipe = new OsdSummaryPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts new file mode 100644 index 00000000000..b02d97644bb --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts @@ -0,0 +1,26 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import * as _ from 'lodash'; + +@Pipe({ + name: 'osdSummary' +}) +export class OsdSummaryPipe implements PipeTransform { + transform(value: any, args?: any): any { + if (!value) { + return ''; + } + + let inCount = 0; + let upCount = 0; + _.each(value.osds, (osd, i) => { + if (osd.in) { + inCount++; + } + if (osd.up) { + upCount++; + } + }); + + return value.osds.length + ' (' + upCount + ' up, ' + inCount + ' in)'; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts new file mode 100644 index 00000000000..67c5f10c5e3 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts @@ -0,0 +1,8 @@ +import { PgStatusStylePipe } from './pg-status-style.pipe'; + +describe('PgStatusStylePipe', () => { + it('create an instance', () => { + const pipe = new PgStatusStylePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts new file mode 100644 index 00000000000..4e9afab97cf --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts @@ -0,0 +1,40 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import * as _ from 'lodash'; + +@Pipe({ + name: 'pgStatusStyle' +}) +export class PgStatusStylePipe implements PipeTransform { + transform(pgStatus: any, args?: any): any { + let warning = false; + let error = false; + + _.each(pgStatus, (value, state) => { + if ( + state.includes('inconsistent') || + state.includes('incomplete') || + !state.includes('active') + ) { + error = true; + } + + if ( + state !== 'active+clean' && + state !== 'active+clean+scrubbing' && + state !== 'active+clean+scrubbing+deep' + ) { + warning = true; + } + }); + + if (error) { + return { color: '#FF0000' }; + } + + if (warning) { + return { color: '#FFC200' }; + } + + return { color: '#00BB00' }; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts new file mode 100644 index 00000000000..d7d5592b653 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts @@ -0,0 +1,8 @@ +import { PgStatusPipe } from './pg-status.pipe'; + +describe('PgStatusPipe', () => { + it('create an instance', () => { + const pipe = new PgStatusPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.ts new file mode 100644 index 00000000000..5c6c7b393c3 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import * as _ from 'lodash'; + +@Pipe({ + name: 'pgStatus' +}) +export class PgStatusPipe implements PipeTransform { + transform(pgStatus: any, args?: any): any { + const strings = []; + _.each(pgStatus, (count, state) => { + strings.push(count + ' ' + state); + }); + + return strings.join(', '); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts index e68609f714f..f8f46254976 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { ToastsManager } from 'ng2-toastr'; -import { Credentials } from '../../../shared/models/credentials.model'; +import { Credentials } from '../../../shared/models/credentials'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; import { AuthService } from '../../../shared/services/auth.service'; diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html index e1338e3e4ee..e93c304f21a 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -25,13 +25,17 @@