"@angular/platform-browser": "^5.0.0",
"@angular/platform-browser-dynamic": "^5.0.0",
"@angular/router": "^5.0.0",
+ "@swimlane/ngx-datatable": "^11.1.7",
"@types/lodash": "^4.14.95",
"awesome-bootstrap-checkbox": "0.3.7",
- "@swimlane/ngx-datatable": "^11.1.7",
"bootstrap": "^3.3.7",
"chart.js": "^2.7.1",
"core-js": "^2.4.1",
"font-awesome": "4.7.0",
"lodash": "^4.17.4",
+ "moment": "2.20.1",
"ng2-charts": "^1.6.0",
"ng2-toastr": "4.1.2",
"ngx-bootstrap": "^2.0.1",
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
+import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
import { PoolDetailComponent } from './ceph/block/pool-detail/pool-detail.component';
import { CephfsComponent } from './ceph/cephfs/cephfs/cephfs.component';
import { ClientsComponent } from './ceph/cephfs/clients/clients.component';
component: RgwDaemonListComponent,
canActivate: [AuthGuardService]
},
+ { path: 'block/iscsi', component: IscsiComponent, canActivate: [AuthGuardService] },
{ path: 'block/pool/:name', component: PoolDetailComponent, canActivate: [AuthGuardService] },
{
path: 'perf_counters/:type/:id',
import { ComponentsModule } from '../../shared/components/components.module';
import { PipesModule } from '../../shared/pipes/pipes.module';
+import { ServicesModule } from '../../shared/services/services.module';
import { SharedModule } from '../../shared/shared.module';
+import { IscsiComponent } from './iscsi/iscsi.component';
import { PoolDetailComponent } from './pool-detail/pool-detail.component';
@NgModule({
TabsModule.forRoot(),
SharedModule,
ComponentsModule,
- PipesModule
+ PipesModule,
+ ServicesModule
],
- declarations: [PoolDetailComponent]
+ declarations: [
+ PoolDetailComponent,
+ IscsiComponent
+ ]
})
export class BlockModule { }
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item">Block</li>
+ <li class="breadcrumb-item active" aria-current="page">iSCSI</li>
+ </ol>
+</nav>
+
+<legend>Daemons</legend>
+<cd-table [data]="daemons"
+ [columns]="daemonsColumns">
+</cd-table>
+
+<legend>Images</legend>
+<cd-table [data]="images"
+ [columns]="imagesColumns">
+</cd-table>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppModule } from '../../../app.module';
+import { IscsiComponent } from './iscsi.component';
+
+describe('IscsiComponent', () => {
+ let component: IscsiComponent;
+ let fixture: ComponentFixture<IscsiComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [AppModule]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
+import { ListPipe } from '../../../shared/pipes/list.pipe';
+import { RelativeDatePipe } from '../../../shared/pipes/relative-date.pipe';
+import { TcmuIscsiService } from '../../../shared/services/tcmu-iscsi.service';
+
+@Component({
+ selector: 'cd-iscsi',
+ templateUrl: './iscsi.component.html',
+ styleUrls: ['./iscsi.component.scss']
+})
+export class IscsiComponent implements OnInit, OnDestroy {
+
+ daemons = [];
+ daemonsColumns: any;
+ images = [];
+ imagesColumns: any;
+ interval: any;
+
+ constructor(private tcmuIscsiService: TcmuIscsiService,
+ cephShortVersionPipe: CephShortVersionPipe,
+ dimlessBinaryPipe: DimlessBinaryPipe,
+ dimlessPipe: DimlessPipe,
+ relativeDatePipe: RelativeDatePipe,
+ listPipe: ListPipe) {
+ this.daemonsColumns = [
+ {
+ name: 'Hostname',
+ prop: 'server_hostname'
+ },
+ {
+ name: '# Active/Optimized',
+ prop: 'optimized_paths',
+ },
+ {
+ name: '# Active/Non-Optimized',
+ prop: 'non_optimized_paths'
+ },
+ {
+ name: 'Version',
+ prop: 'version',
+ pipe: cephShortVersionPipe
+ }
+ ];
+ this.imagesColumns = [
+ {
+ name: 'Pool',
+ prop: 'pool_name'
+ },
+ {
+ name: 'Image',
+ prop: 'name'
+ },
+ {
+ name: 'Active/Optimized',
+ prop: 'optimized_paths',
+ pipe: listPipe
+ },
+ {
+ name: 'Active/Non-Optimized',
+ prop: 'non_optimized_paths',
+ pipe: listPipe
+ },
+ {
+ name: 'Read Bytes',
+ prop: 'stats.rd_bytes',
+ pipe: dimlessBinaryPipe
+ },
+ {
+ name: 'Write Bytes',
+ prop: 'stats.wr_bytes',
+ pipe: dimlessBinaryPipe
+ },
+ {
+ name: 'Read Ops',
+ prop: 'stats.rd',
+ pipe: dimlessPipe
+ },
+ {
+ name: 'Write Ops',
+ prop: 'stats.wr',
+ pipe: dimlessPipe
+ },
+ {
+ name: 'A/O Since',
+ prop: 'optimized_since',
+ pipe: relativeDatePipe
+ },
+ ];
+
+ }
+
+ ngOnInit() {
+ this.refresh();
+
+ this.interval = setInterval(() => {
+ this.refresh();
+ }, 5000);
+ }
+
+ ngOnDestroy() {
+ clearInterval(this.interval);
+ }
+
+ refresh() {
+ this.tcmuIscsiService.tcmuiscsi().then((resp) => {
+ this.daemons = resp.daemons;
+ this.images = resp.images;
+ });
+ }
+
+}
</a>
<ul class="dropdown-menu">
+ <li routerLinkActive="active">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/block/iscsi">iSCSI</a>
+ </li>
<li class="dropdown-submenu">
<a class="dropdown-toggle" data-toggle="dropdown">Pools</a>
<ul *dropdownMenu
--- /dev/null
+import { ListPipe } from './list.pipe';
+
+describe('ListPipe', () => {
+ it('create an instance', () => {
+ const pipe = new ListPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'list'
+})
+export class ListPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ return value.join(', ');
+ }
+}
import { DimlessBinaryPipe } from './dimless-binary.pipe';
import { DimlessPipe } from './dimless.pipe';
import { HealthColorPipe } from './health-color.pipe';
+import { ListPipe } from './list.pipe';
+import { RelativeDatePipe } from './relative-date.pipe';
@NgModule({
imports: [CommonModule],
DimlessBinaryPipe,
HealthColorPipe,
DimlessPipe,
- CephShortVersionPipe
+ CephShortVersionPipe,
+ RelativeDatePipe,
+ ListPipe
],
exports: [
DimlessBinaryPipe,
HealthColorPipe,
DimlessPipe,
- CephShortVersionPipe
+ CephShortVersionPipe,
+ RelativeDatePipe,
+ ListPipe
],
providers: [
CephShortVersionPipe,
DimlessBinaryPipe,
- DimlessPipe
+ DimlessPipe,
+ RelativeDatePipe,
+ ListPipe
]
})
export class PipesModule {}
--- /dev/null
+import { RelativeDatePipe } from './relative-date.pipe';
+
+describe('RelativeDatePipe', () => {
+ it('create an instance', () => {
+ const pipe = new RelativeDatePipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+import * as moment from 'moment';
+
+@Pipe({
+ name: 'relativeDate'
+})
+export class RelativeDatePipe implements PipeTransform {
+ constructor() {}
+
+ transform(value: any, args?: any): any {
+ if (!value) {
+ return 'unknown';
+ }
+ return moment(value * 1000).fromNow();
+ }
+}
truncatedFloat = this.truncate(n, width);
}
- return truncatedFloat + units[unit];
+ return truncatedFloat === '' ? '-' : (truncatedFloat + units[unit]);
}
}
import { NgModule } from '@angular/core';
import { FormatterService } from './formatter.service';
+import { TcmuIscsiService } from './tcmu-iscsi.service';
import { TopLevelService } from './top-level.service';
@NgModule({
CommonModule
],
declarations: [],
- providers: [FormatterService, TopLevelService]
+ providers: [FormatterService, TopLevelService, TcmuIscsiService]
})
export class ServicesModule { }
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class TcmuIscsiService {
+
+ constructor(private http: HttpClient) {
+ }
+
+ tcmuiscsi() {
+ return this.http.get('/api/tcmuiscsi').toPromise().then((resp: any) => {
+ return resp;
+ });
+ }
+}