From 5f4823fc84b63678cc2ce901fe63071160d410ba Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Mon, 5 Feb 2018 14:56:46 +0100 Subject: [PATCH] mgr/dashboard_v2: Add RGW page. Signed-off-by: Volker Theile --- .../dashboard_v2/controllers/perf_counters.py | 62 ++++++++++++++++++ .../frontend/src/app/app-routing.module.ts | 6 ++ .../frontend/src/app/ceph/ceph.module.ts | 2 + .../ceph/cluster/hosts/hosts.component.html | 3 +- .../performance-counter.module.ts | 23 +++++++ .../table-performance-counter.service.spec.ts | 21 +++++++ .../table-performance-counter.service.ts | 26 ++++++++ .../table-performance-counter.component.html | 10 +++ .../table-performance-counter.component.scss | 0 ...able-performance-counter.component.spec.ts | 35 +++++++++++ .../table-performance-counter.component.ts | 60 ++++++++++++++++++ .../rgw-daemon-details.component.html | 11 ++++ .../rgw-daemon-details.component.scss | 0 .../rgw-daemon-details.component.spec.ts | 40 ++++++++++++ .../rgw-daemon-details.component.ts | 47 ++++++++++++++ .../rgw-daemon-list.component.html | 11 ++++ .../rgw-daemon-list.component.scss | 0 .../rgw-daemon-list.component.spec.ts | 35 +++++++++++ .../rgw-daemon-list.component.ts | 54 ++++++++++++++++ .../frontend/src/app/ceph/rgw/rgw.module.ts | 34 ++++++++++ .../rgw/services/rgw-daemon.service.spec.ts | 21 +++++++ .../ceph/rgw/services/rgw-daemon.service.ts | 26 ++++++++ .../navigation/navigation.component.html | 23 +++++-- .../shared/components/components.module.ts | 19 +++--- .../components/datatable/datatable.module.ts | 30 +++++++++ .../table-details.directive.spec.ts | 0 .../table-details.directive.ts | 0 .../table-key-value.component.html | 8 +++ .../table-key-value.component.scss | 0 .../table-key-value.component.spec.ts | 30 +++++++++ .../table-key-value.component.ts | 57 +++++++++++++++++ .../table/table.component.html | 7 ++- .../table/table.component.scss | 2 +- .../table/table.component.spec.ts | 0 .../{ => datatable}/table/table.component.ts | 63 +++++++++++++++---- .../frontend/src/app/shared/shared.module.ts | 1 + .../frontend/src/openattic-theme.scss | 7 ++- src/pybind/mgr/dashboard_v2/module.py | 1 - .../dashboard_v2/tests/test_perf_counters.py | 37 +++++++++++ 39 files changed, 775 insertions(+), 37 deletions(-) create mode 100644 src/pybind/mgr/dashboard_v2/controllers/perf_counters.py create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter.module.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw.module.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/datatable.module.ts rename src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/{table => datatable}/table-details.directive.spec.ts (100%) rename src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/{table => datatable}/table-details.directive.ts (100%) create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table-key-value/table-key-value.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table-key-value/table-key-value.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table-key-value/table-key-value.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table-key-value/table-key-value.component.ts rename src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/{ => datatable}/table/table.component.html (92%) rename src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/{ => datatable}/table/table.component.scss (99%) rename src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/{ => datatable}/table/table.component.spec.ts (100%) rename src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/{ => datatable}/table/table.component.ts (60%) create mode 100644 src/pybind/mgr/dashboard_v2/tests/test_perf_counters.py diff --git a/src/pybind/mgr/dashboard_v2/controllers/perf_counters.py b/src/pybind/mgr/dashboard_v2/controllers/perf_counters.py new file mode 100644 index 00000000000..276b7e318ef --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/controllers/perf_counters.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from ..tools import ApiController, AuthRequired, RESTController + + +class PerfCounter(RESTController): + def __init__(self, service_type, mgr): + PerfCounter.mgr = mgr + self._service_type = service_type + + def _get_rate(self, daemon_type, daemon_name, stat): + data = self.mgr.get_counter(daemon_type, daemon_name, stat)[stat] + if data and len(data) > 1: + return (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0]) + return 0 + + def _get_latest(self, daemon_type, daemon_name, stat): + data = self.mgr.get_counter(daemon_type, daemon_name, stat)[stat] + if data: + return data[-1][1] + return 0 + + def get(self, service_id): + schema = self.mgr.get_perf_schema( + self._service_type, str(service_id)).values()[0] + counters = [] + + for key, value in sorted(schema.items()): + counter = dict() + counter['name'] = str(key) + counter['description'] = value['description'] + # pylint: disable=W0212 + if self.mgr._stattype_to_str(value['type']) == 'counter': + counter['value'] = self._get_rate( + self._service_type, service_id, key) + counter['unit'] = '/s' + else: + counter['value'] = self._get_latest( + self._service_type, service_id, key) + counter['unit'] = '' + counters.append(counter) + + return { + 'service': { + 'type': self._service_type, + 'id': service_id + }, + 'counters': counters + } + + +@ApiController('perf_counters') +@AuthRequired() +class PerfCounters(RESTController): + def __init__(self): + self.mon = PerfCounter('mon', self.mgr) + self.osd = PerfCounter('osd', self.mgr) + self.rgw = PerfCounter('rgw', self.mgr) + + def list(self): + counters = self.mgr.get_all_perf_counters() + return counters 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 231da6ae790..490bce77f89 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 @@ -4,6 +4,7 @@ import { RouterModule, Routes } from '@angular/router'; import { PoolDetailComponent } from './ceph/block/pool-detail/pool-detail.component'; import { HostsComponent } from './ceph/cluster/hosts/hosts.component'; import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component'; +import { RgwDaemonListComponent } from './ceph/rgw/rgw-daemon-list/rgw-daemon-list.component'; import { LoginComponent } from './core/auth/login/login.component'; import { AuthGuardService } from './shared/services/auth-guard.service'; @@ -16,6 +17,11 @@ const routes: Routes = [ }, { path: 'login', component: LoginComponent }, { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] }, + { + path: 'rgw', + component: RgwDaemonListComponent, + canActivate: [AuthGuardService] + }, { path: 'block/pool/:name', component: PoolDetailComponent, canActivate: [AuthGuardService] } ]; 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 2bdca2340d5..97439a43678 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 @@ -4,12 +4,14 @@ import { NgModule } from '@angular/core'; import { BlockModule } from './block/block.module'; import { ClusterModule } from './cluster/cluster.module'; import { DashboardModule } from './dashboard/dashboard.module'; +import { RgwModule } from './rgw/rgw.module'; @NgModule({ imports: [ CommonModule, ClusterModule, DashboardModule, + RgwModule, BlockModule ], declarations: [] diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.html index 2ad38a7c2a0..de0524977ce 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.html +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.html @@ -8,5 +8,6 @@ [columns]="columns" columnMode="flex" [toolHeader]="false" - [footer]="false"> + [footer]="false" + [limit]="0"> diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter.module.ts new file mode 100644 index 00000000000..18ff9ca891c --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { SharedModule } from '../../shared/shared.module'; +import { TablePerformanceCounterService } from './services/table-performance-counter.service'; +import { TablePerformanceCounterComponent } from './table-performance-counter/table-performance-counter.component'; // tslint:disable-line + +@NgModule({ + imports: [ + CommonModule, + SharedModule + ], + declarations: [ + TablePerformanceCounterComponent + ], + providers: [ + TablePerformanceCounterService + ], + exports: [ + TablePerformanceCounterComponent + ] +}) +export class PerformanceCounterModule { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts new file mode 100644 index 00000000000..37350f76b24 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts @@ -0,0 +1,21 @@ +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; + +import { TablePerformanceCounterService } from './table-performance-counter.service'; + +describe('TablePerformanceCounterService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [TablePerformanceCounterService], + imports: [HttpClientTestingModule, HttpClientModule] + }); + }); + + it( + 'should be created', + inject([TablePerformanceCounterService], (service: TablePerformanceCounterService) => { + expect(service).toBeTruthy(); + }) + ); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts new file mode 100644 index 00000000000..2f6ecf3e16d --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts @@ -0,0 +1,26 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class TablePerformanceCounterService { + + private url = '/api/perf_counters'; + + constructor(private http: HttpClient) { } + + list() { + return this.http.get(this.url) + .toPromise() + .then((resp: object): object => { + return resp; + }); + } + + get(service_type: string, service_id: string) { + return this.http.get(`${this.url}/${service_type}/${service_id}`) + .toPromise() + .then((resp: object): Array => { + return resp['counters']; + }); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html new file mode 100644 index 00000000000..dd954792d83 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html @@ -0,0 +1,10 @@ + + + {{ row.value | dimless }} {{ row.unit }} + + diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts new file mode 100644 index 00000000000..dae55c6880e --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts @@ -0,0 +1,35 @@ +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SharedModule } from '../../../shared/shared.module'; +import { TablePerformanceCounterService } from '../services/table-performance-counter.service'; +import { TablePerformanceCounterComponent } from './table-performance-counter.component'; + +describe('TablePerformanceCounterComponent', () => { + let component: TablePerformanceCounterComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TablePerformanceCounterComponent ], + imports: [ + HttpClientTestingModule, + HttpClientModule, + SharedModule + ], + providers: [ TablePerformanceCounterService ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TablePerformanceCounterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts new file mode 100644 index 00000000000..24ef9487fd9 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts @@ -0,0 +1,60 @@ +import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core'; + +import { CdTableColumn } from '../../../shared/models/cd-table-column'; +import { TablePerformanceCounterService } from '../services/table-performance-counter.service'; + +/** + * Display the specified performance counters in a datatable. + */ +@Component({ + selector: 'cd-table-performance-counter', + templateUrl: './table-performance-counter.component.html', + styleUrls: ['./table-performance-counter.component.scss'] +}) +export class TablePerformanceCounterComponent implements OnInit { + + private columns: Array = []; + private counters: Array = []; + + @ViewChild('valueTpl') public valueTpl: TemplateRef; + + /** + * The service type, e.g. 'rgw', 'mds', 'mon', 'osd', ... + */ + @Input() serviceType: string; + + /** + * The service identifier. + */ + @Input() serviceId: string; + + constructor(private performanceCounterService: TablePerformanceCounterService) { } + + ngOnInit() { + this.columns = [ + { + name: 'Name', + prop: 'name', + flexGrow: 1 + }, + { + name: 'Description', + prop: 'description', + flexGrow: 1 + }, + { + name: 'Value', + cellTemplate: this.valueTpl, + flexGrow: 1 + } + ]; + this.getCounters(); + } + + getCounters() { + this.performanceCounterService.get(this.serviceType, this.serviceId) + .then((resp) => { + this.counters = resp; + }); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html new file mode 100644 index 00000000000..83fd492a7b2 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts new file mode 100644 index 00000000000..d48f1cce51f --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts @@ -0,0 +1,40 @@ +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TabsModule } from 'ngx-bootstrap/tabs'; + +import { SharedModule } from '../../../shared/shared.module'; +import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module'; +import { RgwDaemonService } from '../services/rgw-daemon.service'; +import { RgwDaemonDetailsComponent } from './rgw-daemon-details.component'; + +describe('RgwDaemonDetailsComponent', () => { + let component: RgwDaemonDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RgwDaemonDetailsComponent ], + imports: [ + SharedModule, + PerformanceCounterModule, + HttpClientTestingModule, + HttpClientModule, + TabsModule.forRoot() + ], + providers: [ RgwDaemonService ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwDaemonDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts new file mode 100644 index 00000000000..d9f8ff52617 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts @@ -0,0 +1,47 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import * as _ from 'lodash'; + +import { RgwDaemonService } from '../services/rgw-daemon.service'; + +@Component({ + selector: 'cd-rgw-daemon-details', + templateUrl: './rgw-daemon-details.component.html', + styleUrls: ['./rgw-daemon-details.component.scss'] +}) +export class RgwDaemonDetailsComponent implements OnInit { + + private metadata: Array = []; + private serviceId = ''; + + @Input() selected?: Array = []; + + constructor(private rgwDaemonService: RgwDaemonService) { } + + ngOnInit() { + this.getMetaData(); + } + + private getMetaData() { + if (this.selected.length < 1) { + return; + } + + // Get the service id of the first selected row. + this.serviceId = this.selected[0].id; + + this.rgwDaemonService.get(this.serviceId) + .then((resp) => { + const metadata = []; + const keys = _.keys(resp['rgw_metadata']); + keys.sort(); + _.map(keys, (key) => { + metadata.push({ + 'key': key, + 'value': resp['rgw_metadata'][key] + }); + }); + this.metadata = metadata; + }); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html new file mode 100644 index 00000000000..77814b73ebb --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html @@ -0,0 +1,11 @@ + + + diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts new file mode 100644 index 00000000000..cc76da0f113 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts @@ -0,0 +1,35 @@ +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ComponentsModule } from '../../../shared/components/components.module'; +import { RgwDaemonService } from '../services/rgw-daemon.service'; +import { RgwDaemonListComponent } from './rgw-daemon-list.component'; + +describe('RgwDaemonListComponent', () => { + let component: RgwDaemonListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RgwDaemonListComponent ], + imports: [ + ComponentsModule, + HttpClientTestingModule, + HttpClientModule + ], + providers: [ RgwDaemonService ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwDaemonListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts new file mode 100644 index 00000000000..35b8e900217 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts @@ -0,0 +1,54 @@ +import { Component, OnInit } from '@angular/core'; + +import { CdTableColumn } from '../../../shared/models/cd-table-column'; +import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe'; +import { RgwDaemonService } from '../services/rgw-daemon.service'; + +@Component({ + selector: 'cd-rgw-daemon-list', + templateUrl: './rgw-daemon-list.component.html', + styleUrls: ['./rgw-daemon-list.component.scss'] +}) +export class RgwDaemonListComponent implements OnInit { + + private columns: Array = []; + private daemons: Array = []; + + detailsComponent = 'RgwDaemonDetailsComponent'; + + constructor(private rgwDaemonService: RgwDaemonService) { + this.columns = [ + { + name: 'ID', + prop: 'id', + width: 100 + }, + { + name: 'Hostname', + prop: 'server_hostname', + width: 100 + }, + { + name: 'Version', + prop: 'version', + width: 50, + pipe: new CephShortVersionPipe() + } + ]; + } + + ngOnInit() { + this.getDaemonList(); + } + + getDaemonList() { + this.rgwDaemonService.list() + .then((resp) => { + this.daemons = resp; + }); + } + + beforeShowDetails(selected: Array) { + return selected.length === 1; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw.module.ts new file mode 100644 index 00000000000..a888940c27d --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw.module.ts @@ -0,0 +1,34 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { TabsModule } from 'ngx-bootstrap/tabs'; + +import { SharedModule } from '../../shared/shared.module'; +import { PerformanceCounterModule } from '../performance-counter/performance-counter.module'; +import { RgwDaemonDetailsComponent } from './rgw-daemon-details/rgw-daemon-details.component'; +import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.component'; +import { RgwDaemonService } from './services/rgw-daemon.service'; + +@NgModule({ + entryComponents: [ + RgwDaemonDetailsComponent + ], + imports: [ + CommonModule, + SharedModule, + PerformanceCounterModule, + TabsModule.forRoot() + ], + exports: [ + RgwDaemonListComponent, + RgwDaemonDetailsComponent + ], + declarations: [ + RgwDaemonListComponent, + RgwDaemonDetailsComponent + ], + providers: [ + RgwDaemonService + ] +}) +export class RgwModule { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts new file mode 100644 index 00000000000..691cc787a9b --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts @@ -0,0 +1,21 @@ +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; + +import { RgwDaemonService } from './rgw-daemon.service'; + +describe('RgwDaemonService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [RgwDaemonService], + imports: [HttpClientTestingModule, HttpClientModule] + }); + }); + + it( + 'should be created', + inject([RgwDaemonService], (service: RgwDaemonService) => { + expect(service).toBeTruthy(); + }) + ); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts new file mode 100644 index 00000000000..728452b9957 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts @@ -0,0 +1,26 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class RgwDaemonService { + + private url = '/api/rgw/daemon'; + + constructor(private http: HttpClient) { } + + list() { + return this.http.get(this.url) + .toPromise() + .then((resp: any) => { + return resp; + }); + } + + get(id: string) { + return this.http.get(`${this.url}/${id}`) + .toPromise() + .then((resp: any) => { + return resp; + }); + } +} 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 2d3a261ea63..98489f2d250 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 @@ -100,22 +100,33 @@ +
  • + Object Gateway + +
  • @@ -68,7 +69,7 @@ - {{ value }} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.scss similarity index 99% rename from src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.scss rename to src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.scss index 95861c8dad2..c6289884fb1 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.scss +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.scss @@ -1,4 +1,4 @@ -@import '../../../../defaults'; +@import '../../../../../defaults'; .dataTables_wrapper { margin-bottom: 25px; diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.spec.ts similarity index 100% rename from src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.spec.ts rename to src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.spec.ts diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.ts similarity index 60% rename from src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.ts rename to src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.ts index c0df5b16cf6..d2c522e9e75 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.ts @@ -1,4 +1,5 @@ import { + AfterContentChecked, Component, ComponentFactoryResolver, EventEmitter, @@ -12,26 +13,28 @@ import { } from '@angular/core'; import { DatatableComponent } from '@swimlane/ngx-datatable'; +import * as _ from 'lodash'; -import { CdTableColumn } from '../../models/cd-table-column'; -import { TableDetailsDirective } from './table-details.directive'; +import { CdTableColumn } from '../../../models/cd-table-column'; +import { TableDetailsDirective } from '../table-details.directive'; @Component({ selector: 'cd-table', templateUrl: './table.component.html', styleUrls: ['./table.component.scss'] }) -export class TableComponent implements OnInit, OnChanges { +export class TableComponent implements AfterContentChecked, OnInit, OnChanges { @ViewChild(DatatableComponent) table: DatatableComponent; @ViewChild(TableDetailsDirective) detailTemplate: TableDetailsDirective; + @ViewChild('tableCellBoldTpl') tableCellBoldTpl: TemplateRef; - // This is the array with the items to be shown + // This is the array with the items to be shown. @Input() data: any[]; // Each item -> { prop: 'attribute name', name: 'display name' } @Input() columns: CdTableColumn[]; // Method used for setting column widths. @Input() columnMode ?= 'force'; - // Name of the component fe 'TableDetailsComponent' + // Name of the component e.g. 'TableDetailsComponent' @Input() detailsComponent?: string; // Display the tool header, including reload button, pagination and search fields? @Input() toolHeader ?= true; @@ -39,15 +42,19 @@ export class TableComponent implements OnInit, OnChanges { @Input() header ?= true; // Display the table footer? @Input() footer ?= true; - // Should be the function that will update the input data + // Page size to show. Set to 0 to show unlimited number of rows. + @Input() limit ?= 10; + // An optional function that is called before the details page is show. + // The current selection is passed as function argument. To do not display + // the details page, return false. + @Input() beforeShowDetails: Function; + // Should be the function that will update the input data. @Output() fetchData = new EventEmitter(); - @ViewChild('bold') bold: TemplateRef; cellTemplates: { [key: string]: TemplateRef } = {}; - - selectable: String = undefined; + selectionType: string = undefined; search = ''; rows = []; selected = []; @@ -57,7 +64,10 @@ export class TableComponent implements OnInit, OnChanges { pagerPrevious: 'i fa fa-angle-left', pagerNext: 'i fa fa-angle-right' }; - limit = 10; + + // Internal variable to check if it is necessary to recalculate the + // table columns after the browser window has been resized. + private currentWidth: number; constructor(private componentFactoryResolver: ComponentFactoryResolver) { } @@ -71,12 +81,25 @@ export class TableComponent implements OnInit, OnChanges { }); this.reloadData(); if (this.detailsComponent) { - this.selectable = 'multi'; + this.selectionType = 'multi'; + } + } + + ngAfterContentChecked() { + // If the data table is not visible, e.g. another tab is active, and the + // browser window gets resized, the table and its columns won't get resized + // automatically if the tab gets visible again. + // https://github.com/swimlane/ngx-datatable/issues/193 + // https://github.com/swimlane/ngx-datatable/issues/193#issuecomment-329144543 + if (this.table && this.table.element.clientWidth !== this.currentWidth) { + this.currentWidth = this.table.element.clientWidth; + // Force the redrawing of the table. + window.dispatchEvent(new Event('resize')); } } _addTemplates () { - this.cellTemplates.bold = this.bold; + this.cellTemplates.bold = this.tableCellBoldTpl; } ngOnChanges(changes) { @@ -101,6 +124,8 @@ export class TableComponent implements OnInit, OnChanges { toggleExpandRow() { if (this.selected.length > 0) { this.table.rowDetail.toggleExpandRow(this.selected[0]); + } else { + this.detailTemplate.viewContainerRef.clear(); } } @@ -108,6 +133,11 @@ export class TableComponent implements OnInit, OnChanges { if (!this.detailsComponent) { return; } + if (_.isFunction(this.beforeShowDetails)) { + if (!this.beforeShowDetails(this.selected)) { + return; + } + } const factories = Array.from(this.componentFactoryResolver['_factories'].keys()); const factoryClass = >factories.find((x: any) => x.name === this.detailsComponent); this.detailTemplate.viewContainerRef.clear(); @@ -133,4 +163,13 @@ export class TableComponent implements OnInit, OnChanges { // Whenever the filter changes, always go back to the first page this.table.offset = 0; } + + getRowClass() { + // Return the function used to populate a row's CSS classes. + return () => { + return { + 'clickable': !_.isUndefined(this.detailsComponent) + }; + }; + } } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts index e4cbc3b344d..2a6a863ffd3 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts @@ -21,6 +21,7 @@ import { ServicesModule } from './services/services.module'; ], exports: [ PipesModule, + ComponentsModule, ServicesModule, PasswordButtonDirective ], diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss b/src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss index 0d8665ea843..adc791ee76c 100755 --- a/src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss +++ b/src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss @@ -102,12 +102,13 @@ option { .strikethrough { text-decoration: line-through; } - .italic { font-style: italic; } - -text-right { +.bold { + font-weight: bold; +} +.text-right { text-align: right; } diff --git a/src/pybind/mgr/dashboard_v2/module.py b/src/pybind/mgr/dashboard_v2/module.py index 6568c204cd9..8e4e79aeb48 100644 --- a/src/pybind/mgr/dashboard_v2/module.py +++ b/src/pybind/mgr/dashboard_v2/module.py @@ -121,7 +121,6 @@ class Module(MgrModule): cherrypy.engine.start() NotificationQueue.start_queue() logger.info('Waiting for engine...') - self.log.info('Waiting for engine...') cherrypy.engine.block() if 'COVERAGE_ENABLED' in os.environ: _cov.stop() diff --git a/src/pybind/mgr/dashboard_v2/tests/test_perf_counters.py b/src/pybind/mgr/dashboard_v2/tests/test_perf_counters.py new file mode 100644 index 00000000000..94458e00d6a --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/tests/test_perf_counters.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +from .helper import ControllerTestCase, authenticate + + +class PerfCountersControllerTest(ControllerTestCase): + + @authenticate + def test_perf_counters_list(self): + data = self._get('/api/perf_counters') + self.assertStatus(200) + + self.assertIsInstance(data, dict) + self.assertIn('mon.a', data) + self.assertIn('mon.b', data) + self.assertIn('mon.c', data) + self.assertIn('osd.0', data) + self.assertIn('osd.1', data) + self.assertIn('osd.2', data) + + @authenticate + def test_perf_counters_mon_a_get(self): + data = self._get('/api/perf_counters/mon/a') + self.assertStatus(200) + + self.assertIsInstance(data, dict) + self.assertEqual('mon', data['service']['type']) + self.assertEqual('a', data['service']['id']) + self.assertIsInstance(data['counters'], list) + self.assertGreater(len(data['counters']), 0) + counter = data['counters'][0] + self.assertIsInstance(counter, dict) + self.assertIn('description', counter) + self.assertIn('name', counter) + self.assertIn('unit', counter) + self.assertIn('value', counter) -- 2.39.5