]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: integrate notification pills with notification sidebar statusCard
authorNizamudeen A <nia@redhat.com>
Wed, 14 Sep 2022 16:03:56 +0000 (21:33 +0530)
committerNizamudeen A <nia@redhat.com>
Wed, 14 Sep 2022 16:03:56 +0000 (21:33 +0530)
Signed-off-by: Nizamudeen A <nia@redhat.com>
15 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/notification-count.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts

index b088e92251f0a0dfe197f4f6c31b03e776dfbaa8..ab688a40e4e4fe7f36e9149dcc8d662305b09870 100644 (file)
       </dl>
     </cd-card>
 
-    <cd-card *ngIf="healthData"
-             title="Status"
+    <cd-card title="Status"
              i18n-title
              class="col-sm-6 px-3">
-      <div class="d-flex">
-        <i *ngIf="healthData.health?.status == 'HEALTH_OK'"
-           class="pl-2 fa fa-check-circle text-success"></i>
-        <i *ngIf="healthData.health?.status == 'HEALTH_WARN'"
-           class="pl-2 fa fa-exclamation-triangle text-warning"></i>
-        <i *ngIf="healthData.health?.status == 'HEALTH_ERR'"
-           class="pl-2 fa fa-exclamation-triangle text-danger"></i>
-        <p class="pl-2 pr-5">Cluster</p>
-      </div>
-      <hr />
-      <div class="d-flex flex-wrap">
-        <p>Notifications</p>
-        <!-- Potentially make widget component -->
-        <button class="ml-3 btn btn-outline-danger rounded-pill">
-          <i class="fa fa-exclamation-circle"></i>
-          <a>1</a>
-        </button>
-        <span class="ml-2 btn btn-outline-warning rounded-pill">
-          <i class="fa fa-exclamation-triangle"></i>
-          <a>1</a>
-        </span>
-        <span class="ml-2 btn btn-outline-success rounded-pill">
-          <i class="fa fa-check-circle"></i>
-          <a>1</a>
-        </span>
-        <span class="ml-2 btn btn-outline-primary rounded-pill">
-          <i class="fa fa-info-circle"></i>
-          <a>1</a>
-        </span>
-        <span class="ml-2 btn btn-outline-info rounded-pill">
-          <i class="fa fa-bell"></i>
-          <a>1</a>
-        </span>
+      <div class="d-flex ml-2">
+        <i *ngIf="healthData?.health?.status"
+           [ngClass]="[healthData.health.status | healthIcon, icons.large]"
+           [ngStyle]="healthData.health.status | healthColor"
+           [title]="healthData.health.status"></i>
+        <span class="ml-2 mt-n1"
+              i18n>Cluster</span>
       </div>
+      <section class="border-top mt-5">
+        <div class="d-flex flex-wrap">
+          <span class="pt-2"
+                i18n>Notifications</span>
+          <!-- Potentially make widget component -->
+          <button class="btn btn-outline-danger rounded-pill ml-2"
+                  (click)="toggleSidebar('Prometheus', type.error)"
+                  id="dangerNotification">
+            <i [ngClass]="[icons.danger]"></i>
+            <span *ngIf="notificationCountTest$ | async as count">{{ count?.error }}</span>
+          </button>
+
+          <button class="btn btn-outline-success rounded-pill ml-2"
+                  (click)="toggleSidebar('Prometheus', type.success)"
+                  id="successNotification">
+            <i [ngClass]="[icons.success]"></i>
+            <span *ngIf="notificationCountTest$ | async as count">{{ count.success }}</span>
+          </button>
+          <button class="btn btn-outline-primary rounded-pill ml-2"
+                  (click)="toggleSidebar('Prometheus', type.info)"
+                  id="infoNotification">
+            <i [ngClass]="[icons.infoCircle]"></i>
+            <span *ngIf="notificationCountTest$ | async as count">{{ count?.info }}</span>
+          </button>
+          <button class="btn btn-outline-info rounded-pill ml-2"
+                  (click)="toggleSidebar('Ceph')"
+                  id="cephNotification">
+            <i [ngClass]="[icons.bell]"></i>
+            <span *ngIf="notificationCountTest$ | async as count">{{ count?.cephNotifications }}</span>
+          </button>
+        </div>
+      </section>
     </cd-card>
 
     <cd-card title="Capacity"
