]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: add telemetry activation notification
authorTatjana Dehler <tdehler@suse.com>
Wed, 13 May 2020 11:19:47 +0000 (13:19 +0200)
committerTatjana Dehler <tdehler@suse.com>
Fri, 19 Jun 2020 12:28:52 +0000 (14:28 +0200)
The commit adds 2 different parts to show the
Telemetry activation notification in the dashboard:

1. The Telemetry activation notification component
   itself. It contains the definition of the
   notification panel.
2. The Telemetry notification service. The service
   is needed to be able to show/hide the
   notification from:
   * the component itself (e.g. when clicking the
     button button)
   * the Telemetry configuration component (when
     enabling/disabling Telemetry)
   * the navigation component (to set the css-
     classes accordingly)

Fixes: https://tracker.ceph.com/issues/45464
Signed-off-by: Tatjana Dehler <tdehler@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/telemetry/telemetry.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.ts [new file with mode: 0644]

index b3282e1b5d566672396af4f770dff4a5ed2385f4..40c00795e3696b60254d2b0aa062b35f4cc52bce 100644 (file)
@@ -14,6 +14,7 @@ import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../shared/forms/cd-validators';
 import { NotificationService } from '../../../shared/services/notification.service';
+import { TelemetryNotificationService } from '../../../shared/services/telemetry-notification.service';
 import { TextToDownloadService } from '../../../shared/services/text-to-download.service';
 
 @Component({
@@ -50,7 +51,8 @@ export class TelemetryComponent extends CdForm implements OnInit {
     private router: Router,
     private telemetryService: TelemetryService,
     private i18n: I18n,
-    private textToDownloadService: TextToDownloadService
+    private textToDownloadService: TextToDownloadService,
+    private telemetryNotificationService: TelemetryNotificationService
   ) {
     super();
   }
@@ -172,6 +174,7 @@ complete the next step and accept the license.`
 
   disableModule(message: string = null, followUpFunc: Function = null) {
     this.telemetryService.enable(false).subscribe(() => {
+      this.telemetryNotificationService.setVisibility(true);
       if (message) {
         this.notificationService.show(NotificationType.success, message);
       }
@@ -197,6 +200,7 @@ complete the next step and accept the license.`
 
   onSubmit() {
     this.telemetryService.enable().subscribe(() => {
+      this.telemetryNotificationService.setVisibility(false);
       this.notificationService.show(
         NotificationType.success,
         this.i18n('The Telemetry module has been configured and activated successfully.')
index 092770d044b1e2db6c7b54e33cc88e34c4ade727..2a42f711fb55b3ebf294a62b463dadca048c23ff 100644 (file)
@@ -1,4 +1,5 @@
 <cd-pwd-expiration-notification></cd-pwd-expiration-notification>
+<cd-telemetry-notification></cd-telemetry-notification>
 <cd-notifications-sidebar></cd-notifications-sidebar>
 
 <div class="cd-navbar-top">
index 0f7764e05d9d85b86dea02ffad842e510ba9d038..25fb4d6f7897cf7cadccb66147e51e64158d1655 100644 (file)
@@ -57,7 +57,8 @@ describe('NavigationComponent', () => {
         provide: AuthStorageService,
         useValue: {
           getPermissions: jest.fn(),
-          isPwdDisplayed$: { subscribe: jest.fn() }
+          isPwdDisplayed$: { subscribe: jest.fn() },
+          telemetryNotification$: { subscribe: jest.fn() }
         }
       },
       { provide: SummaryService, useValue: { subscribe: jest.fn() } },
index ce04147754015fa07f29179157a688413ad56c39..5d45104cb82659df02f8244a94f9af3181e54340 100644 (file)
@@ -11,6 +11,7 @@ import {
 } from '../../../shared/services/feature-toggles.service';
 import { PrometheusAlertService } from '../../../shared/services/prometheus-alert.service';
 import { SummaryService } from '../../../shared/services/summary.service';
+import { TelemetryNotificationService } from '../../../shared/services/telemetry-notification.service';
 
 @Component({
   selector: 'cd-navigation',
@@ -41,6 +42,7 @@ export class NavigationComponent implements OnInit, OnDestroy {
     private authStorageService: AuthStorageService,
     private summaryService: SummaryService,
     private featureToggles: FeatureTogglesService,
+    private telemetryNotificationService: TelemetryNotificationService,
     public prometheusAlertService: PrometheusAlertService
   ) {
     this.permissions = this.authStorageService.getPermissions();
@@ -63,6 +65,11 @@ export class NavigationComponent implements OnInit, OnDestroy {
         this.showTopNotification('isPwdDisplayed', isDisplayed);
       })
     );
+    this.subs.add(
+      this.telemetryNotificationService.update.subscribe((visible: boolean) => {
+        this.showTopNotification('telemetryNotificationEnabled', visible);
+      })
+    );
   }
 
   ngOnDestroy(): void {
index 84ecc66fc7c5f282c02e630a5b1ed47c2b976f55..55a2779a0402684c5d47b4a8a5ec498d38dc1021 100644 (file)
@@ -38,6 +38,7 @@ import { SelectBadgesComponent } from './select-badges/select-badges.component';
 import { SelectComponent } from './select/select.component';
 import { SparklineComponent } from './sparkline/sparkline.component';
 import { SubmitButtonComponent } from './submit-button/submit-button.component';
+import { TelemetryNotificationComponent } from './telemetry-notification/telemetry-notification.component';
 import { UsageBarComponent } from './usage-bar/usage-bar.component';
 import { ViewCacheComponent } from './view-cache/view-cache.component';
 
@@ -82,6 +83,7 @@ import { ViewCacheComponent } from './view-cache/view-cache.component';
     AlertPanelComponent,
     FormModalComponent,
     PwdExpirationNotificationComponent,
+    TelemetryNotificationComponent,
     OrchestratorDocPanelComponent,
     OrchestratorDocModalComponent
   ],
@@ -104,6 +106,7 @@ import { ViewCacheComponent } from './view-cache/view-cache.component';
     ConfigOptionComponent,
     AlertPanelComponent,
     PwdExpirationNotificationComponent,
+    TelemetryNotificationComponent,
     OrchestratorDocPanelComponent
   ]
 })
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.html
new file mode 100644 (file)
index 0000000..7a801ec
--- /dev/null
@@ -0,0 +1,9 @@
+<ngb-alert class="no-margin-bottom"
+           type="warning"
+           *ngIf="displayNotification"
+           (close)="close($event)">
+  <div i18n>The Telemetry module is not submitting telemetry data at the
+    moment. Click
+  <a routerLink="/telemetry"
+     class="alert-link">here</a> to activate it now.</div>
+</ngb-alert>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.scss
new file mode 100644 (file)
index 0000000..6862c2f
--- /dev/null
@@ -0,0 +1,3 @@
+::ng-deep .no-margin-bottom {
+  margin-bottom: 0;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.spec.ts
new file mode 100644 (file)
index 0000000..7f2b472
--- /dev/null
@@ -0,0 +1,125 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { UserFormModel } from '../../../core/auth/user-form/user-form.model';
+import { MgrModuleService } from '../../api/mgr-module.service';
+import { UserService } from '../../api/user.service';
+import { PipesModule } from '../../pipes/pipes.module';
+import { AuthStorageService } from '../../services/auth-storage.service';
+import { NotificationService } from '../../services/notification.service';
+import { TelemetryNotificationService } from '../../services/telemetry-notification.service';
+import { TelemetryNotificationComponent } from './telemetry-notification.component';
+
+describe('TelemetryActivationNotificationComponent', () => {
+  let component: TelemetryNotificationComponent;
+  let fixture: ComponentFixture<TelemetryNotificationComponent>;
+
+  let authStorageService: AuthStorageService;
+  let userService: UserService;
+  let mgrModuleService: MgrModuleService;
+  let notificationService: NotificationService;
+
+  let isNotificationHiddenSpy: jasmine.Spy;
+  let getUsernameSpy: jasmine.Spy;
+  let userServiceGetSpy: jasmine.Spy;
+  let getConfigSpy: jasmine.Spy;
+
+  const user: UserFormModel = {
+    username: 'username',
+    password: undefined,
+    name: 'User 1',
+    email: 'user1@email.com',
+    roles: ['read-only'],
+    enabled: true,
+    pwdExpirationDate: undefined,
+    pwdUpdateRequired: true
+  };
+  const admin: UserFormModel = {
+    username: 'admin',
+    password: undefined,
+    name: 'User 1',
+    email: 'user1@email.com',
+    roles: ['administrator'],
+    enabled: true,
+    pwdExpirationDate: undefined,
+    pwdUpdateRequired: true
+  };
+  const telemetryEnabledConfig = {
+    enabled: true
+  };
+  const telemetryDisabledConfig = {
+    enabled: false
+  };
+
+  configureTestBed({
+    declarations: [TelemetryNotificationComponent],
+    imports: [NgbAlertModule, HttpClientTestingModule, ToastrModule.forRoot(), PipesModule],
+    providers: [MgrModuleService, UserService, i18nProviders]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(TelemetryNotificationComponent);
+    component = fixture.componentInstance;
+    authStorageService = TestBed.inject(AuthStorageService);
+    userService = TestBed.inject(UserService);
+    mgrModuleService = TestBed.inject(MgrModuleService);
+    notificationService = TestBed.inject(NotificationService);
+
+    isNotificationHiddenSpy = spyOn(component, 'isNotificationHidden').and.returnValue(false);
+    getUsernameSpy = spyOn(authStorageService, 'getUsername').and.returnValue('username');
+    userServiceGetSpy = spyOn(userService, 'get').and.returnValue(of(admin)); // Not the best name but it sounded better than `getSpy`
+    getConfigSpy = spyOn(mgrModuleService, 'getConfig').and.returnValue(
+      of(telemetryDisabledConfig)
+    );
+  });
+
+  it('should create', () => {
+    fixture.detectChanges();
+    expect(component).toBeTruthy();
+  });
+
+  it('should not show notification again if the user closed it before', () => {
+    isNotificationHiddenSpy.and.returnValue(true);
+    fixture.detectChanges();
+    expect(component.displayNotification).toBe(false);
+  });
+
+  it('should not show notification for an user without administrator role', () => {
+    userServiceGetSpy.and.returnValue(of(user));
+    fixture.detectChanges();
+    expect(component.displayNotification).toBe(false);
+  });
+
+  it('should not show notification if the module is enabled already', () => {
+    getUsernameSpy.and.returnValue('admin');
+    getConfigSpy.and.returnValue(of(telemetryEnabledConfig));
+    fixture.detectChanges();
+    expect(component.displayNotification).toBe(false);
+  });
+
+  it('should show the notification if all pre-conditions set accordingly', () => {
+    fixture.detectChanges();
+    expect(component.displayNotification).toBe(true);
+  });
+
+  it('should hide the notification if the user closes it', () => {
+    spyOn(notificationService, 'show');
+    fixture.detectChanges();
+    component.close();
+    expect(notificationService.show).toHaveBeenCalled();
+    expect(localStorage.getItem('telemetry_notification_hidden')).toBe('true');
+  });
+
+  it('should hide the notification if the user logs out', () => {
+    const telemetryNotificationService = TestBed.inject(TelemetryNotificationService);
+    spyOn(telemetryNotificationService, 'setVisibility');
+    fixture.detectChanges();
+    component.ngOnDestroy();
+    expect(telemetryNotificationService.setVisibility).toHaveBeenCalledWith(false);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/telemetry-notification/telemetry-notification.component.ts
new file mode 100644 (file)
index 0000000..974f625
--- /dev/null
@@ -0,0 +1,69 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+
+import { UserFormModel } from '../../../core/auth/user-form/user-form.model';
+import { MgrModuleService } from '../../api/mgr-module.service';
+import { UserService } from '../../api/user.service';
+import { NotificationType } from '../../enum/notification-type.enum';
+import { AuthStorageService } from '../../services/auth-storage.service';
+import { NotificationService } from '../../services/notification.service';
+import { TelemetryNotificationService } from '../../services/telemetry-notification.service';
+
+@Component({
+  selector: 'cd-telemetry-notification',
+  templateUrl: './telemetry-notification.component.html',
+  styleUrls: ['./telemetry-notification.component.scss']
+})
+export class TelemetryNotificationComponent implements OnInit, OnDestroy {
+  displayNotification = false;
+
+  constructor(
+    private mgrModuleService: MgrModuleService,
+    private authStorageService: AuthStorageService,
+    private userService: UserService,
+    private notificationService: NotificationService,
+    private telemetryNotificationService: TelemetryNotificationService,
+    private i18n: I18n
+  ) {}
+
+  ngOnInit() {
+    this.telemetryNotificationService.update.subscribe((visible: boolean) => {
+      this.displayNotification = visible;
+    });
+
+    if (!this.isNotificationHidden()) {
+      const username = this.authStorageService.getUsername();
+      this.userService.get(username).subscribe((user: UserFormModel) => {
+        if (user.roles.includes('administrator')) {
+          this.mgrModuleService.getConfig('telemetry').subscribe((options) => {
+            if (!options['enabled']) {
+              this.telemetryNotificationService.setVisibility(true);
+            }
+          });
+        }
+      });
+    }
+  }
+
+  ngOnDestroy() {
+    this.telemetryNotificationService.setVisibility(false);
+  }
+
+  isNotificationHidden(): boolean {
+    return localStorage.getItem('telemetry_notification_hidden') === 'true';
+  }
+
+  close() {
+    this.telemetryNotificationService.setVisibility(false);
+    localStorage.setItem('telemetry_notification_hidden', 'true');
+    this.notificationService.show(
+      NotificationType.success,
+      this.i18n('Telemetry activation reminder muted'),
+      this.i18n(
+        'You can activate the module on the Telemetry configuration ' +
+          'page (<b>Dashboard Settings</b> -> <b>Telemetry configuration</b>) at any time.'
+      )
+    );
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.spec.ts
new file mode 100644 (file)
index 0000000..41d8060
--- /dev/null
@@ -0,0 +1,33 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '../../../testing/unit-test-helper';
+import { TelemetryNotificationService } from './telemetry-notification.service';
+
+describe('TelemetryNotificationService', () => {
+  let service: TelemetryNotificationService;
+
+  configureTestBed({
+    providers: [TelemetryNotificationService]
+  });
+
+  beforeEach(() => {
+    service = TestBed.inject(TelemetryNotificationService);
+    spyOn(service.update, 'emit');
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+
+  it('should set notification visibility to true', () => {
+    service.setVisibility(true);
+    expect(service.visible).toBe(true);
+    expect(service.update.emit).toHaveBeenCalledWith(true);
+  });
+
+  it('should set notification visibility to false', () => {
+    service.setVisibility(false);
+    expect(service.visible).toBe(false);
+    expect(service.update.emit).toHaveBeenCalledWith(false);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/telemetry-notification.service.ts
new file mode 100644 (file)
index 0000000..fcb2e02
--- /dev/null
@@ -0,0 +1,16 @@
+import { EventEmitter, Injectable, Output } from '@angular/core';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class TelemetryNotificationService {
+  visible = false;
+
+  @Output()
+  update: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+  setVisibility(visible: boolean) {
+    this.visible = visible;
+    this.update.emit(visible);
+  }
+}