--- /dev/null
+# -*- 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
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';
},
{ path: 'login', component: LoginComponent },
{ path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
+ {
+ path: 'rgw',
+ component: RgwDaemonListComponent,
+ canActivate: [AuthGuardService]
+ },
{ path: 'block/pool/:name', component: PoolDetailComponent, canActivate: [AuthGuardService] }
];
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: []
[columns]="columns"
columnMode="flex"
[toolHeader]="false"
- [footer]="false">
+ [footer]="false"
+ [limit]="0">
</cd-table>
--- /dev/null
+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 { }
--- /dev/null
+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();
+ })
+ );
+});
--- /dev/null
+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<object> => {
+ return resp['counters'];
+ });
+ }
+}
--- /dev/null
+<cd-table [data]="counters"
+ [columns]="columns"
+ columnMode="flex"
+ [toolHeader]="false"
+ [footer]="false"
+ [limit]="0">
+ <ng-template #valueTpl let-row="row">
+ {{ row.value | dimless }} {{ row.unit }}
+ </ng-template>
+</cd-table>
--- /dev/null
+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<TablePerformanceCounterComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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<CdTableColumn> = [];
+ private counters: Array<object> = [];
+
+ @ViewChild('valueTpl') public valueTpl: TemplateRef<any>;
+
+ /**
+ * 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;
+ });
+ }
+}
--- /dev/null
+<tabset>
+ <tab heading="Details">
+ <cd-table-key-value [data]="metadata">
+ </cd-table-key-value>
+ </tab>
+ <tab heading="Performance Counters">
+ <cd-table-performance-counter serviceType="rgw"
+ [serviceId]="serviceId">
+ </cd-table-performance-counter>
+ </tab>
+</tabset>
--- /dev/null
+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<RgwDaemonDetailsComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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<object> = [];
+ private serviceId = '';
+
+ @Input() selected?: Array<any> = [];
+
+ 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;
+ });
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item">Object Gateway</li>
+ </ol>
+</nav>
+<cd-table [data]="daemons"
+ [columns]="columns"
+ [detailsComponent]="detailsComponent"
+ (fetchData)="getDaemonList()"
+ [beforeShowDetails]="beforeShowDetails">
+</cd-table>
--- /dev/null
+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<RgwDaemonListComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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<CdTableColumn> = [];
+ private daemons: Array<object> = [];
+
+ 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<object>) {
+ return selected.length === 1;
+ }
+}
--- /dev/null
+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 { }
--- /dev/null
+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();
+ })
+ );
+});
--- /dev/null
+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;
+ });
+ }
+}
</li>
</ul>
</li>
+ <li routerLinkActive="active"
+ class="tc_menuitem tc_menuitem_rgw">
+ <a i18n
+ routerLink="/rgw">Object Gateway
+ </a>
+ </li>
<!--<li class="dropdown tc_menuitem tc_menuitem_ceph_rgw">
<a href=""
class="dropdown-toggle"
- data-toggle="dropdown"><ng-container i18n>Object Gateway</ng-container> <span class="caret"></span>
+ data-toggle="dropdown">
+ <ng-container i18n>Object Gateway</ng-container>
+ <span class="caret"></span>
</a>
- <ul class="dropdown-menu">
+ <ul *dropdownMenu
+ class="dropdown-menu">
<li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_ceph_rgw_users">
+ class="tc_submenuitem tc_submenuitem_rgw_users">
<a i18n
- routerLink="/ceph-rgw-users">Users
+ class="dropdown-item"
+ routerLink="/rgw-users">Users
</a>
</li>
<li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_ceph_rgw_buckets">
+ class="tc_submenuitem tc_submenuitem_rgw_buckets">
<a i18n
- routerLink="/ceph-rgw-buckets">Buckets
+ class="dropdown-item"
+ routerLink="/rgw-buckets">Buckets
</a>
</li>
</ul>
-import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
-
-import { TableDetailsDirective } from './table/table-details.directive';
-import { TableComponent } from './table/table.component';
+import { DataTableModule } from './datatable/datatable.module';
@NgModule({
- entryComponents: [],
- imports: [CommonModule, NgxDatatableModule, FormsModule],
- declarations: [TableComponent, TableDetailsDirective],
- exports: [TableComponent, NgxDatatableModule]
+ imports: [
+ DataTableModule
+ ],
+ declarations: [],
+ providers: [],
+ exports: [
+ DataTableModule
+ ]
})
export class ComponentsModule {}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+
+import { PipesModule } from '../../pipes/pipes.module';
+import { TableDetailsDirective } from './table-details.directive';
+import { TableKeyValueComponent } from './table-key-value/table-key-value.component';
+import { TableComponent } from './table/table.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ NgxDatatableModule,
+ FormsModule,
+ PipesModule
+ ],
+ declarations: [
+ TableComponent,
+ TableDetailsDirective,
+ TableKeyValueComponent
+ ],
+ exports: [
+ TableComponent,
+ NgxDatatableModule,
+ TableKeyValueComponent
+ ]
+})
+export class DataTableModule { }
--- /dev/null
+import { TableDetailsDirective } from './table-details.directive';
+
+describe('TableDetailsDirective', () => {
+ it('should create an instance', () => {
+ const directive = new TableDetailsDirective(null);
+ expect(directive).toBeTruthy();
+ });
+});
--- /dev/null
+import { Directive, Input, ViewContainerRef } from '@angular/core';
+
+@Directive({
+ selector: '[cdTableDetails]'
+})
+export class TableDetailsDirective {
+ @Input() selected?: any[];
+
+ constructor(public viewContainerRef: ViewContainerRef) { }
+
+}
--- /dev/null
+<cd-table [data]="data"
+ [columns]="columns"
+ columnMode="flex"
+ [toolHeader]="false"
+ [header]="false"
+ [footer]="false"
+ [limit]="0">
+</cd-table>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+
+import { TableComponent } from '../table/table.component';
+import { TableKeyValueComponent } from './table-key-value.component';
+
+describe('TableKeyValueComponent', () => {
+ let component: TableKeyValueComponent;
+ let fixture: ComponentFixture<TableKeyValueComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ TableComponent, TableKeyValueComponent ],
+ imports: [ FormsModule, NgxDatatableModule ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TableKeyValueComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core';
+
+import { CellTemplate } from '../../../enum/cell-template.enum';
+import { CdTableColumn } from '../../../models/cd-table-column';
+
+/**
+ * Display the given data in a 2 column data table. The left column
+ * shows the 'key' attribute, the right column the 'value' attribute.
+ * The data table has the following characteristics:
+ * - No header and footer is displayed
+ * - The relation of the width for the columns 'key' and 'value' is 1:3
+ * - The 'key' column is displayed in bold text
+ */
+@Component({
+ selector: 'cd-table-key-value',
+ templateUrl: './table-key-value.component.html',
+ styleUrls: ['./table-key-value.component.scss']
+})
+export class TableKeyValueComponent implements OnInit {
+
+ private columns: Array<CdTableColumn> = [];
+
+ /**
+ * An array of objects to be displayed in the data table.
+ */
+ @Input() data: Array<object> = [];
+
+ /**
+ * The name of the attribute to be displayed as key.
+ * Defaults to 'key'.
+ * @type {string}
+ */
+ @Input() key = 'key';
+
+ /**
+ * The name of the attribute to be displayed as value.
+ * Defaults to 'value'.
+ * @type {string}
+ */
+ @Input() value = 'value';
+
+ constructor() { }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ prop: this.key,
+ flexGrow: 1,
+ cellTransformation: CellTemplate.bold
+ },
+ {
+ prop: this.value,
+ flexGrow: 3
+ }
+ ];
+ }
+}
--- /dev/null
+<div class="dataTables_wrapper">
+ <div class="dataTables_header clearfix"
+ *ngIf="toolHeader">
+ <!-- actions -->
+ <div class="oadatatableactions">
+ <ng-content select="table-actions"></ng-content>
+ </div>
+ <!-- end actions -->
+
+ <!-- search -->
+ <div class="input-group">
+ <span class="input-group-addon">
+ <i class="glyphicon glyphicon-search"></i>
+ </span>
+ <input class="form-control"
+ type="text"
+ [(ngModel)]="search"
+ (keyup)='updateFilter($event)'>
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default clear-input tc_clearInputBtn"
+ (click)="updateFilter()">
+ <i class="icon-prepend fa fa-remove"></i>
+ </button>
+ </span>
+ </div>
+ <!-- end search -->
+
+ <!-- pagination limit -->
+ <div class="input-group dataTables_paginate">
+ <input class="form-control"
+ type="number"
+ min="1"
+ max="9999"
+ [value]="limit"
+ (click)="setLimit($event)"
+ (keyup)="setLimit($event)"
+ (blur)="setLimit($event)">
+ </div>
+ <!-- end pagination limit-->
+
+ <!-- refresh button -->
+ <div class="widget-toolbar tc_refreshBtn">
+ <a (click)="reloadData()">
+ <i class="fa fa-lg fa-refresh"></i>
+ </a>
+ </div>
+ <!-- end refresh button -->
+ </div>
+ <ngx-datatable #table
+ class="bootstrap oadatatable"
+ [cssClasses]="paginationClasses"
+ [selectionType]="selectionType"
+ [selected]="selected"
+ (select)="toggleExpandRow()"
+ [columns]="columns"
+ [columnMode]="columnMode"
+ [rows]="rows"
+ [rowClass]="getRowClass()"
+ [headerHeight]="header ? 'auto' : 0"
+ [footerHeight]="footer ? 'auto' : 0"
+ [limit]="limit > 0 ? limit : undefined"
+ [loadingIndicator]="true"
+ [rowHeight]="'auto'">
+ <!-- Row Detail Template -->
+ <ngx-datatable-row-detail (toggle)="updateDetailView()">
+ </ngx-datatable-row-detail>
+ </ngx-datatable>
+</div>
+<ng-template cdTableDetails></ng-template>
+<!-- cell templates that can be accessed from outside -->
+<ng-template #tableCellBoldTpl
+ let-row="row"
+ let-value="value">
+ <strong>{{ value }}</strong>
+</ng-template>
--- /dev/null
+@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;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+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<TableComponent>;
+ 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);
+ });
+});
--- /dev/null
+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<any>;
+
+ // 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<any>
+ } = {};
+ 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 = <Type<any>>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)
+ };
+ };
+ }
+}
+++ /dev/null
-import { TableDetailsDirective } from './table-details.directive';
-
-describe('TableDetailsDirective', () => {
- it('should create an instance', () => {
- const directive = new TableDetailsDirective(null);
- expect(directive).toBeTruthy();
- });
-});
+++ /dev/null
-import { Directive, Input, ViewContainerRef } from '@angular/core';
-
-@Directive({
- selector: '[cdTableDetails]'
-})
-export class TableDetailsDirective {
- @Input() selected?: any[];
-
- constructor(public viewContainerRef: ViewContainerRef) { }
-
-}
+++ /dev/null
-<div class="dataTables_wrapper">
- <div class="dataTables_header clearfix"
- *ngIf="toolHeader">
- <!-- actions -->
- <div class="oadatatableactions">
- <ng-content select="table-actions"></ng-content>
- </div>
- <!-- end actions -->
-
- <!-- search -->
- <div class="input-group">
- <span class="input-group-addon">
- <i class="glyphicon glyphicon-search"></i>
- </span>
- <input class="form-control"
- type="text"
- [(ngModel)]="search"
- (keyup)='updateFilter($event)'>
- <span class="input-group-btn">
- <button type="button"
- class="btn btn-default clear-input tc_clearInputBtn"
- (click)="updateFilter()">
- <i class="icon-prepend fa fa-remove"></i>
- </button>
- </span>
- </div>
- <!-- end search -->
-
- <!-- pagination limit -->
- <div class="input-group dataTables_paginate">
- <input class="form-control"
- type="number"
- min="1"
- max="9999"
- [value]="limit"
- (click)="setLimit($event)"
- (keyup)="setLimit($event)"
- (blur)="setLimit($event)">
- </div>
- <!-- end pagination limit-->
-
- <!-- refresh button -->
- <div class="widget-toolbar tc_refreshBtn">
- <a (click)="reloadData()">
- <i class="fa fa-lg fa-refresh"></i>
- </a>
- </div>
- <!-- end refresh button -->
- </div>
- <ngx-datatable #table
- class="bootstrap oadatatable"
- [cssClasses]="paginationClasses"
- [selectionType]="selectable"
- [selected]="selected"
- (select)="toggleExpandRow()"
- [columns]="columns"
- [columnMode]="columnMode"
- [rows]="rows"
- [headerHeight]="header ? 'auto' : 0"
- [footerHeight]="footer ? 'auto' : 0"
- [limit]="limit"
- [loadingIndicator]="true"
- [rowHeight]="'auto'">
- <!-- Row Detail Template -->
- <ngx-datatable-row-detail (toggle)="updateDetailView()">
- </ngx-datatable-row-detail>
- </ngx-datatable>
-</div>
-<ng-template cdTableDetails></ng-template>
-<!-- cell templates that can be accessed from outside -->
-<ng-template #bold
- let-row="row"
- let-value="value">
- <strong>{{ value }}</strong>
-</ng-template>
+++ /dev/null
-@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;
- }
- }
- }
- }
-}
+++ /dev/null
-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<TableComponent>;
- 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);
- });
-});
+++ /dev/null
-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<any>;
- cellTemplates: {
- [key: string]: TemplateRef<any>
- } = {};
-
- 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 = <Type<any>>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;
- }
-}
],
exports: [
PipesModule,
+ ComponentsModule,
ServicesModule,
PasswordButtonDirective
],
.strikethrough {
text-decoration: line-through;
}
-
.italic {
font-style: italic;
}
-
-text-right {
+.bold {
+ font-weight: bold;
+}
+.text-right {
text-align: right;
}
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()
--- /dev/null
+# -*- 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)