-<ng-template #popTemplate>
+<ng-template #notificationsTpl>
<div *ngIf="notifications.length > 0">
- <div class="separator">
- <ng-container i18n>Recent Notifications</ng-container>
- <div *ngIf="notifications.length > 0"
- class="pull-right">
- <a (click)="removeAll()"
- i18n>Remove all</a>
- </div>
- </div>
+ <button type="button" class="btn btn-default btn-sm btn-block" (click)="removeAll()">
+ <i class="fa fa-trash-o" aria-hidden="true"></i>
+
+ <ng-container i18n>Remove all</ng-container>
+ </button>
<hr>
<div *ngFor="let notification of notifications">
<table>
<tr>
<td rowspan="3" class="icon-col text-center">
- <span [ngClass]="['fa-stack fa-2x', notification.textClass()]">
+ <span [ngClass]="['fa-stack fa-2x', notification.textClass]">
<i class="fa fa-circle fa-stack-2x"></i>
- <i [ngClass]="['fa fa-stack-1x fa-inverse', notification.iconClass()]"></i>
+ <i [ngClass]="['fa fa-stack-1x fa-inverse', notification.iconClass]"></i>
</span>
</td>
<td>
</td>
</tr>
<tr>
- <td>
- {{ notification.message }}
+ <td [innerHtml]="notification.message">
</td>
</tr>
<tr>
- <td>
- <small class="date">{{ notification.timestamp | cdDate }}</small>
- </td>
+ <td [innerHtml]="notificationService.renderTimeAndApplicationHtml(notification)"></td>
</tr>
</table>
<hr>
</div>
</div>
- <!-- Empty -->
+</ng-template>
+
+<ng-template #emptyTpl>
<div *ngIf="notifications.length === 0">
<div class="message">
There are no notifications.
</div>
</div>
</ng-template>
-<a [popover]="popTemplate"
+
+<ng-template #popTpl>
+ <ng-container *ngTemplateOutlet="notificationsTpl"></ng-container>
+ <ng-container *ngTemplateOutlet="emptyTpl"></ng-container>
+</ng-template>
+
+<a [popover]="popTpl"
placement="bottom"
container="body"
outsideClick="true"
import * as _ from 'lodash';
-import { NotificationType } from '../../../shared/enum/notification-type.enum';
import { CdNotification } from '../../../shared/models/cd-notification';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { NotificationService } from '../../../shared/services/notification.service';
private interval: number;
constructor(
- private notificationService: NotificationService,
+ public notificationService: NotificationService,
private prometheusNotificationService: PrometheusNotificationService,
private authStorageService: AuthStorageService,
private prometheusAlertService: PrometheusAlertService,
it('should test search manipulation', () => {
let searchTerms = [];
- spyOn(component, 'subSearch').and.callFake((d, search, c) => {
+ spyOn(component, 'subSearch').and.callFake((d, search) => {
expect(search).toEqual(searchTerms);
});
const searchTest = (s: string, st: string[]) => {
beforeEach(() => {
component.ngOnInit();
component.data = [];
- component.updating = false;
+ component['updating'] = false;
});
it('should call fetchData callback function', () => {
expect(component.loadingError).toBeTruthy();
expect(component.data.length).toBe(0);
expect(component.loadingIndicator).toBeFalsy();
- expect(component.updating).toBeFalsy();
+ expect(component['updating']).toBeFalsy();
});
component.reloadData();
});
expect(component.loadingError).toBeFalsy();
expect(component.data.length).toBe(10);
expect(component.loadingIndicator).toBeFalsy();
- expect(component.updating).toBeFalsy();
+ expect(component['updating']).toBeFalsy();
});
component.reloadData();
});
};
});
- const expectUseCustomClass = (values: any[], expectation: string) => {
- values.forEach((value) => expect(component.useCustomClass(value)).toBe(expectation));
- };
-
it('should throw an error if custom classes are not set', () => {
component.customCss = undefined;
expect(() => component.useCustomClass('active')).toThrowError('Custom classes are not set!');
});
it('should not return any class', () => {
- expectUseCustomClass(['', 'something', 123, { complex: 1 }, [1, 2, 3]], undefined);
+ ['', 'something', 123, { complex: 1 }, [1, 2, 3]].forEach((value) =>
+ expect(component.useCustomClass(value)).toBe(undefined)
+ );
});
it('should match a string and return the corresponding class', () => {
}
if (_.isInteger(this.autoReload) && this.autoReload > 0) {
this.ngZone.runOutsideAngular(() => {
- this.reloadSubscriber = observableTimer(0, this.autoReload).subscribe((x) => {
+ this.reloadSubscriber = observableTimer(0, this.autoReload).subscribe(() => {
this.ngZone.run(() => {
return this.reloadData();
});
.map((v, i) => ((_.isFunction(v) && v(value)) || v === value) && classes[i])
.filter((x) => x)
.join(' ');
- return (!_.isEmpty(css) && css) || undefined;
+ return _.isEmpty(css) ? undefined : css;
}
ngOnChanges(changes) {
--- /dev/null
+import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotification, CdNotificationConfig } from './cd-notification';
+
+describe('cd-notification classes', () => {
+ const expectObject = (something: object, expected: object) => {
+ Object.keys(expected).forEach((key) => expect(something[key]).toBe(expected[key]));
+ };
+
+ // As these Models have a view methods they need to be tested
+ describe('CdNotificationConfig', () => {
+ it('should create a new config without any parameters', () => {
+ expectObject(new CdNotificationConfig(), {
+ application: 'Ceph',
+ applicationClass: 'ceph-icon',
+ message: undefined,
+ options: undefined,
+ title: undefined,
+ type: 1
+ });
+ });
+
+ it('should create a new config with parameters', () => {
+ expectObject(
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'Some Alert',
+ 'Something failed',
+ undefined,
+ 'Prometheus'
+ ),
+ {
+ application: 'Prometheus',
+ applicationClass: 'prometheus-icon',
+ message: 'Something failed',
+ options: undefined,
+ title: 'Some Alert',
+ type: 0
+ }
+ );
+ });
+ });
+
+ describe('CdNotification', () => {
+ beforeEach(() => {
+ const baseTime = new Date('2022-02-22');
+ spyOn(global, 'Date').and.returnValue(baseTime);
+ });
+
+ it('should create a new config without any parameters', () => {
+ expectObject(new CdNotification(), {
+ application: 'Ceph',
+ applicationClass: 'ceph-icon',
+ iconClass: 'fa-info',
+ message: undefined,
+ options: undefined,
+ textClass: 'text-info',
+ timestamp: '2022-02-22T00:00:00.000Z',
+ title: undefined,
+ type: 1
+ });
+ });
+
+ it('should create a new config with parameters', () => {
+ expectObject(
+ new CdNotification(
+ new CdNotificationConfig(
+ NotificationType.error,
+ 'Some Alert',
+ 'Something failed',
+ undefined,
+ 'Prometheus'
+ )
+ ),
+ {
+ application: 'Prometheus',
+ applicationClass: 'prometheus-icon',
+ iconClass: 'fa-exclamation-triangle',
+ message: 'Something failed',
+ options: undefined,
+ textClass: 'text-danger',
+ timestamp: '2022-02-22T00:00:00.000Z',
+ title: 'Some Alert',
+ type: 0
+ }
+ );
+ });
+
+ it('should expect the right success classes', () => {
+ expectObject(new CdNotification(new CdNotificationConfig(NotificationType.success)), {
+ iconClass: 'fa-check',
+ textClass: 'text-success'
+ });
+ });
+ });
+});
import { NotificationType } from '../enum/notification-type.enum';
export class CdNotificationConfig {
- constructor(
- public type: NotificationType,
- public title: string,
- public message?: string, // Use this for error notifications only
- public options?: any | ToastOptions
- ) {}
-}
+ applicationClass: string;
-export class CdNotification {
- timestamp: string;
+ private classes = {
+ Ceph: 'ceph-icon',
+ Prometheus: 'prometheus-icon'
+ };
constructor(
public type: NotificationType = NotificationType.info,
public title?: string,
- public message?: string
+ public message?: string, // Use this for additional information only
+ public options?: any | ToastOptions,
+ public application: string = 'Ceph'
) {
- /* string representation of the Date object so it can be directly compared
- with the timestamps parsed from localStorage */
- this.timestamp = new Date().toJSON();
+ this.applicationClass = this.classes[this.application];
}
+}
- textClass() {
- switch (this.type) {
- case NotificationType.error:
- return 'text-danger';
- case NotificationType.info:
- return 'text-info';
- case NotificationType.success:
- return 'text-success';
- }
- }
+export class CdNotification extends CdNotificationConfig {
+ timestamp: string;
+ textClass: string;
+ iconClass: string;
+
+ private textClasses = ['text-danger', 'text-info', 'text-success'];
+ private iconClasses = ['fa-exclamation-triangle', 'fa-info', 'fa-check'];
- iconClass() {
- switch (this.type) {
- case NotificationType.error:
- return 'fa-exclamation-triangle';
- case NotificationType.info:
- return 'fa-info';
- case NotificationType.success:
- return 'fa-check';
- }
+ constructor(private config: CdNotificationConfig = new CdNotificationConfig()) {
+ super(config.type, config.title, config.message, config.options, config.application);
+ delete this.config;
+ /* string representation of the Date object so it can be directly compared
+ with the timestamps parsed from localStorage */
+ this.timestamp = new Date().toJSON();
+ this.iconClass = this.iconClasses[this.type];
+ this.textClass = this.textClasses[this.type];
}
}
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { TestBed } from '@angular/core/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { Router } from '@angular/router';
+import { ToastsManager } from 'ng2-toastr';
+
import { configureTestBed, i18nProviders } from '../../../testing/unit-test-helper';
import { AppModule } from '../../app.module';
+import { CdNotification, CdNotificationConfig } from '../models/cd-notification';
import { ApiInterceptorService } from './api-interceptor.service';
import { NotificationService } from './notification.service';
let router: Router;
const url = 'api/xyz';
- const runRouterTest = (errorOpts, expectedCallParams, done) => {
+ const httpError = (error, errorOpts, done = (resp) => {}) => {
httpClient.get(url).subscribe(
() => {},
(resp) => {
// Error must have been forwarded by the interceptor.
expect(resp instanceof HttpErrorResponse).toBeTruthy();
- done();
+ done(resp);
}
);
- httpTesting.expectOne(url).error(new ErrorEvent('abc'), errorOpts);
+ httpTesting.expectOne(url).error(error, errorOpts);
+ };
+
+ const runRouterTest = (errorOpts, expectedCallParams) => {
+ httpError(new ErrorEvent('abc'), errorOpts);
httpTesting.verify();
expect(router.navigate).toHaveBeenCalledWith(...expectedCallParams);
};
- const runNotificationTest = (error, errorOpts, expectedCallParams, done) => {
- httpClient.get(url).subscribe(
- () => {},
- (resp) => {
- // Error must have been forwarded by the interceptor.
- expect(resp instanceof HttpErrorResponse).toBeTruthy();
- done();
- }
- );
- httpTesting.expectOne(url).error(error, errorOpts);
+ const runNotificationTest = (error, errorOpts, expectedCallParams) => {
+ httpError(error, errorOpts);
httpTesting.verify();
- expect(notificationService.show).toHaveBeenCalledWith(...expectedCallParams);
+ expect(notificationService.show).toHaveBeenCalled();
+ expect(notificationService.save).toHaveBeenCalledWith(expectedCallParams);
+ };
+
+ const createCdNotification = (type, title?, message?, options?, application?) => {
+ return new CdNotification(new CdNotificationConfig(type, title, message, options, application));
};
configureTestBed({
imports: [AppModule, HttpClientTestingModule],
- providers: [NotificationService, i18nProviders]
+ providers: [
+ NotificationService,
+ i18nProviders,
+ {
+ provide: ToastsManager,
+ useValue: {
+ error: () => true
+ }
+ }
+ ]
});
beforeEach(() => {
+ const baseTime = new Date('2022-02-22');
+ spyOn(global, 'Date').and.returnValue(baseTime);
+
httpClient = TestBed.get(HttpClient);
httpTesting = TestBed.get(HttpTestingController);
+
notificationService = TestBed.get(NotificationService);
- spyOn(notificationService, 'show');
+ spyOn(notificationService, 'show').and.callThrough();
+ spyOn(notificationService, 'save');
+
router = TestBed.get(Router);
spyOn(router, 'navigate');
});
expect(service).toBeTruthy();
});
- it('should redirect 401', (done) => {
- runRouterTest(
- {
- status: 401
- },
- [['/login']],
- done
- );
- });
+ describe('test different error behaviours', () => {
+ beforeEach(() => {
+ spyOn(window, 'setTimeout').and.callFake((fn) => fn());
+ });
- it('should redirect 403', (done) => {
- runRouterTest(
- {
- status: 403
- },
- [['/403']],
- done
- );
- });
+ it('should redirect 401', () => {
+ runRouterTest(
+ {
+ status: 401
+ },
+ [['/login']]
+ );
+ });
- it('should show notification (error string)', function(done) {
- runNotificationTest(
- 'foobar',
- {
- status: 500,
- statusText: 'Foo Bar'
- },
- [0, '500 - Foo Bar', 'foobar'],
- done
- );
- });
+ it('should redirect 403', () => {
+ runRouterTest(
+ {
+ status: 403
+ },
+ [['/403']]
+ );
+ });
- it('should show notification (error object, triggered from backend)', function(done) {
- runNotificationTest(
- { detail: 'abc' },
- {
- status: 504,
- statusText: 'AAA bbb CCC'
- },
- [0, '504 - AAA bbb CCC', 'abc'],
- done
- );
- });
+ it('should show notification (error string)', () => {
+ runNotificationTest(
+ 'foobar',
+ {
+ status: 500,
+ statusText: 'Foo Bar'
+ },
+ createCdNotification(0, '500 - Foo Bar', 'foobar')
+ );
+ });
- it('should show notification (error object with unknown keys)', function(done) {
- runNotificationTest(
- { type: 'error' },
- {
- status: 0,
- statusText: 'Unknown Error',
- message: 'Http failure response for (unknown url): 0 Unknown Error',
- name: 'HttpErrorResponse',
- ok: false,
- url: null
- },
- [0, '0 - Unknown Error', 'Http failure response for api/xyz: 0 Unknown Error'],
- done
- );
- });
+ it('should show notification (error object, triggered from backend)', () => {
+ runNotificationTest(
+ { detail: 'abc' },
+ {
+ status: 504,
+ statusText: 'AAA bbb CCC'
+ },
+ createCdNotification(0, '504 - AAA bbb CCC', 'abc')
+ );
+ });
- it('should show notification (undefined error)', function(done) {
- runNotificationTest(
- undefined,
- {
- status: 502
- },
- [0, '502 - Unknown Error', 'Http failure response for api/xyz: 502 '],
- done
- );
- });
+ it('should show notification (error object with unknown keys)', () => {
+ runNotificationTest(
+ { type: 'error' },
+ {
+ status: 0,
+ statusText: 'Unknown Error',
+ message: 'Http failure response for (unknown url): 0 Unknown Error',
+ name: 'HttpErrorResponse',
+ ok: false,
+ url: null
+ },
+ createCdNotification(
+ 0,
+ '0 - Unknown Error',
+ 'Http failure response for api/xyz: 0 Unknown Error'
+ )
+ );
+ });
- it('should show 400 notification', function(done) {
- spyOn(notificationService, 'notifyTask');
- httpClient.get(url).subscribe(
- () => {},
- (resp) => {
- // Error must have been forwarded by the interceptor.
- expect(resp instanceof HttpErrorResponse).toBeTruthy();
- done();
- }
- );
- httpTesting
- .expectOne(url)
- .error({ task: { name: 'mytask', metadata: { component: 'foobar' } } }, { status: 400 });
- httpTesting.verify();
- expect(notificationService.show).toHaveBeenCalledTimes(0);
- expect(notificationService.notifyTask).toHaveBeenCalledWith({
- exception: { task: { metadata: { component: 'foobar' }, name: 'mytask' } },
- metadata: { component: 'foobar' },
- name: 'mytask',
- success: false
+ it('should show notification (undefined error)', () => {
+ runNotificationTest(
+ undefined,
+ {
+ status: 502
+ },
+ createCdNotification(0, '502 - Unknown Error', 'Http failure response for api/xyz: 502 ')
+ );
});
+
+ it('should show 400 notification', () => {
+ spyOn(notificationService, 'notifyTask');
+ httpError({ task: { name: 'mytask', metadata: { component: 'foobar' } } }, { status: 400 });
+ httpTesting.verify();
+ expect(notificationService.show).toHaveBeenCalledTimes(0);
+ expect(notificationService.notifyTask).toHaveBeenCalledWith({
+ exception: { task: { metadata: { component: 'foobar' }, name: 'mytask' } },
+ metadata: { component: 'foobar' },
+ name: 'mytask',
+ success: false
+ });
+ });
+ });
+
+ describe('interceptor error handling', () => {
+ it('should show default behaviour', fakeAsync(() => {
+ httpError(undefined, { status: 500 });
+ tick(10);
+ expect(notificationService.save).toHaveBeenCalled();
+ }));
+
+ it('should prevent the default behaviour with preventDefault', fakeAsync(() => {
+ httpError(undefined, { status: 500 }, (resp) => resp.preventDefault());
+ tick(10);
+ expect(notificationService.save).not.toHaveBeenCalled();
+ }));
+
+ it('should prevent the default behaviour by status code', fakeAsync(() => {
+ httpError(undefined, { status: 500 }, (resp) => resp.ignoreStatusCode(500));
+ tick(10);
+ expect(notificationService.save).not.toHaveBeenCalled();
+ }));
+
+ it('should use different application icon (default Ceph) in error message', fakeAsync(() => {
+ const msg = 'Cannot connect to Alertmanager';
+ httpError(undefined, { status: 500 }, (resp) => {
+ (resp.application = 'Prometheus'), (resp.message = msg);
+ });
+ tick(10);
+ expect(notificationService.save).toHaveBeenCalledWith(
+ createCdNotification(0, '500 - Unknown Error', msg, undefined, 'Prometheus')
+ );
+ }));
});
});
import { catchError } from 'rxjs/operators';
import { NotificationType } from '../enum/notification-type.enum';
+import { CdNotificationConfig } from '../models/cd-notification';
import { FinishedTask } from '../models/finished-task';
import { AuthStorageService } from './auth-storage.service';
import { NotificationService } from './notification.service';
break;
}
- let timeoutId;
- if (showNotification) {
- let message = '';
- if (_.isPlainObject(resp.error) && _.isString(resp.error.detail)) {
- message = resp.error.detail; // Error was triggered by the backend.
- } else if (_.isString(resp.error)) {
- message = resp.error;
- } else if (_.isString(resp.message)) {
- message = resp.message;
- }
- timeoutId = this.notificationService.show(
- NotificationType.error,
- `${resp.status} - ${resp.statusText}`,
- message
- );
- }
+ const timeoutId = showNotification ? this.prepareNotification(resp) : undefined;
/**
* Decorated preventDefault method (in case error previously had
})
);
}
+
+ private prepareNotification(resp): number {
+ return this.notificationService.show(() => {
+ let message = '';
+ if (_.isPlainObject(resp.error) && _.isString(resp.error.detail)) {
+ message = resp.error.detail; // Error was triggered by the backend.
+ } else if (_.isString(resp.error)) {
+ message = resp.error;
+ } else if (_.isString(resp.message)) {
+ message = resp.message;
+ }
+ return new CdNotificationConfig(
+ NotificationType.error,
+ `${resp.status} - ${resp.statusText}`,
+ message,
+ undefined,
+ resp['application']
+ );
+ });
+ }
}
+import { DatePipe } from '@angular/common';
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import * as _ from 'lodash';
import { NotificationType } from '../enum/notification-type.enum';
import { CdNotificationConfig } from '../models/cd-notification';
import { FinishedTask } from '../models/finished-task';
+import { CdDatePipe } from '../pipes/cd-date.pipe';
import { NotificationService } from './notification.service';
import { TaskMessageService } from './task-message.service';
describe('NotificationService', () => {
- let notificationService: NotificationService;
+ let service: NotificationService;
const toastFakeService = {
error: () => true,
info: () => true,
configureTestBed({
providers: [
+ CdDatePipe,
+ DatePipe,
NotificationService,
TaskMessageService,
{ provide: ToastsManager, useValue: toastFakeService },
+ { provide: CdDatePipe, useValue: { transform: (d) => d } },
i18nProviders
]
});
beforeEach(() => {
- notificationService = TestBed.get(NotificationService);
- notificationService.removeAll();
+ service = TestBed.get(NotificationService);
+ service.removeAll();
});
it('should be created', () => {
- expect(notificationService).toBeTruthy();
+ expect(service).toBeTruthy();
});
it('should read empty notification list', () => {
localStorage.setItem('cdNotifications', '[]');
- expect(notificationService['dataSource'].getValue()).toEqual([]);
+ expect(service['dataSource'].getValue()).toEqual([]);
});
it('should read old notifications', fakeAsync(() => {
'cdNotifications',
'[{"type":2,"message":"foobar","timestamp":"2018-05-24T09:41:32.726Z"}]'
);
- const service = new NotificationService(null, null);
+ service = new NotificationService(null, null, null);
expect(service['dataSource'].getValue().length).toBe(1);
}));
it('should cancel a notification', fakeAsync(() => {
- const timeoutId = notificationService.show(NotificationType.error, 'Simple test');
- notificationService.cancel(timeoutId);
+ const timeoutId = service.show(NotificationType.error, 'Simple test');
+ service.cancel(timeoutId);
tick(5000);
- expect(notificationService['dataSource'].getValue().length).toBe(0);
+ expect(service['dataSource'].getValue().length).toBe(0);
}));
it('should create a success notification and save it', fakeAsync(() => {
- notificationService.show(new CdNotificationConfig(NotificationType.success, 'Simple test'));
+ service.show(new CdNotificationConfig(NotificationType.success, 'Simple test'));
tick(100);
- expect(notificationService['dataSource'].getValue().length).toBe(1);
- expect(notificationService['dataSource'].getValue()[0].type).toBe(NotificationType.success);
+ expect(service['dataSource'].getValue().length).toBe(1);
+ expect(service['dataSource'].getValue()[0].type).toBe(NotificationType.success);
}));
it('should create an error notification and save it', fakeAsync(() => {
- notificationService.show(NotificationType.error, 'Simple test');
+ service.show(NotificationType.error, 'Simple test');
tick(100);
- expect(notificationService['dataSource'].getValue().length).toBe(1);
- expect(notificationService['dataSource'].getValue()[0].type).toBe(NotificationType.error);
+ expect(service['dataSource'].getValue().length).toBe(1);
+ expect(service['dataSource'].getValue()[0].type).toBe(NotificationType.error);
}));
it('should create an info notification and save it', fakeAsync(() => {
- notificationService.show(new CdNotificationConfig(NotificationType.info, 'Simple test'));
+ service.show(new CdNotificationConfig(NotificationType.info, 'Simple test'));
tick(100);
- expect(notificationService['dataSource'].getValue().length).toBe(1);
- const notification = notificationService['dataSource'].getValue()[0];
+ expect(service['dataSource'].getValue().length).toBe(1);
+ const notification = service['dataSource'].getValue()[0];
expect(notification.type).toBe(NotificationType.info);
expect(notification.title).toBe('Simple test');
expect(notification.message).toBe(undefined);
it('should never have more then 10 notifications', fakeAsync(() => {
for (let index = 0; index < 15; index++) {
- notificationService.show(NotificationType.info, 'Simple test');
+ service.show(NotificationType.info, 'Simple test');
tick(100);
}
- expect(notificationService['dataSource'].getValue().length).toBe(10);
+ expect(service['dataSource'].getValue().length).toBe(10);
}));
it('should show a success task notification', fakeAsync(() => {
const task = _.assign(new FinishedTask(), {
success: true
});
- notificationService.notifyTask(task, true);
+ service.notifyTask(task, true);
tick(100);
- expect(notificationService['dataSource'].getValue().length).toBe(1);
- const notification = notificationService['dataSource'].getValue()[0];
+ expect(service['dataSource'].getValue().length).toBe(1);
+ const notification = service['dataSource'].getValue()[0];
expect(notification.type).toBe(NotificationType.success);
expect(notification.title).toBe('Executed unknown task');
expect(notification.message).toBe(undefined);
}
}
);
- notificationService.notifyTask(task);
+ service.notifyTask(task);
tick(100);
- expect(notificationService['dataSource'].getValue().length).toBe(1);
- const notification = notificationService['dataSource'].getValue()[0];
+ expect(service['dataSource'].getValue().length).toBe(1);
+ const notification = service['dataSource'].getValue()[0];
expect(notification.type).toBe(NotificationType.error);
expect(notification.title).toBe(`Failed to create RBD 'somePool/someImage'`);
expect(notification.message).toBe(`Name is already used by RBD 'somePool/someImage'.`);
const n2 = new CdNotificationConfig(NotificationType.info, 'Some info');
beforeEach(() => {
- spyOn(notificationService, 'show').and.stub();
+ spyOn(service, 'show').and.stub();
});
it('filters out duplicated notifications on single call', fakeAsync(() => {
- notificationService.queueNotifications([n1, n1, n2, n2]);
+ service.queueNotifications([n1, n1, n2, n2]);
tick(500);
- expect(notificationService.show).toHaveBeenCalledTimes(2);
+ expect(service.show).toHaveBeenCalledTimes(2);
}));
it('filters out duplicated notifications presented in different calls', fakeAsync(() => {
- notificationService.queueNotifications([n1, n2]);
- notificationService.queueNotifications([n1, n2]);
+ service.queueNotifications([n1, n2]);
+ service.queueNotifications([n1, n2]);
tick(500);
- expect(notificationService.show).toHaveBeenCalledTimes(2);
+ expect(service.show).toHaveBeenCalledTimes(2);
}));
it('will reset the timeout on every call', fakeAsync(() => {
- notificationService.queueNotifications([n1, n2]);
+ service.queueNotifications([n1, n2]);
tick(400);
- notificationService.queueNotifications([n1, n2]);
+ service.queueNotifications([n1, n2]);
tick(100);
- expect(notificationService.show).toHaveBeenCalledTimes(0);
+ expect(service.show).toHaveBeenCalledTimes(0);
tick(400);
- expect(notificationService.show).toHaveBeenCalledTimes(2);
+ expect(service.show).toHaveBeenCalledTimes(2);
}));
it('wont filter out duplicated notifications if timeout was reached before', fakeAsync(() => {
- notificationService.queueNotifications([n1, n2]);
+ service.queueNotifications([n1, n2]);
tick(500);
- notificationService.queueNotifications([n1, n2]);
+ service.queueNotifications([n1, n2]);
tick(500);
- expect(notificationService.show).toHaveBeenCalledTimes(4);
+ expect(service.show).toHaveBeenCalledTimes(4);
}));
});
+
+ describe('showToasty', () => {
+ let toastr: ToastsManager;
+ const time = '2022-02-22T00:00:00.000Z';
+
+ beforeEach(() => {
+ const baseTime = new Date(time);
+ spyOn(global, 'Date').and.returnValue(baseTime);
+ spyOn(window, 'setTimeout').and.callFake((fn) => fn());
+
+ toastr = TestBed.get(ToastsManager);
+ // spyOn needs to know the methods before spying and can't read the array for clarification
+ ['error', 'info', 'success'].forEach((method: 'error' | 'info' | 'success') =>
+ spyOn(toastr, method).and.stub()
+ );
+ });
+
+ it('should show with only title defined', () => {
+ service.show(NotificationType.info, 'Some info');
+ expect(toastr.info).toHaveBeenCalledWith(
+ `<small class="date">${time}</small>` +
+ '<i class="pull-right custom-icon ceph-icon" title="Ceph"></i>',
+ 'Some info',
+ undefined
+ );
+ });
+
+ it('should show with title and message defined', () => {
+ service.show(
+ () =>
+ new CdNotificationConfig(NotificationType.error, 'Some error', 'Some operation failed')
+ );
+ expect(toastr.error).toHaveBeenCalledWith(
+ 'Some operation failed<br>' +
+ `<small class="date">${time}</small>` +
+ '<i class="pull-right custom-icon ceph-icon" title="Ceph"></i>',
+ 'Some error',
+ undefined
+ );
+ });
+
+ it('should show with title, message and application defined', () => {
+ service.show(
+ new CdNotificationConfig(
+ NotificationType.success,
+ 'Alert resolved',
+ 'Some alert resolved',
+ undefined,
+ 'Prometheus'
+ )
+ );
+ expect(toastr.success).toHaveBeenCalledWith(
+ 'Some alert resolved<br>' +
+ `<small class="date">${time}</small>` +
+ '<i class="pull-right custom-icon prometheus-icon" title="Prometheus"></i>',
+ 'Alert resolved',
+ undefined
+ );
+ });
+ });
});
import { Injectable } from '@angular/core';
import * as _ from 'lodash';
-import { ToastsManager } from 'ng2-toastr';
+import { ToastOptions, ToastsManager } from 'ng2-toastr';
import { BehaviorSubject } from 'rxjs';
import { NotificationType } from '../enum/notification-type.enum';
import { CdNotification, CdNotificationConfig } from '../models/cd-notification';
import { FinishedTask } from '../models/finished-task';
+import { CdDatePipe } from '../pipes/cd-date.pipe';
import { ServicesModule } from './services.module';
import { TaskMessageService } from './task-message.service';
private queueTimeoutId: number;
KEY = 'cdNotifications';
- constructor(public toastr: ToastsManager, private taskMessageService: TaskMessageService) {
+ constructor(
+ public toastr: ToastsManager,
+ private taskMessageService: TaskMessageService,
+ private cdDatePipe: CdDatePipe
+ ) {
const stringNotifications = localStorage.getItem(this.KEY);
let notifications: CdNotification[] = [];
/**
* Method used for saving a shown notification (check show() method).
- * @param {Notification} notification
*/
- save(type: NotificationType, title: string, message?: string) {
- const notification = new CdNotification(type, title, message);
-
+ save(notification: CdNotification) {
const recent = this.dataSource.getValue();
recent.push(notification);
while (recent.length > 10) {
recent.shift();
}
-
this.dataSource.next(recent);
localStorage.setItem(this.KEY, JSON.stringify(recent));
}
* @param {string} [message] The message to be displayed. Note, use this field
* for error notifications only.
* @param {*} [options] toastr compatible options, used when creating a toastr
+ * @param {string} [application] Only needed if notification comes from an external application
* @returns The timeout ID that is set to be able to cancel the notification.
*/
- show(type: NotificationType, title: string, message?: string, options?: any): number;
- show(config: CdNotificationConfig): number;
show(
- arg: NotificationType | CdNotificationConfig,
+ type: NotificationType,
+ title: string,
+ message?: string,
+ options?: any | ToastOptions,
+ application?: string
+ ): number;
+ show(config: CdNotificationConfig | (() => CdNotificationConfig)): number;
+ show(
+ arg: NotificationType | CdNotificationConfig | (() => CdNotificationConfig),
title?: string,
message?: string,
- options?: any
+ options?: any | ToastOptions,
+ application?: string
): number {
- let type;
- if (_.isObject(arg)) {
- ({ message, type, title, options } = <CdNotificationConfig>arg);
- } else {
- type = arg;
- }
return window.setTimeout(() => {
- this.save(type, title, message);
- if (!message) {
- message = '';
- }
- switch (type) {
- case NotificationType.error:
- this.toastr.error(message, title, options);
- break;
- case NotificationType.info:
- this.toastr.info(message, title, options);
- break;
- case NotificationType.success:
- this.toastr.success(message, title, options);
- break;
+ let config: CdNotificationConfig;
+ if (_.isFunction(arg)) {
+ config = arg() as CdNotificationConfig;
+ } else if (_.isObject(arg)) {
+ config = arg as CdNotificationConfig;
+ } else {
+ config = new CdNotificationConfig(
+ arg as NotificationType,
+ title,
+ message,
+ options,
+ application
+ );
}
+ const notification = new CdNotification(config);
+ this.save(notification);
+ this.showToasty(notification);
}, 10);
}
+ private showToasty(notification: CdNotification) {
+ this.toastr[['error', 'info', 'success'][notification.type]](
+ (notification.message ? notification.message + '<br>' : '') +
+ this.renderTimeAndApplicationHtml(notification),
+ notification.title,
+ notification.options
+ );
+ }
+
+ renderTimeAndApplicationHtml(notification: CdNotification): string {
+ return `<small class="date">${this.cdDatePipe.transform(
+ notification.timestamp
+ )}</small><i class="pull-right custom-icon ${notification.applicationClass}" title="${
+ notification.application
+ }"></i>`;
+ }
+
notifyTask(finishedTask: FinishedTask, success: boolean = true) {
let notification: CdNotificationConfig;
if (finishedTask.success && success) {
.nav-tabs {
margin-bottom: 15px;
}
+/* Icons */
+.ceph-icon {
+ background: url('assets/Ceph_Logo_Stacked_RGB_120411_fa_228x228.png');
+}
+.prometheus-icon {
+ background: url('assets/prometheus_logo.png');
+}
+.custom-icon {
+ padding: 10px;
+ margin-right: 1em;
+ background-clip: padding-box;
+ background-size: contain;
+ background-repeat: no-repeat;
+}