index b43d462878bf562a50613025987906ebe6e0c2b4..8ac13de9e4ebf3492483ac9c3db4796f724282ca 100644 (file)
@@ -1,14 +1,19 @@
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { NO_ERRORS_SCHEMA } from '@angular/core';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
 import { RouterTestingModule } from '@angular/router/testing';
 
 import { BehaviorSubject, of } from 'rxjs';
 
 import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { HealthService } from '~/app/shared/api/health.service';
 import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
 import { CssHelper } from '~/app/shared/classes/css-helper';
-import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { NotificationService } from '~/app/shared/services/notification.service';
 import { SummaryService } from '~/app/shared/services/summary.service';
 import { configureTestBed } from '~/testing/unit-test-helper';
 import { CardComponent } from '../card/card.component';
@@ -33,6 +38,30 @@ describe('CardComponent', () => {
   let fixture: ComponentFixture<DashboardComponent>;
   let configurationService: ConfigurationService;
   let orchestratorService: MgrModuleService;
+  let getHealthSpy: jasmine.Spy;
+  let getNotificationCountSpy: jasmine.Spy;
+  const healthPayload: Record<string, any> = {
+    health: { status: 'HEALTH_OK' },
+    mon_status: { monmap: { mons: [] }, quorum: [] },
+    osd_map: { osds: [] },
+    mgr_map: { standbys: [] },
+    hosts: 0,
+    rgw: 0,
+    fs_map: { filesystems: [], standbys: [] },
+    iscsi_daemons: 0,
+    client_perf: {},
+    scrub_status: 'Inactive',
+    pools: [],
+    df: { stats: {} },
+    pg_info: { object_stats: { num_objects: 0 } }
+  };
+
+  const notificationCountPayload: Record<string, number> = {
+    info: 3,
+    error: 4,
+    success: 5,
+    cephNotifications: 10
+  }
 
   const configValueData: any = {
     value: [
@@ -52,13 +81,14 @@ describe('CardComponent', () => {
   };
 
   configureTestBed({
-    imports: [RouterTestingModule, HttpClientTestingModule],
+    imports: [RouterTestingModule, HttpClientTestingModule, 
+      ToastrModule.forRoot(),
+      PipesModule],
     declarations: [DashboardComponent, CardComponent, DashboardPieComponent],
     schemas: [NO_ERRORS_SCHEMA],
     providers: [
-      CssHelper,
-      DimlessBinaryPipe,
-      { provide: SummaryService, useClass: SummaryServiceMock }
+      { provide: SummaryService, useClass: SummaryServiceMock },
+      CssHelper
     ]
   });
 
@@ -67,6 +97,10 @@ describe('CardComponent', () => {
     component = fixture.componentInstance;
     configurationService = TestBed.inject(ConfigurationService);
     orchestratorService = TestBed.inject(MgrModuleService);
+    getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth');
+    getHealthSpy.and.returnValue(of(healthPayload));
+    getNotificationCountSpy = spyOn(TestBed.inject(NotificationService), 'getNotificationCount');
+    getNotificationCountSpy.and.returnValue(of(notificationCountPayload));
   });
 
   it('should create', () => {
@@ -86,4 +120,67 @@ describe('CardComponent', () => {
     expect(component.detailsCardData.orchestrator).toBe('Cephadm');
     expect(component.detailsCardData.cephVersion).toBe('17.0.0-12222-gcd0cd7cb quincy (dev)');
   });
+
+  it('should check if the respective icon is shown for each status', () => {
+    const payload = _.cloneDeep(healthPayload);
+
+    // HEALTH_WARN
+    payload.health['status'] = 'HEALTH_WARN';
+    payload.health['checks'] = [
+      { severity: 'HEALTH_WARN', type: 'WRN', summary: { message: 'fake warning' } }
+    ];
+
+    getHealthSpy.and.returnValue(of(payload));
+    fixture.detectChanges();
+    const clusterStatusCard = fixture.debugElement.query(
+      By.css('cd-card[title="Status"] i')
+    );
+    expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
+
+    // HEALTH_ERR
+    payload.health['status'] = 'HEALTH_ERR';
+    payload.health['checks'] = [
+      { severity: 'HEALTH_ERR', type: 'ERR', summary: { message: 'fake error' } }
+    ];
+
+    getHealthSpy.and.returnValue(of(payload));
+    fixture.detectChanges();
+    expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
+
+    // HEALTH_OK
+    payload.health['status'] = 'HEALTH_OK';
+    payload.health['checks'] = [
+      { severity: 'HEALTH_OK', type: 'OK', summary: { message: 'fake success' } }
+    ];
+
+    getHealthSpy.and.returnValue(of(payload));
+    fixture.detectChanges();
+    expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
+  });
+
+  it('should show the actual notification count on each notification pill', () => {
+    const payload = _.cloneDeep(notificationCountPayload);
+    fixture.detectChanges();
+
+    const cephNotification = fixture.debugElement.query(
+      By.css('button[id=cephNotification] span')
+    );
+
+    const successNotification = fixture.debugElement.query(
+      By.css('button[id=successNotification] span')
+    );
+
+    const dangerNotification = fixture.debugElement.query(
+      By.css('button[id=dangerNotification] span')
+    );
+
+    const infoNotification = fixture.debugElement.query(
+      By.css('button[id=infoNotification] span')
+    );
+
+    expect(cephNotification.nativeElement.textContent).toBe(payload.cephNotifications.toString());
+    expect(successNotification.nativeElement.textContent).toBe(payload.success.toString());
+    expect(dangerNotification.nativeElement.textContent).toBe(payload.error.toString());
+    expect(infoNotification.nativeElement.textContent).toBe(payload.info.toString());
+  });
 });
index 5c094b00fd540fbeeb746f2aa1a5be9b6edf3fe2..07fb38180c64b7f31c34094ce925e0a8bc0fc4f7 100644 (file)
@@ -7,8 +7,12 @@ import { ClusterService } from '~/app/shared/api/cluster.service';
 import { ConfigurationService } from '~/app/shared/api/configuration.service';
 import { HealthService } from '~/app/shared/api/health.service';
 import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 import { OsdService } from '~/app/shared/api/osd.service';
 import { DashboardDetails } from '~/app/shared/models/cd-details';
+import { NotificationCount } from '~/app/shared/models/notification-count.model';
+import { NotificationService } from '~/app/shared/services/notification.service';
 import { Permissions } from '~/app/shared/models/permissions';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import {
@@ -22,7 +26,7 @@ import { SummaryService } from '~/app/shared/services/summary.service';
   templateUrl: './dashboard.component.html',
   styleUrls: ['./dashboard.component.scss']
 })
-export class DashboardComponent implements OnInit, OnDestroy {
+export class DashboardComponent implements OnInit, OnDestroy{
   detailsCardData: DashboardDetails = {};
   osdSettings$: Observable<any>;
   interval = new Subscription();
@@ -31,6 +35,13 @@ export class DashboardComponent implements OnInit, OnDestroy {
   color: string;
   capacity$: Observable<any>;
   healthData: any;
+
+  notificationType: number;
+  notificationCountTest$: Observable<NotificationCount>;
+  type = NotificationType;
+
+  icons = Icons;
+
   constructor(
     private summaryService: SummaryService,
     private configService: ConfigurationService,
@@ -39,7 +50,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
     private osdService: OsdService,
     private authStorageService: AuthStorageService,
     private featureToggles: FeatureTogglesService,
-    private healthService: HealthService
+    private healthService: HealthService,
+    public notificationService: NotificationService,
   ) {
     this.permissions = this.authStorageService.getPermissions();
     this.enabledFeature$ = this.featureToggles.get();
@@ -51,6 +63,12 @@ export class DashboardComponent implements OnInit, OnDestroy {
     this.osdSettings$ = this.osdService.getOsdSettings();
     this.capacity$ = this.clusterService.getCapacity();
     this.getHealth();
+    this.notificationCountTest$ = this.notificationService.getNotificationCount();
+  }
+
+  toggleSidebar(notificationApplication = 'Prometheus', notificationType = -1) {
+    this.notificationService.toggleSidebar(false, notificationApplication, notificationType, (this.notificationType !== notificationType))
+    this.notificationType = notificationType;
   }
 
   ngOnDestroy() {
index 89c6c4037941be719b0ce01a5808ce1ca1bd2f0b..ebaf331966f1553832dab8818ed74774b4120d41 100644 (file)
@@ -42,6 +42,6 @@ export class NotificationsComponent implements OnInit, OnDestroy {
   }
 
   toggleSidebar() {
-    this.notificationService.toggleSidebar();
+    this.notificationService.toggleSidebar(false, 'all');
   }
 }
index bba23747b01d4be0aeceebd3f300887f4e0bd8a9..090cdb9b93cb5bcdd63bacbbfbbae4d337627282 100644 (file)
@@ -1,7 +1,8 @@
 <ng-template #tasksTpl>
   <!-- Executing -->
   <div *ngFor="let executingTask of executingTasks; trackBy:trackByFn">
-    <div class="card tc_task border-0">
+    <div class="card tc_task border-0"
+         *ngIf="notificationApplication !== 'Prometheus'">
       <div class="row no-gutters">
         <div class="col-md-2 text-center">
           <span [ngClass]="[icons.stack, icons.large2x]"
@@ -44,7 +45,7 @@
   <ng-container *ngIf="notifications.length > 0">
     <button type="button"
             class="btn btn-light btn-block"
-            (click)="removeAll(); $event.stopPropagation()">
+            (click)="removeSpecific(notificationType, notificationApplication); $event.stopPropagation()">
       <i [ngClass]="[icons.trash]"
          aria-hidden="true"></i>
       &nbsp;
@@ -55,7 +56,8 @@
 
     <div *ngFor="let notification of notifications; let i = index"
          [ngClass]="notification.borderClass">
-      <div class="card tc_notification border-0">
+      <div class="card tc_notification border-0"
+           *ngIf="(notification.type === notificationType || notificationType === -1) && (notification.application === notificationApplication || notificationApplication === 'all')">
         <div class="row no-gutters">
           <div class="col-md-2 text-center">
             <span [ngClass]="[icons.stack, icons.large2x,  notification.textClass]">
@@ -91,9 +93,9 @@
             </div>
           </div>
         </div>
+        <hr>
       </div>
 
-      <hr>
     </div>
   </ng-container>
 </ng-template>
   </div>
 </ng-template>
 
-<div class="card"
-     (clickOutside)="closeSidebar()"
-     [clickOutsideEnabled]="isSidebarOpened">
+<div class="card">
   <div class="card-header">
     <ng-container i18n>Tasks and Notifications</ng-container>
 
index 596f3c358bf4d8b9f44de077ef225364645068f1..42398431c2999f25b3813dd8de8b8a319aaa2bea 100644 (file)
@@ -166,7 +166,7 @@ describe('NotificationsSidebarComponent', () => {
     it('should always close if sidebarSubject value is true', fakeAsync(() => {
       // Closed before next value
       expect(component.isSidebarOpened).toBeFalsy();
-      notificationService.sidebarSubject.next(true);
+      notificationService.sidebarSubject.next({forceClose: true, keepOpen: false});
       tick();
       expect(component.isSidebarOpened).toBeFalsy();
 
index 8c5caf7ff6bd69c8eb0e51ff573dc95bed139b28..2b20c23106058778e425188b287e38ef1c838a3c 100644 (file)
@@ -5,7 +5,7 @@ import {
   HostBinding,
   NgZone,
   OnDestroy,
-  OnInit
+  OnInit,
 } from '@angular/core';
 
 import { Mutex } from 'async-mutex';
@@ -33,6 +33,10 @@ import { TaskMessageService } from '~/app/shared/services/task-message.service';
 export class NotificationsSidebarComponent implements OnInit, OnDestroy {
   @HostBinding('class.active') isSidebarOpened = false;
 
+  notificationType: number;
+
+  notificationApplication: string = 'Ceph';
+
   notifications: CdNotification[];
   private interval: number;
   private timeout: number;
@@ -93,13 +97,24 @@ export class NotificationsSidebarComponent implements OnInit, OnDestroy {
     );
 
     this.subs.add(
-      this.notificationService.sidebarSubject.subscribe((forceClose) => {
+      this.notificationService.sidebarSubject.subscribe(({ forceClose, notificationType, notificationApplication, keepOpen }) => {
         if (forceClose) {
           this.isSidebarOpened = false;
+        } else if (keepOpen) {
+          this.isSidebarOpened = true;
         } else {
           this.isSidebarOpened = !this.isSidebarOpened;
+          this.notificationType = -1;
         }
 
+        notificationType !== -1
+          ? this.notificationType = notificationType
+          : this.notificationType = -1;
+
+          notificationApplication
+          ? this.notificationApplication = notificationApplication
+          : this.notificationApplication = 'Ceph';
+
         window.clearTimeout(this.timeout);
         this.timeout = window.setTimeout(() => {
           this.cdRef.detectChanges();
@@ -157,6 +172,12 @@ export class NotificationsSidebarComponent implements OnInit, OnDestroy {
     this.notificationService.remove(index);
   }
 
+  removeSpecific(type: number, application = 'Prometheus') {
+    application === 'all'
+      ? this.removeAll()
+      : this.notificationService.removeSpecific(type, application)
+  }
+
   closeSidebar() {
     this.isSidebarOpened = false;
   }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts
new file mode 100644 (file)
index 0000000..d11937e
--- /dev/null
@@ -0,0 +1,6 @@
+export enum HealthIcon {
+    HEALTH_ERR = 'fa fa-exclamation-circle',
+    HEALTH_WARN = 'fa fa-exclamation-triangle',
+    HEALTH_OK = 'fa fa-check-circle'
+  }
+  
\ No newline at end of file
index 6b65f04e8cb2f47af1489ba22dc7d5177fa4d966..b7ecde6f1f0c38f697368afa345aa14cfb0835f9 100644 (file)
@@ -34,6 +34,8 @@ export enum Icons {
   info = 'fa fa-info', // Notification information
   infoCircle = 'fa fa-info-circle', // Info on landing page
   questionCircle = 'fa fa-question-circle-o',
+  danger = 'fa fa-exclamation-circle',
+  success = 'fa fa-check-circle',
   check = 'fa fa-check', // Notification check
   show = 'fa fa-eye', // Show
   paragraph = 'fa fa-paragraph', // Silence Matcher - Attribute name
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/notification-count.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/notification-count.model.ts
new file mode 100644 (file)
index 0000000..b5ef070
--- /dev/null
@@ -0,0 +1,6 @@
+export interface NotificationCount {
+    error: number;
+    info: number;
+    success: number;
+    cephNotifications: number;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts
new file mode 100644 (file)
index 0000000..e4450d9
--- /dev/null
@@ -0,0 +1,20 @@
+import { HealthIconPipe } from './health-icon.pipe';
+
+describe('HealthIconPipe', () => {
+  const pipe = new HealthIconPipe();
+  it('create an instance', () => {
+    expect(pipe).toBeTruthy();
+  });
+
+  it('transforms "HEALTH_OK"', () => {
+    expect(pipe.transform('HEALTH_OK')).toEqual('fa fa-check-circle');
+  });
+
+  it('transforms "HEALTH_WARN"', () => {
+    expect(pipe.transform('HEALTH_WARN')).toEqual('fa fa-exclamation-triangle');
+  });
+
+  it('transforms "HEALTH_ERR"', () => {
+    expect(pipe.transform('HEALTH_ERR')).toEqual('fa fa-exclamation-circle');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts
new file mode 100644 (file)
index 0000000..0398626
--- /dev/null
@@ -0,0 +1,16 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { HealthIcon } from '../enum/health-icon.enum';
+
+@Pipe({
+  name: 'healthIcon'
+})
+export class HealthIconPipe implements PipeTransform {
+
+  constructor() {}
+
+  transform(value: any): any {
+    return Object.keys(HealthIcon).includes(value as HealthIcon)
+      ? HealthIcon[value]
+      : null;
+  }
+}
index 91d611c0a55fc5c982bb7821d847e5772f0593bb..5abd7ece8637857ff4da7570a0039f8b6d53b362 100755 (executable)
@@ -30,6 +30,7 @@ import { SanitizeHtmlPipe } from './sanitize-html.pipe';
 import { SearchHighlightPipe } from './search-highlight.pipe';
 import { TruncatePipe } from './truncate.pipe';
 import { UpperFirstPipe } from './upper-first.pipe';
+import { HealthIconPipe } from './health-icon.pipe';
 
 @NgModule({
   imports: [CommonModule],
@@ -62,7 +63,8 @@ import { UpperFirstPipe } from './upper-first.pipe';
     MapPipe,
     TruncatePipe,
     SanitizeHtmlPipe,
-    SearchHighlightPipe
+    SearchHighlightPipe,
+    HealthIconPipe
   ],
   exports: [
     ArrayPipe,
@@ -93,7 +95,8 @@ import { UpperFirstPipe } from './upper-first.pipe';
     MapPipe,
     TruncatePipe,
     SanitizeHtmlPipe,
-    SearchHighlightPipe
+    SearchHighlightPipe,
+    HealthIconPipe
   ],
   providers: [
     ArrayPipe,
@@ -120,7 +123,8 @@ import { UpperFirstPipe } from './upper-first.pipe';
     DurationPipe,
     MapPipe,
     TruncatePipe,
-    SanitizeHtmlPipe
+    SanitizeHtmlPipe,
+    HealthIconPipe
   ]
 })
 export class PipesModule {}
index 028dd90ea39684f174fd8f6007a48697741691b0..8685b6e524d31799220fcaafbfb09d61d79b5055 100644 (file)
@@ -51,7 +51,7 @@ describe('NotificationService', () => {
       'cdNotifications',
       '[{"type":2,"message":"foobar","timestamp":"2018-05-24T09:41:32.726Z"}]'
     );
-    service = new NotificationService(null, null, null);
+    service = new NotificationService(null, null, null, null);
     expect(service['dataSource'].getValue().length).toBe(1);
   }));
 
index c05dbce0f571fb7ea173f64e095c84407a632d8c..3ebe2efd165e32f1cca8003475e80ffd62347807 100644 (file)
@@ -2,13 +2,15 @@ import { Injectable } from '@angular/core';
 
 import _ from 'lodash';
 import { IndividualConfig, ToastrService } from 'ngx-toastr';
-import { BehaviorSubject, Subject } from 'rxjs';
+import { BehaviorSubject, Observable, of as observableOf, Subject } from 'rxjs';
 
 import { NotificationType } from '../enum/notification-type.enum';
 import { CdNotification, CdNotificationConfig } from '../models/cd-notification';
 import { FinishedTask } from '../models/finished-task';
+import { NotificationCount } from '../models/notification-count.model';
 import { CdDatePipe } from '../pipes/cd-date.pipe';
 import { TaskMessageService } from './task-message.service';
+import { TimerService } from './timer.service';
 
 @Injectable({
   providedIn: 'root'
@@ -30,7 +32,8 @@ export class NotificationService {
   constructor(
     public toastr: ToastrService,
     private taskMessageService: TaskMessageService,
-    private cdDatePipe: CdDatePipe
+    private cdDatePipe: CdDatePipe,
+    private timerService: TimerService
   ) {
     const stringNotifications = localStorage.getItem(this.KEY);
     let notifications: CdNotification[] = [];
@@ -55,6 +58,27 @@ export class NotificationService {
     this.dataSource.next([]);
   }
 
+  /**
+   * Removes a specific set of notification given the type and application.
+   */
+  removeSpecific(type: number, app = 'Prometheus') {
+    const recent = this.dataSource.getValue();
+    let indices = [];
+
+    for(let index = 0; index < recent.length; index++) {
+      if (app === 'Prometheus' && recent[index].type === type && recent[index].application === app) {
+        indices.push(index);
+      } else if (app !== 'Prometheus' && recent[index].application === app) {
+        indices.push(index);
+      }
+    }
+    for (const index of indices.reverse()) {
+      recent.splice(index, 1);
+    }
+    this.dataSource.next(recent);
+    localStorage.setItem(this.KEY, JSON.stringify(recent));
+  }
+
   /**
    * Removes a single saved notifications
    */
@@ -123,6 +147,24 @@ export class NotificationService {
     }, 10);
   }
 
+  getNotificationCount() : Observable<NotificationCount> {
+    return this.timerService.get(() => observableOf({
+      error: this._getNotificationCount(NotificationType.error),
+      info: this._getNotificationCount(NotificationType.info),
+      success: this._getNotificationCount(NotificationType.success),
+      cephNotifications: this._getNotificationCount(undefined, 'Ceph')
+     }), 1000
+    );
+  }
+
+  private _getNotificationCount(type?: number, application = 'Prometheus') {
+    return this.dataSource.getValue().filter((notification: CdNotification) =>
+      application === 'Prometheus'
+        ? notification.application === application && notification.type === type
+        : notification.application === application
+    ).length;
+  }
+
   private queueToShow(config: CdNotificationConfig) {
     this.cancel(this.queuedTimeoutId);
     if (!this.queued.find((c) => _.isEqual(c, config))) {
@@ -231,7 +273,12 @@ export class NotificationService {
     this.hideToasties = suspend;
   }
 
-  toggleSidebar(forceClose = false) {
-    this.sidebarSubject.next(forceClose);
+  toggleSidebar(forceClose = false, notificationApplication = 'Prometheus', notificationType = -1, keepOpen = false) {
+    this.sidebarSubject.next({
+      forceClose: forceClose,
+      notificationApplication: notificationApplication,
+      notificationType: notificationType,
+      keepOpen: keepOpen
+    });
   }
 }