import { RouterModule, Routes } from '@angular/router';
import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
+import { MirroringComponent } from './ceph/block/mirroring/mirroring.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';
{ path: 'cephfs/:id/clients', component: ClientsComponent, canActivate: [AuthGuardService] },
{ path: 'cephfs/:id', component: CephfsComponent, canActivate: [AuthGuardService] },
{ path: 'configuration', component: ConfigurationComponent, canActivate: [AuthGuardService] },
+ { path: 'mirroring', component: MirroringComponent, canActivate: [AuthGuardService] },
{ path: '404', component: NotFoundComponent },
{ path: 'osd', component: OsdListComponent, canActivate: [AuthGuardService] },
{ path: '**', redirectTo: '/404'}
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
-import { TabsModule } from 'ngx-bootstrap';
+import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
+import { TabsModule } from 'ngx-bootstrap/tabs';
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 { MirrorHealthColorPipe } from './mirror-health-color.pipe';
+import { MirroringComponent } from './mirroring/mirroring.component';
import { PoolDetailComponent } from './pool-detail/pool-detail.component';
@NgModule({
CommonModule,
FormsModule,
TabsModule.forRoot(),
+ ProgressbarModule.forRoot(),
SharedModule,
ComponentsModule,
PipesModule,
],
declarations: [
PoolDetailComponent,
- IscsiComponent
+ IscsiComponent,
+ MirroringComponent,
+ MirrorHealthColorPipe
]
})
export class BlockModule { }
--- /dev/null
+import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
+
+describe('MirrorHealthColorPipe', () => {
+ it('create an instance', () => {
+ const pipe = new MirrorHealthColorPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'mirrorHealthColor'
+})
+export class MirrorHealthColorPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ if (value === 'warning') {
+ return 'label label-warning';
+ } else if (value === 'error') {
+ return 'label label-danger';
+ } else if (value === 'success') {
+ return 'label label-success';
+ }
+ return 'label label-info';
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item" i18n>Block</li>
+ <li class="breadcrumb-item active"
+ aria-current="page" i18n>Mirroring</li>
+ </ol>
+</nav>
+
+<cd-view-cache [status]="status"></cd-view-cache>
+
+<div class="row">
+ <div class="col-sm-6">
+ <fieldset>
+ <legend i18n>Daemons</legend>
+
+ <cd-table [data]="daemons.data"
+ columnMode="flex"
+ [columns]="daemons.columns"
+ (fetchData)="refresh()">
+ </cd-table>
+ </fieldset>
+ </div>
+
+ <div class="col-sm-6">
+ <fieldset>
+ <legend i18n>Pools</legend>
+
+ <cd-table [data]="pools.data"
+ columnMode="flex"
+ [columns]="pools.columns"
+ (fetchData)="refresh()">
+ </cd-table>
+ </fieldset>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-md-12">
+ <fieldset>
+ <legend i18n>Images</legend>
+ <tabset>
+ <tab heading="Issues" i18n-heading>
+ <cd-table [data]="image_error.data"
+ columnMode="flex"
+ [columns]="image_error.columns"
+ (fetchData)="refresh()">
+ </cd-table>
+ </tab>
+ <tab heading="Syncing" i18n-heading>
+ <cd-table [data]="image_syncing.data"
+ columnMode="flex"
+ [columns]="image_syncing.columns"
+ (fetchData)="refresh()">
+ </cd-table>
+ </tab>
+ <tab heading="Ready" i18n-heading>
+ <cd-table [data]="image_ready.data"
+ columnMode="flex"
+ [columns]="image_ready.columns"
+ (fetchData)="refresh()">
+ </cd-table>
+ </tab>
+ </tabset>
+ </fieldset>
+ </div>
+</div>
+
+<ng-template #healthTmpl
+ let-row="row"
+ let-value="value">
+ <span [ngClass]="row.health_color | mirrorHealthColor">{{ value }}</span>
+</ng-template>
+
+<ng-template #stateTmpl
+ let-row="row"
+ let-value="value">
+ <span [ngClass]="row.state_color | mirrorHealthColor">{{ value }}</span>
+</ng-template>
+
+<ng-template #syncTmpl>
+ <span class="label label-info">Syncing</span>
+</ng-template>
+
+<ng-template #progressTmpl
+ let-value="value">
+ <progressbar type="info"
+ [value]="value">
+ </progressbar>
+</ng-template>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
+import { TabsModule } from 'ngx-bootstrap/tabs';
+import { Observable } from 'rxjs/Observable';
+
+import { RbdMirroringService } from '../../../shared/services/rbd-mirroring.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { BlockModule } from '../block.module';
+import { MirrorHealthColorPipe } from '../mirror-health-color.pipe';
+import { MirroringComponent } from './mirroring.component';
+
+describe('MirroringComponent', () => {
+ let component: MirroringComponent;
+ let fixture: ComponentFixture<MirroringComponent>;
+
+ const fakeService = {
+ get: (service_type: string, service_id: string) => {
+ return Observable.create(observer => {
+ return () => console.log('disposed');
+ });
+ }
+ };
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ declarations: [MirroringComponent, MirrorHealthColorPipe],
+ imports: [
+ SharedModule,
+ TabsModule.forRoot(),
+ ProgressbarModule.forRoot(),
+ HttpClientTestingModule
+ ],
+ providers: [{ provide: RbdMirroringService, useValue: fakeService }]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MirroringComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
+import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
+import { RbdMirroringService } from '../../../shared/services/rbd-mirroring.service';
+
+@Component({
+ selector: 'cd-mirroring',
+ templateUrl: './mirroring.component.html',
+ styleUrls: ['./mirroring.component.scss']
+})
+export class MirroringComponent implements OnInit, OnDestroy {
+ @ViewChild('healthTmpl') healthTmpl: TemplateRef<any>;
+ @ViewChild('stateTmpl') stateTmpl: TemplateRef<any>;
+ @ViewChild('syncTmpl') syncTmpl: TemplateRef<any>;
+ @ViewChild('progressTmpl') progressTmpl: TemplateRef<any>;
+
+ contentData: any;
+ interval: any;
+
+ status: ViewCacheStatus;
+ daemons = {
+ data: [],
+ columns: []
+ };
+ pools = {
+ data: [],
+ columns: {}
+ };
+ image_error = {
+ data: [],
+ columns: {}
+ };
+ image_syncing = {
+ data: [],
+ columns: {}
+ };
+ image_ready = {
+ data: [],
+ columns: {}
+ };
+
+ constructor(
+ private http: HttpClient,
+ private rbdMirroringService: RbdMirroringService,
+ private cephShortVersionPipe: CephShortVersionPipe
+ ) { }
+
+ ngOnInit() {
+ this.daemons.columns = [
+ { prop: 'instance_id', name: 'Instance', flexGrow: 2 },
+ { prop: 'id', name: 'ID', flexGrow: 2 },
+ { prop: 'server_hostname', name: 'Hostname', flexGrow: 2 },
+ {
+ prop: 'server_hostname',
+ name: 'Version',
+ pipe: this.cephShortVersionPipe,
+ flexGrow: 2
+ },
+ {
+ prop: 'health',
+ name: 'Health',
+ cellTemplate: this.healthTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ this.pools.columns = [
+ { prop: 'name', name: 'Name', flexGrow: 2 },
+ { prop: 'mirror_mode', name: 'Mode', flexGrow: 2 },
+ { prop: 'leader_id', name: 'Leader', flexGrow: 2 },
+ { prop: 'image_local_count', name: '# Local', flexGrow: 2 },
+ { prop: 'image_remote_count', name: '# Remote', flexGrow: 2 },
+ {
+ prop: 'health',
+ name: 'Health',
+ cellTemplate: this.healthTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ this.image_error.columns = [
+ { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
+ { prop: 'name', name: 'Image', flexGrow: 2 },
+ { prop: 'description', name: 'Issue', flexGrow: 4 },
+ {
+ prop: 'state',
+ name: 'State',
+ cellTemplate: this.stateTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ this.image_syncing.columns = [
+ { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
+ { prop: 'name', name: 'Image', flexGrow: 2 },
+ {
+ prop: 'progress',
+ name: 'Progress',
+ cellTemplate: this.progressTmpl,
+ flexGrow: 2
+ },
+ {
+ prop: 'state',
+ name: 'State',
+ cellTemplate: this.syncTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ this.image_ready.columns = [
+ { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
+ { prop: 'name', name: 'Image', flexGrow: 2 },
+ { prop: 'description', name: 'Description', flexGrow: 4 },
+ {
+ prop: 'state',
+ name: 'State',
+ cellTemplate: this.stateTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ setTimeout(() => {
+ this.interval = this.refresh();
+ }, 30000);
+ }
+
+ ngOnDestroy() {
+ clearInterval(this.interval);
+ }
+
+ refresh() {
+ this.rbdMirroringService.get().subscribe((data: any) => {
+ this.daemons.data = data.content_data.daemons;
+ this.pools.data = data.content_data.pools;
+ this.image_error.data = data.content_data.image_error;
+ this.image_syncing.data = data.content_data.image_syncing;
+ this.image_ready.data = data.content_data.image_ready;
+
+ this.status = data.status;
+ });
+ }
+}
class="dropdown tc_menuitem tc_menuitem_block">
<a dropdownToggle
class="dropdown-toggle"
- data-toggle="dropdown">
+ data-toggle="dropdown"
+ [ngStyle]="blockHealthColor()">
<ng-container i18n>Block</ng-container>
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_block_mirroring">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/mirroring/"> Mirroring
+ <small *ngIf="summaryData?.rbd_mirroring?.warnings !== 0"
+ class="label label-warning">{{ summaryData?.rbd_mirroring?.warnings }}</small>
+ <small *ngIf="summaryData?.rbd_mirroring?.errors !== 0"
+ class="label label-danger">{{ summaryData?.rbd_mirroring?.errors }}</small>
+ </a>
+ </li>
+
<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>
this.rbdPools = data.rbd_pools;
});
}
+
+ blockHealthColor() {
+ if (this.summaryData && this.summaryData.rbd_mirroring) {
+ if (this.summaryData.rbd_mirroring.errors > 0) {
+ return { color: '#d9534f' };
+ } else if (this.summaryData.rbd_mirroring.warnings > 0) {
+ return { color: '#f0ad4e' };
+ }
+ }
+ }
}
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { RbdMirroringService } from './rbd-mirroring.service';
+
+describe('RbdMirroringService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [RbdMirroringService],
+ imports: [HttpClientTestingModule, HttpClientModule]
+ });
+ });
+
+ it('should be created', inject([RbdMirroringService], (service: RbdMirroringService) => {
+ expect(service).toBeTruthy();
+ }));
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class RbdMirroringService {
+ constructor(private http: HttpClient) {}
+
+ get() {
+ return this.http.get('/api/rbdmirror');
+ }
+}
import { ConfigurationService } from './configuration.service';
import { FormatterService } from './formatter.service';
+import { RbdMirroringService } from './rbd-mirroring.service';
import { SummaryService } from './summary.service';
import { TcmuIscsiService } from './tcmu-iscsi.service';
@NgModule({
- imports: [
- CommonModule
- ],
+ imports: [CommonModule],
declarations: [],
- providers: [FormatterService, SummaryService, TcmuIscsiService, ConfigurationService]
+ providers: [
+ FormatterService,
+ SummaryService,
+ TcmuIscsiService,
+ ConfigurationService,
+ RbdMirroringService
+ ]
})
export class ServicesModule { }