From: Volker Theile Date: Mon, 5 Feb 2018 13:56:46 +0000 (+0100) Subject: mgr/dashboard_v2: Add RGW page. X-Git-Tag: v13.0.2~84^2~73 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=5f4823fc84b63678cc2ce901fe63071160d410ba;p=ceph.git mgr/dashboard_v2: Add RGW page. Signed-off-by: Volker Theile --- 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 + +
  • +
    + +
    + + + +
    + + + + + + + +
    + + + +
    + +
    + + + +
    + + + +
    + + + + + + + + + + + + {{ value }} + diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.scss new file mode 100644 index 00000000000..c6289884fb1 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.scss @@ -0,0 +1,176 @@ +@import '../../../../../defaults'; + +.dataTables_wrapper { + margin-bottom: 25px; + .separator { + height: 30px; + border-left: 1px solid rgba(0,0,0,.09); + padding-left: 5px; + margin-left: 5px; + display: inline-block; + vertical-align: middle; + } + .widget-toolbar { + display: inline-block; + float: right; + width: auto; + height: 30px; + line-height: 28px; + position: relative; + border-left: 1px solid rgba(0,0,0,.09); + cursor: pointer; + padding: 0 8px; + text-align: center; + } + .dropdown-menu { + white-space: nowrap; + & > li { + cursor: pointer; + & > label { + width: 100%; + margin-bottom: 0; + padding-left: 20px; + padding-right: 20px; + cursor: pointer; + &:hover { + background-color: #f5f5f5; + } + & > input { + cursor: pointer; + } + } + } + } + th.oadatatablecheckbox { + width: 16px; + } + .dataTables_length>input { + line-height: 25px; + text-align: right; + } +} +.dataTables_header { + background-color: #f6f6f6; + border: 1px solid #d1d1d1; + border-bottom: none; + padding: 5px; + position: relative; + .oadatatableactions { + display: inline-block; + } + .input-group { + float: right; + border-left: 1px solid rgba(0,0,0,.09); + padding-left: 8px; + width: 40%; + max-width: 350px; + .form-control { + height: 30px; + } + .clear-input { + height: 30px; + i { + vertical-align: text-top; + } + } + } + .input-group.dataTables_paginate { + width: 8%; + } +} + +::ng-deep .oadatatable { + border: $border-color; + margin-bottom: 0; + max-width: none!important; + .datatable-header { + background-clip: padding-box; + background-color: #f9f9f9; + background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%); + background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%); + background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0); + .sort-asc, .sort-desc { + color: $oa-color-blue; + } + .datatable-header-cell{ + @include table-cell; + text-align: left; + font-weight: bold; + .datatable-header-cell-label { + &:after { + font-family: FontAwesome; + font-weight: 400; + height: 9px; + left: 10px; + line-height: 12px; + position: relative; + vertical-align: baseline; + width: 12px; + } + } + &.sortable { + .datatable-header-cell-label:after { + content: " \f0dc"; + } + &.sort-active { + &.sort-asc .datatable-header-cell-label:after { + content: " \f160"; + } + &.sort-desc .datatable-header-cell-label:after { + content: " \f161"; + } + } + } + &:first-child { + border-left: none; + } + } + } + .datatable-body { + .datatable-body-row { + &.datatable-row-even { + background-color: #ffffff; + } + &.datatable-row-odd { + background-color: #f6f6f6; + } + &.active, &.active:hover { + background-color: $bg-color-light-blue; + } + .datatable-body-cell{ + @include table-cell; + &:first-child { + border-left: none; + } + .datatable-body-cell-label { + display: block; + } + } + } + } + .datatable-footer { + .selected-count, .page-count { + font-style: italic; + padding-left: 5px; + } + .datatable-pager .pager { + margin-right: 5px; + .pages { + & > a, & > span { + display: inline-block; + padding: 5px 10px; + margin-bottom: 5px; + border: none; + } + a:hover { + background-color: $oa-color-light-blue; + } + &.active > a { + background-color: $bg-color-light-blue; + } + } + } + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.spec.ts new file mode 100644 index 00000000000..07a1825d86a --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.spec.ts @@ -0,0 +1,88 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; + +import { NgxDatatableModule, TableColumn } from '@swimlane/ngx-datatable'; + +import { TableComponent } from './table.component'; + +describe('TableComponent', () => { + let component: TableComponent; + let fixture: ComponentFixture; + const columns: TableColumn[] = []; + const createFakeData = (n) => { + const data = []; + for (let i = 0; i < n; i++) { + data.push({ + a: i, + b: i * i, + c: -(i % 10) + }); + } + return data; + }; + + beforeEach( + async(() => { + TestBed.configureTestingModule({ + declarations: [TableComponent], + imports: [NgxDatatableModule, FormsModule] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(TableComponent); + component = fixture.componentInstance; + }); + + beforeEach(() => { + component.data = createFakeData(100); + component.useData(); + component.columns = [ + {prop: 'a'}, + {prop: 'b'}, + {prop: 'c'} + ]; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have rows', () => { + expect(component.data.length).toBe(100); + expect(component.rows.length).toBe(component.data.length); + }); + + it('should have an int in setLimit parsing a string', () => { + expect(component.limit).toBe(10); + expect(component.limit).toEqual(jasmine.any(Number)); + + const e = {target: {value: '1'}}; + component.setLimit(e); + expect(component.limit).toBe(1); + expect(component.limit).toEqual(jasmine.any(Number)); + e.target.value = '-20'; + component.setLimit(e); + expect(component.limit).toBe(1); + }); + + it('should search for 13', () => { + component.search = '13'; + expect(component.rows.length).toBe(100); + component.updateFilter(true); + expect(component.rows[0].a).toBe(13); + expect(component.rows[1].b).toBe(1369); + expect(component.rows[2].b).toBe(3136); + expect(component.rows.length).toBe(3); + }); + + it('should restore full table after search', () => { + component.search = '13'; + expect(component.rows.length).toBe(100); + component.updateFilter(true); + expect(component.rows.length).toBe(3); + component.updateFilter(); + expect(component.rows.length).toBe(100); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.ts new file mode 100644 index 00000000000..d2c522e9e75 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/datatable/table/table.component.ts @@ -0,0 +1,175 @@ +import { + AfterContentChecked, + Component, + ComponentFactoryResolver, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + TemplateRef, + Type, + ViewChild +} 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'; + +@Component({ + selector: 'cd-table', + templateUrl: './table.component.html', + styleUrls: ['./table.component.scss'] +}) +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. + @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 e.g. 'TableDetailsComponent' + @Input() detailsComponent?: string; + // Display the tool header, including reload button, pagination and search fields? + @Input() toolHeader ?= true; + // Display the table header? + @Input() header ?= true; + // Display the table footer? + @Input() footer ?= true; + // 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(); + + cellTemplates: { + [key: string]: TemplateRef + } = {}; + selectionType: string = undefined; + search = ''; + rows = []; + selected = []; + paginationClasses = { + pagerLeftArrow: 'i fa fa-angle-double-left', + pagerRightArrow: 'i fa fa-angle-double-right', + pagerPrevious: 'i fa fa-angle-left', + pagerNext: 'i fa fa-angle-right' + }; + + // 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) { } + + ngOnInit() { + this._addTemplates(); + this.columns.map((column) => { + if (column.cellTransformation) { + column.cellTemplate = this.cellTemplates[column.cellTransformation]; + } + return column; + }); + this.reloadData(); + if (this.detailsComponent) { + 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.tableCellBoldTpl; + } + + ngOnChanges(changes) { + this.useData(); + } + + setLimit(e) { + const value = parseInt(e.target.value, 10); + if (value > 0) { + this.limit = value; + } + } + + reloadData() { + this.fetchData.emit(); + } + + useData() { + this.rows = [...this.data]; + } + + toggleExpandRow() { + if (this.selected.length > 0) { + this.table.rowDetail.toggleExpandRow(this.selected[0]); + } else { + this.detailTemplate.viewContainerRef.clear(); + } + } + + updateDetailView() { + 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(); + const cmpRef = this.detailTemplate.viewContainerRef.createComponent( + this.componentFactoryResolver.resolveComponentFactory(factoryClass) + ); + cmpRef.instance.selected = this.selected; + } + + updateFilter(event?) { + if (!event) { + this.search = ''; + } + const val = this.search.toLowerCase(); + const columns = this.columns; + // update the rows + this.rows = this.data.filter(function (d) { + return columns.filter((c) => { + return (typeof d[c.prop] === 'string' || typeof d[c.prop] === 'number') + && (d[c.prop] + '').toLowerCase().indexOf(val) !== -1; + }).length > 0; + }); + // 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/components/table/table-details.directive.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table-details.directive.spec.ts deleted file mode 100644 index b3e26843cba..00000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table-details.directive.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TableDetailsDirective } from './table-details.directive'; - -describe('TableDetailsDirective', () => { - it('should create an instance', () => { - const directive = new TableDetailsDirective(null); - expect(directive).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table-details.directive.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table-details.directive.ts deleted file mode 100644 index 5c529dc72b7..00000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table-details.directive.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Directive, Input, ViewContainerRef } from '@angular/core'; - -@Directive({ - selector: '[cdTableDetails]' -}) -export class TableDetailsDirective { - @Input() selected?: any[]; - - constructor(public viewContainerRef: ViewContainerRef) { } - -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.html deleted file mode 100644 index 30e1e42a51f..00000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.html +++ /dev/null @@ -1,75 +0,0 @@ -
    -
    - -
    - -
    - - - -
    - - - - - - - -
    - - - -
    - -
    - - - -
    - - - -
    - -
    - - - - - -
    - - - - {{ 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/table/table.component.scss deleted file mode 100644 index 95861c8dad2..00000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.scss +++ /dev/null @@ -1,176 +0,0 @@ -@import '../../../../defaults'; - -.dataTables_wrapper { - margin-bottom: 25px; - .separator { - height: 30px; - border-left: 1px solid rgba(0,0,0,.09); - padding-left: 5px; - margin-left: 5px; - display: inline-block; - vertical-align: middle; - } - .widget-toolbar { - display: inline-block; - float: right; - width: auto; - height: 30px; - line-height: 28px; - position: relative; - border-left: 1px solid rgba(0,0,0,.09); - cursor: pointer; - padding: 0 8px; - text-align: center; - } - .dropdown-menu { - white-space: nowrap; - & > li { - cursor: pointer; - & > label { - width: 100%; - margin-bottom: 0; - padding-left: 20px; - padding-right: 20px; - cursor: pointer; - &:hover { - background-color: #f5f5f5; - } - & > input { - cursor: pointer; - } - } - } - } - th.oadatatablecheckbox { - width: 16px; - } - .dataTables_length>input { - line-height: 25px; - text-align: right; - } -} -.dataTables_header { - background-color: #f6f6f6; - border: 1px solid #d1d1d1; - border-bottom: none; - padding: 5px; - position: relative; - .oadatatableactions { - display: inline-block; - } - .input-group { - float: right; - border-left: 1px solid rgba(0,0,0,.09); - padding-left: 8px; - width: 40%; - max-width: 350px; - .form-control { - height: 30px; - } - .clear-input { - height: 30px; - i { - vertical-align: text-top; - } - } - } - .input-group.dataTables_paginate { - width: 8%; - } -} - -::ng-deep .oadatatable { - border: $border-color; - margin-bottom: 0; - max-width: none!important; - .datatable-header { - background-clip: padding-box; - background-color: #f9f9f9; - background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%); - background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%); - background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0); - .sort-asc, .sort-desc { - color: $oa-color-blue; - } - .datatable-header-cell{ - @include table-cell; - text-align: left; - font-weight: bold; - .datatable-header-cell-label { - &:after { - font-family: FontAwesome; - font-weight: 400; - height: 9px; - left: 10px; - line-height: 12px; - position: relative; - vertical-align: baseline; - width: 12px; - } - } - &.sortable { - .datatable-header-cell-label:after { - content: " \f0dc"; - } - &.sort-active { - &.sort-asc .datatable-header-cell-label:after { - content: " \f160"; - } - &.sort-desc .datatable-header-cell-label:after { - content: " \f161"; - } - } - } - &:first-child { - border-left: none; - } - } - } - .datatable-body { - .datatable-body-row { - &.datatable-row-even { - background-color: #ffffff; - } - &.datatable-row-odd { - background-color: #f6f6f6; - } - &.active, &.active:hover { - background-color: $bg-color-light-blue; - } - .datatable-body-cell{ - @include table-cell; - &:first-child { - border-left: none; - } - .datatable-body-cell-label { - display: block; - } - } - } - } - .datatable-footer { - .selected-count, .page-count { - font-style: italic; - padding-left: 5px; - } - .datatable-pager .pager { - margin-right: 5px; - .pages { - & > a, & > span { - display: inline-block; - padding: 5px 10px; - margin-bottom: 5px; - border: none; - } - a:hover { - background-color: $oa-color-light-blue; - } - &.active > a { - background-color: $bg-color-light-blue; - } - } - } - } -} 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/table/table.component.spec.ts deleted file mode 100644 index 07a1825d86a..00000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; - -import { NgxDatatableModule, TableColumn } from '@swimlane/ngx-datatable'; - -import { TableComponent } from './table.component'; - -describe('TableComponent', () => { - let component: TableComponent; - let fixture: ComponentFixture; - const columns: TableColumn[] = []; - const createFakeData = (n) => { - const data = []; - for (let i = 0; i < n; i++) { - data.push({ - a: i, - b: i * i, - c: -(i % 10) - }); - } - return data; - }; - - beforeEach( - async(() => { - TestBed.configureTestingModule({ - declarations: [TableComponent], - imports: [NgxDatatableModule, FormsModule] - }).compileComponents(); - }) - ); - - beforeEach(() => { - fixture = TestBed.createComponent(TableComponent); - component = fixture.componentInstance; - }); - - beforeEach(() => { - component.data = createFakeData(100); - component.useData(); - component.columns = [ - {prop: 'a'}, - {prop: 'b'}, - {prop: 'c'} - ]; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should have rows', () => { - expect(component.data.length).toBe(100); - expect(component.rows.length).toBe(component.data.length); - }); - - it('should have an int in setLimit parsing a string', () => { - expect(component.limit).toBe(10); - expect(component.limit).toEqual(jasmine.any(Number)); - - const e = {target: {value: '1'}}; - component.setLimit(e); - expect(component.limit).toBe(1); - expect(component.limit).toEqual(jasmine.any(Number)); - e.target.value = '-20'; - component.setLimit(e); - expect(component.limit).toBe(1); - }); - - it('should search for 13', () => { - component.search = '13'; - expect(component.rows.length).toBe(100); - component.updateFilter(true); - expect(component.rows[0].a).toBe(13); - expect(component.rows[1].b).toBe(1369); - expect(component.rows[2].b).toBe(3136); - expect(component.rows.length).toBe(3); - }); - - it('should restore full table after search', () => { - component.search = '13'; - expect(component.rows.length).toBe(100); - component.updateFilter(true); - expect(component.rows.length).toBe(3); - component.updateFilter(); - expect(component.rows.length).toBe(100); - }); -}); 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/table/table.component.ts deleted file mode 100644 index c0df5b16cf6..00000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/table/table.component.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { - Component, - ComponentFactoryResolver, - EventEmitter, - Input, - OnChanges, - OnInit, - Output, - TemplateRef, - Type, - ViewChild -} from '@angular/core'; - -import { DatatableComponent } from '@swimlane/ngx-datatable'; - -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 { - @ViewChild(DatatableComponent) table: DatatableComponent; - @ViewChild(TableDetailsDirective) detailTemplate: TableDetailsDirective; - - // 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' - @Input() detailsComponent?: string; - // Display the tool header, including reload button, pagination and search fields? - @Input() toolHeader ?= true; - // Display the table header? - @Input() header ?= true; - // Display the table footer? - @Input() footer ?= true; - // 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; - search = ''; - rows = []; - selected = []; - paginationClasses = { - pagerLeftArrow: 'i fa fa-angle-double-left', - pagerRightArrow: 'i fa fa-angle-double-right', - pagerPrevious: 'i fa fa-angle-left', - pagerNext: 'i fa fa-angle-right' - }; - limit = 10; - - constructor(private componentFactoryResolver: ComponentFactoryResolver) { } - - ngOnInit() { - this._addTemplates(); - this.columns.map((column) => { - if (column.cellTransformation) { - column.cellTemplate = this.cellTemplates[column.cellTransformation]; - } - return column; - }); - this.reloadData(); - if (this.detailsComponent) { - this.selectable = 'multi'; - } - } - - _addTemplates () { - this.cellTemplates.bold = this.bold; - } - - ngOnChanges(changes) { - this.useData(); - } - - setLimit(e) { - const value = parseInt(e.target.value, 10); - if (value > 0) { - this.limit = value; - } - } - - reloadData() { - this.fetchData.emit(); - } - - useData() { - this.rows = [...this.data]; - } - - toggleExpandRow() { - if (this.selected.length > 0) { - this.table.rowDetail.toggleExpandRow(this.selected[0]); - } - } - - updateDetailView() { - if (!this.detailsComponent) { - return; - } - const factories = Array.from(this.componentFactoryResolver['_factories'].keys()); - const factoryClass = >factories.find((x: any) => x.name === this.detailsComponent); - this.detailTemplate.viewContainerRef.clear(); - const cmpRef = this.detailTemplate.viewContainerRef.createComponent( - this.componentFactoryResolver.resolveComponentFactory(factoryClass) - ); - cmpRef.instance.selected = this.selected; - } - - updateFilter(event?) { - if (!event) { - this.search = ''; - } - const val = this.search.toLowerCase(); - const columns = this.columns; - // update the rows - this.rows = this.data.filter(function (d) { - return columns.filter((c) => { - return (typeof d[c.prop] === 'string' || typeof d[c.prop] === 'number') - && (d[c.prop] + '').toLowerCase().indexOf(val) !== -1; - }).length > 0; - }); - // Whenever the filter changes, always go back to the first page - this.table.offset = 0; - } -} 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)