]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard_v2: add mirroring page
authorTiago Melo <tmelo@suse.com>
Mon, 19 Feb 2018 10:16:03 +0000 (10:16 +0000)
committerRicardo Dias <rdias@suse.com>
Mon, 5 Mar 2018 13:07:15 +0000 (13:07 +0000)
Signed-off-by: Tiago Melo <tmelo@suse.com>
13 files changed:
src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/services.module.ts

index 7e7e84106cb63ab586e0af128f5a4aef0bdafe51..8883796d367dbb2a952d9caab36751eb3b60a7f8 100644 (file)
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
 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';
@@ -40,6 +41,7 @@ const routes: Routes = [
   { 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'}
index b1c71c52a7cdf024430f194f78c0865a56126d79..6e094fa04d6ab24867a51cebf4aedcdde08bd72e 100644 (file)
@@ -2,13 +2,16 @@ import { CommonModule } from '@angular/common';
 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({
@@ -16,6 +19,7 @@ import { PoolDetailComponent } from './pool-detail/pool-detail.component';
     CommonModule,
     FormsModule,
     TabsModule.forRoot(),
+    ProgressbarModule.forRoot(),
     SharedModule,
     ComponentsModule,
     PipesModule,
@@ -23,7 +27,9 @@ import { PoolDetailComponent } from './pool-detail/pool-detail.component';
   ],
   declarations: [
     PoolDetailComponent,
-    IscsiComponent
+    IscsiComponent,
+    MirroringComponent,
+    MirrorHealthColorPipe
   ]
 })
 export class BlockModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts
new file mode 100644 (file)
index 0000000..f22bcf2
--- /dev/null
@@ -0,0 +1,8 @@
+import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
+
+describe('MirrorHealthColorPipe', () => {
+  it('create an instance', () => {
+    const pipe = new MirrorHealthColorPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.ts
new file mode 100644 (file)
index 0000000..43d880f
--- /dev/null
@@ -0,0 +1,17 @@
+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';
+  }
+}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.html
new file mode 100644 (file)
index 0000000..405889e
--- /dev/null
@@ -0,0 +1,89 @@
+<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>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts
new file mode 100644 (file)
index 0000000..accc564
--- /dev/null
@@ -0,0 +1,50 @@
+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();
+  });
+});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.ts
new file mode 100644 (file)
index 0000000..b89a23f
--- /dev/null
@@ -0,0 +1,146 @@
+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;
+    });
+  }
+}
index 378e70a95714f3d5898c7aab4c238cdb60336f85..e8981802eed3c2f5fe06a5052a97db47800b483b 100644 (file)
           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>
index 70087338dadefc4603346353cab4e13de035637f..ee61c41134fa2e8e7628d75d0ccbb2f080b1a470 100644 (file)
@@ -18,4 +18,14 @@ export class NavigationComponent implements OnInit {
       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' };
+      }
+    }
+  }
 }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts
new file mode 100644 (file)
index 0000000..0f59831
--- /dev/null
@@ -0,0 +1,18 @@
+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();
+  }));
+});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.ts
new file mode 100644 (file)
index 0000000..cec38d1
--- /dev/null
@@ -0,0 +1,11 @@
+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');
+  }
+}
index 051f970ac6d453042e0a495b9a0e308f91167812..04d4a3ccbde3cbaed7c193a621fc055d57704863 100644 (file)
@@ -3,14 +3,19 @@ import { NgModule } from '@angular/core';
 
 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 { }