diff --git a/metron-interface/metron-alerts/cypress/integration/search/auto-polling.feature.spec.js b/metron-interface/metron-alerts/cypress/integration/search/auto-polling.feature.spec.js index c99f700bc5..fd901333c6 100644 --- a/metron-interface/metron-alerts/cypress/integration/search/auto-polling.feature.spec.js +++ b/metron-interface/metron-alerts/cypress/integration/search/auto-polling.feature.spec.js @@ -29,6 +29,18 @@ describe('Automatic data polling on Alerts View', () => { cy.route('GET', '/api/v1/global/config', 'fixture:config.json'); cy.route('GET', appConfigJSON.contextMenuConfigURL, 'fixture:context-menu.conf.json'); + + cy.route({ + url: '/api/v1/hdfs?path=user-settings', + method: 'GET', + response: {}, + }); + + cy.route({ + url: '/api/v1/hdfs?path=user-settings', + method: 'POST', + response: {}, + }); }; beforeEach(() => { diff --git a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.spec.ts b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.spec.ts index 9b0487a8bc..f2edf53dc6 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.spec.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.spec.ts @@ -28,8 +28,6 @@ import { SearchService } from 'app/service/search.service'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { AppConfigService } from 'app/service/app-config.service'; import { GlobalConfigService } from 'app/service/global-config.service'; -import { DataSource } from 'app/service/data-source'; -import { ElasticSearchLocalstorageImpl } from 'app/service/elasticsearch-localstorage-impl'; import { DialogService } from 'app/service/dialog.service'; import { By } from '@angular/platform-browser'; import { AlertComment } from './alert-comment'; @@ -39,6 +37,7 @@ import { CommentAddRemoveRequest } from '../../model/comment-add-remove-request' import { AlertSource } from '../../model/alert-source'; import { of } from 'rxjs/index'; import { Router } from '@angular/router'; +import { UserSettingsService } from 'app/service/user-settings.service'; const alertDetail = { 'enrichments:geo:ip_dst_addr:locID': '5308655', @@ -126,10 +125,7 @@ describe('AlertDetailsComponent', () => { appConfigStatic: {}, getApiRoot: () => {}, } }, - { - provide: DataSource, - useClass: ElasticSearchLocalstorageImpl - }, + UserSettingsService, { provide: AlertsService, useValue: { escalate: () => {} diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/auto-polling/auto-polling.service.spec.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/auto-polling/auto-polling.service.spec.ts index 029d0e47e0..217e5f1336 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/auto-polling/auto-polling.service.spec.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/auto-polling/auto-polling.service.spec.ts @@ -27,6 +27,9 @@ import { DialogService } from 'app/service/dialog.service'; import { RestError } from 'app/model/rest-error'; import { DialogType } from 'app/model/dialog-type'; import { RefreshInterval } from 'app/alerts/configure-rows/configure-rows-enums'; +import { UserSettingsService } from 'app/service/user-settings.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { AppConfigService } from 'app/service/app-config.service'; const DEFAULT_POLLING_INTERVAL = RefreshInterval.TEN_MIN; @@ -63,11 +66,18 @@ describe('AutoPollingService', () => { } beforeEach(() => { - localStorage.getItem = () => null; - localStorage.setItem = () => {}; - TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], providers: [ + { + provide: AppConfigService, + useValue: { + getApiRoot() { return ''; } + } + }, + UserSettingsService, AutoPollingService, { provide: DialogService, useClass: () => {} }, { provide: SearchService, useClass: () => { return { @@ -447,35 +457,27 @@ describe('AutoPollingService', () => { describe('polling state persisting and restoring', () => { - it('should persist polling state on start', () => { - spyOn(localStorage, 'setItem'); - autoPollingService.start(); - expect(localStorage.setItem).toHaveBeenCalledWith('autoPolling', `{"isActive":true,"refreshInterval":${DEFAULT_POLLING_INTERVAL}}`); - }); - - it('should persist polling state on stop', () => { - spyOn(localStorage, 'setItem'); - autoPollingService.stop(); - expect(localStorage.setItem).toHaveBeenCalledWith('autoPolling', `{"isActive":false,"refreshInterval":${DEFAULT_POLLING_INTERVAL}}`); - }); - it('should persist polling state on interval change', () => { - spyOn(localStorage, 'setItem'); + const userSettingsService = TestBed.get(UserSettingsService); + spyOn(userSettingsService, 'save').and.callThrough(); autoPollingService.setInterval(4); - expect(localStorage.setItem).toHaveBeenCalledWith('autoPolling', '{"isActive":false,"refreshInterval":4}'); + expect(userSettingsService.save).toHaveBeenCalledWith({ + autoPolling: '{"isActive":false,"refreshInterval":4}' + }); }); it('should restore polling state on construction with a delay', fakeAsync(() => { const queryBuilderFake = TestBed.get(QueryBuilder); const dialogServiceFake = TestBed.get(DialogService); + const userSettingsService = TestBed.get(UserSettingsService); - spyOn(localStorage, 'getItem').and.returnValue('{"isActive":true,"refreshInterval":443}'); + spyOn(userSettingsService, 'get').and.returnValue(of('{"isActive":true,"refreshInterval":443}')); - const localAutoPollingSvc = new AutoPollingService(searchServiceFake, queryBuilderFake, dialogServiceFake); + const localAutoPollingSvc = new AutoPollingService(searchServiceFake, queryBuilderFake, dialogServiceFake, userSettingsService); tick(localAutoPollingSvc.AUTO_START_DELAY); - expect(localStorage.getItem).toHaveBeenCalledWith('autoPolling'); + expect(userSettingsService.get).toHaveBeenCalledWith('autoPolling'); expect(localAutoPollingSvc.getIsPollingActive()).toBe(true); expect(localAutoPollingSvc.getInterval()).toBe(443); @@ -485,11 +487,12 @@ describe('AutoPollingService', () => { it('should start polling on construction when persisted isActive==true', fakeAsync(() => { const queryBuilderFake = TestBed.get(QueryBuilder); const dialogServiceFake = TestBed.get(DialogService); + const userSettingsService = TestBed.get(UserSettingsService); spyOn(searchServiceFake, 'search').and.callThrough(); - spyOn(localStorage, 'getItem').and.returnValue('{"isActive":true,"refreshInterval":10}'); + spyOn(userSettingsService, 'get').and.returnValue(of('{"isActive":true,"refreshInterval":10}')); - const localAutoPollingSvc = new AutoPollingService(searchServiceFake, queryBuilderFake, dialogServiceFake); + const localAutoPollingSvc = new AutoPollingService(searchServiceFake, queryBuilderFake, dialogServiceFake, userSettingsService); tick(localAutoPollingSvc.AUTO_START_DELAY); @@ -507,11 +510,12 @@ describe('AutoPollingService', () => { it('should start polling on construction with the persisted interval', fakeAsync(() => { const queryBuilderFake = TestBed.get(QueryBuilder); const dialogServiceFake = TestBed.get(DialogService); + const userSettingsService = TestBed.get(UserSettingsService); spyOn(searchServiceFake, 'search').and.callThrough(); - spyOn(localStorage, 'getItem').and.returnValue('{"isActive":true,"refreshInterval":4}'); + spyOn(userSettingsService, 'get').and.returnValue(of('{"isActive":true,"refreshInterval":4}')); - const localAutoPollingSvc = new AutoPollingService(searchServiceFake, queryBuilderFake, dialogServiceFake); + const localAutoPollingSvc = new AutoPollingService(searchServiceFake, queryBuilderFake, dialogServiceFake, userSettingsService); tick(localAutoPollingSvc.AUTO_START_DELAY); @@ -526,5 +530,4 @@ describe('AutoPollingService', () => { localAutoPollingSvc.stop(); })); }); - }); diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/auto-polling/auto-polling.service.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/auto-polling/auto-polling.service.ts index 23cd572714..1bf076a9fe 100755 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/auto-polling/auto-polling.service.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/auto-polling/auto-polling.service.ts @@ -16,7 +16,7 @@ * limitations under the License. */ import { Injectable } from '@angular/core'; -import { Subscription, Subject, Observable, interval, onErrorResumeNext, timer } from 'rxjs'; +import { Subscription, Subject, Observable, interval, timer } from 'rxjs'; import { SearchService } from 'app/service/search.service'; import { QueryBuilder } from '../query-builder'; import { SearchResponse } from 'app/model/search-response'; @@ -26,6 +26,7 @@ import { RestError } from 'app/model/rest-error'; import { DialogType } from 'app/shared/metron-dialog/metron-dialog.component'; import { DialogService } from 'app/service/dialog.service'; import { RefreshInterval } from '../../configure-rows/configure-rows-enums'; +import { UserSettingsService } from 'app/service/user-settings.service'; interface AutoPollingStateModel { isActive: boolean, @@ -49,6 +50,7 @@ export class AutoPollingService { constructor(private searchService: SearchService, private queryBuilder: QueryBuilder, private dialogService: DialogService, + private userSettingsService: UserSettingsService ) { this.restoreState(); } @@ -108,19 +110,24 @@ export class AutoPollingService { } private persistState(key = this.AUTO_POLLING_STORAGE_KEY): void { - localStorage.setItem(key, JSON.stringify(this.getStateModel())); + this.userSettingsService.save({ + [key]: JSON.stringify(this.getStateModel()) + }).subscribe(); } private restoreState(key = this.AUTO_POLLING_STORAGE_KEY): void { - const persistedState = JSON.parse(localStorage.getItem(key)) as AutoPollingStateModel; - - if (persistedState) { - this.refreshInterval = persistedState.refreshInterval; - - if (persistedState.isActive) { - timer(this.AUTO_START_DELAY).subscribe(this.start.bind(this)); - } - } + this.userSettingsService.get(key) + .subscribe((autoPollingState) => { + const persistedState = autoPollingState ? JSON.parse(autoPollingState) as AutoPollingStateModel : null; + + if (persistedState) { + this.refreshInterval = persistedState.refreshInterval; + + if (persistedState.isActive) { + timer(this.AUTO_START_DELAY).subscribe(this.start.bind(this)); + } + } + }); } private getStateModel(): AutoPollingStateModel { diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.spec.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.spec.ts index 604359b990..a13c11d22e 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.spec.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.spec.ts @@ -34,6 +34,7 @@ import { DialogService } from 'app/service/dialog.service'; import { AppConfigService } from '../../../service/app-config.service'; import { ContextMenuComponent } from 'app/shared/context-menu/context-menu.component'; import { of } from 'rxjs'; +import { UserSettingsService } from 'app/service/user-settings.service'; @Component({selector: 'metron-table-pagination', template: ''}) class MetronTablePaginationComponent { @@ -63,7 +64,8 @@ describe('TableViewComponent', () => { { provide: AppConfigService, useClass: FakeAppConfigService }, { provide: GlobalConfigService, useValue: { get: () => { return of({})} - }} + }}, + UserSettingsService ], declarations: [ MetronTableDirective, diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.spec.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.spec.ts index 0d33eba96d..c48dc27ba3 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.spec.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.spec.ts @@ -34,6 +34,7 @@ import { MetaAlertService } from '../../../service/meta-alert.service'; import { DialogService } from 'app/service/dialog.service'; import { AppConfigService } from '../../../service/app-config.service'; import { of } from 'rxjs'; +import { UserSettingsService } from 'app/service/user-settings.service'; class FakeAppConfigService { @@ -57,7 +58,8 @@ describe('TreeViewComponent', () => { { provide: AppConfigService, useClass: FakeAppConfigService }, { provide: GlobalConfigService, useValue: { get: () => { return of({})} - }} + }}, + UserSettingsService ], declarations: [ MetronTableDirective, diff --git a/metron-interface/metron-alerts/src/app/alerts/configure-rows/configure-rows.module.ts b/metron-interface/metron-alerts/src/app/alerts/configure-rows/configure-rows.module.ts index 9915007bcb..7f7b5b7d35 100644 --- a/metron-interface/metron-alerts/src/app/alerts/configure-rows/configure-rows.module.ts +++ b/metron-interface/metron-alerts/src/app/alerts/configure-rows/configure-rows.module.ts @@ -24,15 +24,13 @@ import { QueryBuilder } from '../alerts-list/query-builder'; import { ShowHideService } from './show-hide/show-hide.service'; import { TimezoneConfigComponent } from './timezone-config/timezone-config.component'; import { TimezoneConfigService } from './timezone-config/timezone-config.service'; +import { AppConfigService } from 'app/service/app-config.service'; +import { UserSettingsService } from 'app/service/user-settings.service'; @NgModule({ imports: [ SharedModule, SwitchModule ], declarations: [ ConfigureRowsComponent, ShowHideAlertEntriesComponent, TimezoneConfigComponent ], exports: [ ConfigureRowsComponent ], - providers: [ QueryBuilder, ShowHideService, TimezoneConfigService, ], + providers: [ AppConfigService, UserSettingsService, QueryBuilder, ShowHideService, TimezoneConfigService, ], }) -export class ConfigureRowsModule { - - constructor(private showHideService: ShowHideService, private timezoneConfigService: TimezoneConfigService) {} - -} +export class ConfigureRowsModule {} diff --git a/metron-interface/metron-alerts/src/app/alerts/configure-rows/show-hide/show-hide.service.spec.ts b/metron-interface/metron-alerts/src/app/alerts/configure-rows/show-hide/show-hide.service.spec.ts index e2c99d44e2..61f63010e3 100644 --- a/metron-interface/metron-alerts/src/app/alerts/configure-rows/show-hide/show-hide.service.spec.ts +++ b/metron-interface/metron-alerts/src/app/alerts/configure-rows/show-hide/show-hide.service.spec.ts @@ -22,7 +22,9 @@ import { QueryBuilder, FilteringMode } from 'app/alerts/alerts-list/query-builde import { Spy } from 'jasmine-core'; import { Filter } from 'app/model/filter'; -import { serializePath } from '@angular/router/src/url_tree'; +import { UserSettingsService } from 'app/service/user-settings.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { AppConfigService } from 'app/service/app-config.service'; class QueryBuilderMock { addOrUpdateFilter = () => {}; @@ -32,21 +34,31 @@ class QueryBuilderMock { describe('ShowHideService', () => { let queryBuilderMock: QueryBuilderMock; + let userSettingsService: UserSettingsService; beforeEach(() => { - spyOn(localStorage, 'getItem').and.returnValues('true', 'false'); - spyOn(localStorage, 'setItem'); spyOn(ShowHideService.prototype, 'setFilterFor').and.callThrough(); + spyOn(UserSettingsService.prototype, 'get').and.callThrough(); + spyOn(UserSettingsService.prototype, 'save').and.callThrough(); TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule ], providers: [ + { + provide: AppConfigService, + useValue: { + getApiRoot() { return ''; } + } + }, + UserSettingsService, ShowHideService, { provide: QueryBuilder, useClass: QueryBuilderMock }, ] }); queryBuilderMock = getTestBed().get(QueryBuilder); + userSettingsService = getTestBed().get(UserSettingsService); }); it('should be created', inject([ShowHideService], (service: ShowHideService) => { @@ -57,36 +69,38 @@ describe('ShowHideService', () => { expect(service.queryBuilder).toBeTruthy(); })); - it('should get persisted state from localStorage', inject([ShowHideService], (service: ShowHideService) => { - expect(localStorage.getItem).toHaveBeenCalledWith(service.HIDE_RESOLVE_STORAGE_KEY); - expect(localStorage.getItem).toHaveBeenCalledWith(service.HIDE_DISMISS_STORAGE_KEY); + it('should get persisted state', inject([ShowHideService], (service: ShowHideService) => { + expect(userSettingsService.get).toHaveBeenCalledWith(service.HIDE_RESOLVE_STORAGE_KEY); + expect(userSettingsService.get).toHaveBeenCalledWith(service.HIDE_DISMISS_STORAGE_KEY); })); it('should set initial filter state', inject([ShowHideService], (service: ShowHideService) => { - expect((service.setFilterFor as Spy).calls.argsFor(0)[1]).toBe(true); - expect((service.setFilterFor as Spy).calls.argsFor(0)[0]).toBe('RESOLVE'); - expect((service.setFilterFor as Spy).calls.argsFor(1)[0]).toBe('DISMISS'); - expect((service.setFilterFor as Spy).calls.argsFor(1)[1]).toBe(false); + expect((service.setFilterFor as Spy).calls.argsFor(0)).toEqual(['RESOLVE', false]); + expect((service.setFilterFor as Spy).calls.argsFor(1)).toEqual(['DISMISS', false]); })); - it('should set value loaded from localStorage to hideDismissed ', inject([ShowHideService], (service: ShowHideService) => { + it('should set value to hideDismissed ', inject([ShowHideService], (service: ShowHideService) => { expect(service.hideDismissed).toBe(false); })); - it('should set value loaded from localStorage to hideResolved', inject([ShowHideService], (service: ShowHideService) => { - expect(service.hideResolved).toBe(true); + it('should set value to hideResolved', inject([ShowHideService], (service: ShowHideService) => { + expect(service.hideResolved).toBe(false); })); - it('should save state to localStorage on change for RESOLVE', inject([ShowHideService], (service: ShowHideService) => { + it('should save state on change for RESOLVE', inject([ShowHideService], (service: ShowHideService) => { service.setFilterFor('RESOLVE', true); - expect(localStorage.setItem).toHaveBeenCalledWith(service.HIDE_RESOLVE_STORAGE_KEY, true); + expect(userSettingsService.save).toHaveBeenCalledWith({ + [service.HIDE_RESOLVE_STORAGE_KEY]: true + }); })); - it('should save state to localStorage on change for DISMISS', inject([ShowHideService], (service: ShowHideService) => { + it('should save state for DISMISS', inject([ShowHideService], (service: ShowHideService) => { service.setFilterFor('DISMISS', true); - expect(localStorage.setItem).toHaveBeenCalledWith(service.HIDE_DISMISS_STORAGE_KEY, true); + expect(userSettingsService.save).toHaveBeenCalledWith({ + [service.HIDE_DISMISS_STORAGE_KEY]: true + }); })); it('should be able to add RESOLVE filter to QueryBuilder', inject([ShowHideService], (service: ShowHideService) => { diff --git a/metron-interface/metron-alerts/src/app/alerts/configure-rows/show-hide/show-hide.service.ts b/metron-interface/metron-alerts/src/app/alerts/configure-rows/show-hide/show-hide.service.ts index f5244d1e90..e3c2d0c1c0 100644 --- a/metron-interface/metron-alerts/src/app/alerts/configure-rows/show-hide/show-hide.service.ts +++ b/metron-interface/metron-alerts/src/app/alerts/configure-rows/show-hide/show-hide.service.ts @@ -18,6 +18,7 @@ import { Injectable } from '@angular/core'; import { QueryBuilder, FilteringMode } from 'app/alerts/alerts-list/query-builder'; import { Filter } from 'app/model/filter'; +import { UserSettingsService } from 'app/service/user-settings.service'; @Injectable({ providedIn: 'root' @@ -37,12 +38,18 @@ export class ShowHideService { hideResolved = false; hideDismissed = false; - constructor(public queryBuilder: QueryBuilder) { - this.hideResolved = localStorage.getItem(this.HIDE_RESOLVE_STORAGE_KEY) === 'true'; - this.setFilterFor(this.RESOLVE, this.hideResolved); + constructor(public queryBuilder: QueryBuilder, private userSettingsService: UserSettingsService) { + this.userSettingsService.get(this.HIDE_RESOLVE_STORAGE_KEY) + .subscribe((hideResolved) => { + this.hideResolved = !!hideResolved; + this.setFilterFor(this.RESOLVE, this.hideResolved); + }); - this.hideDismissed = localStorage.getItem(this.HIDE_DISMISS_STORAGE_KEY) === 'true'; - this.setFilterFor(this.DISMISS, this.hideDismissed); + this.userSettingsService.get(this.HIDE_DISMISS_STORAGE_KEY) + .subscribe((hideDismissed) => { + this.hideDismissed = !!hideDismissed; + this.setFilterFor(this.DISMISS, this.hideDismissed); + }); } setFilterFor(alertStatus, isHide) { @@ -58,12 +65,16 @@ export class ShowHideService { case this.DISMISS: filterOperation(this.dismissFilter); this.hideDismissed = isHide; - localStorage.setItem(this.HIDE_DISMISS_STORAGE_KEY, isHide); + this.userSettingsService.save({ + [this.HIDE_DISMISS_STORAGE_KEY]: isHide + }).subscribe(); break; case this.RESOLVE: filterOperation(this.resolveFilter); this.hideResolved = isHide; - localStorage.setItem(this.HIDE_RESOLVE_STORAGE_KEY, isHide); + this.userSettingsService.save({ + [this.HIDE_RESOLVE_STORAGE_KEY]: isHide + }).subscribe(); break; } } diff --git a/metron-interface/metron-alerts/src/app/alerts/configure-rows/timezone-config/timezone-config.service.spec.ts b/metron-interface/metron-alerts/src/app/alerts/configure-rows/timezone-config/timezone-config.service.spec.ts index 758336b189..7f1f0d1eae 100644 --- a/metron-interface/metron-alerts/src/app/alerts/configure-rows/timezone-config/timezone-config.service.spec.ts +++ b/metron-interface/metron-alerts/src/app/alerts/configure-rows/timezone-config/timezone-config.service.spec.ts @@ -19,37 +19,49 @@ import { TestBed } from '@angular/core/testing'; import { TimezoneConfigService } from './timezone-config.service'; import { SwitchComponent } from 'app/shared/switch/switch.component'; +import { UserSettingsService } from 'app/service/user-settings.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { AppConfigService } from 'app/service/app-config.service'; describe('TimezoneConfigService', () => { let service: TimezoneConfigService; + let userSettingsService: UserSettingsService; beforeEach(() => { - spyOn(localStorage, 'getItem').and.returnValues('true'); TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule ], declarations: [ SwitchComponent ], - providers: [ TimezoneConfigService ] + providers: [ + { + provide: AppConfigService, + useValue: { + getApiRoot() { return ''; } + } + }, + UserSettingsService, + TimezoneConfigService + ] }); spyOn(TimezoneConfigService.prototype, 'toggleUTCtoLocal').and.callThrough(); + spyOn(UserSettingsService.prototype, 'get').and.callThrough(); + service = TestBed.get(TimezoneConfigService); + userSettingsService = TestBed.get(UserSettingsService); }); it('should be created', () => { expect(service).toBeTruthy(); }); - it('should get persisted state from localStorage', () => { - expect(localStorage.getItem).toHaveBeenCalledWith(service.CONVERT_UTC_TO_LOCAL_KEY); - }); - - it('should set initial switch state', () => { - expect(service.toggleUTCtoLocal).toHaveBeenCalledWith(true); + it('should get persisted state', () => { + expect(userSettingsService.get).toHaveBeenCalledWith(service.CONVERT_UTC_TO_LOCAL_KEY); }); it('should return the current timezone configuration with getTimezoneConfig()', () => { - expect(service.getTimezoneConfig()).toBe(true); - service.showLocal = false; expect(service.getTimezoneConfig()).toBe(false); + service.showLocal = true; + expect(service.getTimezoneConfig()).toBe(true); }); }); diff --git a/metron-interface/metron-alerts/src/app/alerts/configure-rows/timezone-config/timezone-config.service.ts b/metron-interface/metron-alerts/src/app/alerts/configure-rows/timezone-config/timezone-config.service.ts index db736f1520..c56b077164 100644 --- a/metron-interface/metron-alerts/src/app/alerts/configure-rows/timezone-config/timezone-config.service.ts +++ b/metron-interface/metron-alerts/src/app/alerts/configure-rows/timezone-config/timezone-config.service.ts @@ -16,6 +16,7 @@ * limitations under the License. */ import { Injectable } from '@angular/core'; +import { UserSettingsService } from 'app/service/user-settings.service'; @Injectable({ providedIn: 'root' @@ -26,14 +27,18 @@ export class TimezoneConfigService { showLocal = false; - constructor() { - this.showLocal = localStorage.getItem(this.CONVERT_UTC_TO_LOCAL_KEY) === 'true'; - this.toggleUTCtoLocal(this.showLocal); + constructor(private userSettingsService: UserSettingsService) { + this.userSettingsService.get(this.CONVERT_UTC_TO_LOCAL_KEY) + .subscribe((showLocal) => { + this.showLocal = !!showLocal; + }); } toggleUTCtoLocal(isLocal: boolean) { this.showLocal = isLocal; - localStorage.setItem(this.CONVERT_UTC_TO_LOCAL_KEY, isLocal.toString()); + this.userSettingsService.save({ + [this.CONVERT_UTC_TO_LOCAL_KEY]: isLocal + }).subscribe(); } getTimezoneConfig() { diff --git a/metron-interface/metron-alerts/src/app/alerts/saved-searches/saved-searches.component.html b/metron-interface/metron-alerts/src/app/alerts/saved-searches/saved-searches.component.html index 71fb1dbde6..0cdeb1f58e 100644 --- a/metron-interface/metron-alerts/src/app/alerts/saved-searches/saved-searches.component.html +++ b/metron-interface/metron-alerts/src/app/alerts/saved-searches/saved-searches.component.html @@ -22,7 +22,7 @@
+ (onSelect)="onSaveRecentSearchSelect($event)">
diff --git a/metron-interface/metron-alerts/src/app/alerts/saved-searches/saved-searches.component.ts b/metron-interface/metron-alerts/src/app/alerts/saved-searches/saved-searches.component.ts index ab182c0527..a9d5c5615b 100644 --- a/metron-interface/metron-alerts/src/app/alerts/saved-searches/saved-searches.component.ts +++ b/metron-interface/metron-alerts/src/app/alerts/saved-searches/saved-searches.component.ts @@ -43,17 +43,8 @@ export class SavedSearchesComponent implements OnInit { private dialogService: DialogService) { } - doDeleteRecentSearch(selectedSearch: SaveSearch) { - this.saveSearchService.deleteRecentSearch(selectedSearch).subscribe(() => { - this.ngOnInit(); - }, - error => { - this.ngOnInit(); - }); - } doDeleteSearch(selectedSearch: SaveSearch) { this.saveSearchService.deleteSavedSearch(selectedSearch).subscribe(() => { - this.doDeleteRecentSearch(selectedSearch); this.ngOnInit(); }, error => { @@ -61,22 +52,6 @@ export class SavedSearchesComponent implements OnInit { }); } - deleteRecentSearch($event) { - let selectedSearch = this.recentSearcheObj.find( - savedSearch => savedSearch.name === $event.key - ); - const confirmedSubscription = this.dialogService - .launchDialog( - 'Do you wish to delete recent search ' + selectedSearch.name - ) - .subscribe(action => { - if (action === ConfirmationType.Confirmed) { - this.doDeleteRecentSearch(selectedSearch); - } - confirmedSubscription.unsubscribe(); - }); - } - deleteSearch($event) { let selectedSearch = this.searches.find( savedSearch => savedSearch.name === $event.key diff --git a/metron-interface/metron-alerts/src/app/app-routing.module.ts b/metron-interface/metron-alerts/src/app/app-routing.module.ts index f899a15839..de344a4d69 100644 --- a/metron-interface/metron-alerts/src/app/app-routing.module.ts +++ b/metron-interface/metron-alerts/src/app/app-routing.module.ts @@ -15,19 +15,55 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; -import {AuthGuard} from './shared/auth-guard'; -import {LoginGuard} from './shared/login-guard'; +import { NgModule, Injectable } from '@angular/core'; +import { Routes, RouterModule, Resolve } from '@angular/router'; +import { AuthGuard } from './shared/auth-guard'; +import { LoginGuard } from './shared/login-guard'; +import { UserSettingsService } from './service/user-settings.service'; + +@Injectable() +export class UserSettingsResolver implements Resolve { + constructor(private userSettingsService: UserSettingsService) {} + resolve(): Promise { + return this.userSettingsService.load(); + } +} const routes: Routes = [ { path: '', redirectTo: 'alerts-list', pathMatch: 'full'}, { path: 'login', loadChildren: 'app/login/login.module#LoginModule', canActivate: [LoginGuard]}, - { path: 'alerts-list', loadChildren: 'app/alerts/alerts-list/alerts-list.module#AlertsListModule', canActivate: [AuthGuard]}, - { path: 'save-search', loadChildren: 'app/alerts/save-search/save-search.module#SaveSearchModule', canActivate: [AuthGuard]}, - { path: 'saved-searches', loadChildren: 'app/alerts/saved-searches/saved-searches.module#SavedSearchesModule', - canActivate: [AuthGuard]}, - { path: 'pcap', loadChildren: 'app/pcap/pcap.module#PcapModule', canActivate: [AuthGuard] } + { + path: 'alerts-list', + loadChildren: 'app/alerts/alerts-list/alerts-list.module#AlertsListModule', + canActivate: [AuthGuard], + resolve: { + userSettings: UserSettingsResolver + } + }, + { + path: 'save-search', + loadChildren: 'app/alerts/save-search/save-search.module#SaveSearchModule', + canActivate: [AuthGuard], + resolve: { + userSettings: UserSettingsResolver + } + }, + { + path: 'saved-searches', + loadChildren: 'app/alerts/saved-searches/saved-searches.module#SavedSearchesModule', + canActivate: [AuthGuard], + resolve: { + userSettings: UserSettingsResolver + } + }, + { + path: 'pcap', + loadChildren: 'app/pcap/pcap.module#PcapModule', + canActivate: [AuthGuard], + resolve: { + userSettings: UserSettingsResolver + } + } ]; @NgModule({ diff --git a/metron-interface/metron-alerts/src/app/app.module.ts b/metron-interface/metron-alerts/src/app/app.module.ts index efba0e34e2..e606966d32 100644 --- a/metron-interface/metron-alerts/src/app/app.module.ts +++ b/metron-interface/metron-alerts/src/app/app.module.ts @@ -19,29 +19,27 @@ import { Router } from '@angular/router'; import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { APP_INITIALIZER } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; -import {MetronAlertsRoutingModule} from './app-routing.module'; -import {AlertsListModule} from './alerts/alerts-list/alerts-list.module'; -import {AlertDetailsModule} from './alerts/alert-details/alerts-details.module'; -import {ConfigureTableModule} from './alerts/configure-table/configure-table.module'; -import {ConfigureTableService} from './service/configure-table.service'; -import {SaveSearchModule} from './alerts/save-search/save-search.module'; -import {SaveSearchService} from './service/save-search.service'; -import {SavedSearchesModule} from './alerts/saved-searches/saved-searches.module'; -import {ConfigureRowsModule} from './alerts/configure-rows/configure-rows.module'; -import {ColumnNamesService} from './service/column-names.service'; -import {DataSource} from './service/data-source'; -import {ElasticSearchLocalstorageImpl} from './service/elasticsearch-localstorage-impl'; -import {LoginModule} from './login/login.module'; -import {AuthGuard} from './shared/auth-guard'; -import {AuthenticationService} from './service/authentication.service'; -import {LoginGuard} from './shared/login-guard'; -import {UpdateService} from './service/update.service'; -import {MetaAlertService} from './service/meta-alert.service'; +import { MetronAlertsRoutingModule, UserSettingsResolver } from './app-routing.module'; +import { AlertsListModule } from './alerts/alerts-list/alerts-list.module'; +import { AlertDetailsModule } from './alerts/alert-details/alerts-details.module'; +import { ConfigureTableModule } from './alerts/configure-table/configure-table.module'; +import { ConfigureTableService } from './service/configure-table.service'; +import { SaveSearchModule } from './alerts/save-search/save-search.module'; +import { SaveSearchService } from './service/save-search.service'; +import { SavedSearchesModule } from './alerts/saved-searches/saved-searches.module'; +import { ConfigureRowsModule } from './alerts/configure-rows/configure-rows.module'; +import { ColumnNamesService } from './service/column-names.service'; +import { LoginModule } from './login/login.module'; +import { AuthGuard } from './shared/auth-guard'; +import { AuthenticationService } from './service/authentication.service'; +import { LoginGuard } from './shared/login-guard'; +import { UpdateService } from './service/update.service'; +import { MetaAlertService } from './service/meta-alert.service'; import { MetaAlertsModule } from './alerts/meta-alerts/meta-alerts.module'; import { SearchService } from './service/search.service'; import { GlobalConfigService } from './service/global-config.service'; @@ -94,7 +92,6 @@ const icons: IconDefinition[] = [ ToolOutline, WarningOutline, FileOutline ]; NgZorroAntdModule, ], providers: [{ provide: APP_INITIALIZER, useFactory: initConfig, deps: [AppConfigService], multi: true }, - { provide: DataSource, useClass: ElasticSearchLocalstorageImpl }, { provide: HTTP_INTERCEPTORS, useClass: DefaultHeadersInterceptor, multi: true }, { provide: NZ_ICONS, useValue: icons }, AppConfigService, @@ -109,6 +106,7 @@ const icons: IconDefinition[] = [ ToolOutline, WarningOutline, FileOutline ]; MetaAlertService, GlobalConfigService, DialogService, + UserSettingsResolver ], bootstrap: [AppComponent] }) diff --git a/metron-interface/metron-alerts/src/app/service/authentication.service.ts b/metron-interface/metron-alerts/src/app/service/authentication.service.ts index f54dee2709..9a3327b48c 100644 --- a/metron-interface/metron-alerts/src/app/service/authentication.service.ts +++ b/metron-interface/metron-alerts/src/app/service/authentication.service.ts @@ -18,12 +18,11 @@ */ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import {Router} from '@angular/router'; +import { Router } from '@angular/router'; import { ReplaySubject } from 'rxjs'; import { GlobalConfigService } from './global-config.service'; -import { DataSource } from './data-source'; import { AppConfigService } from './app-config.service'; -import {HttpUtil} from "../utils/httpUtil"; +import { HttpUtil } from '../utils/httpUtil'; @Injectable() export class AuthenticationService { @@ -38,7 +37,6 @@ export class AuthenticationService { constructor(private http: HttpClient, private router: Router, private globalConfigService: GlobalConfigService, - private dataSource: DataSource, private appConfigService: AppConfigService) { this.init(); } @@ -48,22 +46,23 @@ export class AuthenticationService { this.currentUser = response.toString(); if (this.currentUser) { this.onLoginEvent.next(true); - this.dataSource.getDefaultAlertTableColumnNames(); } - }, error => { + }, () => { this.onLoginEvent.next(false); }); } public login(username: string, password: string, onError): void { let credentials = btoa(username + ':' + password); - this.getCurrentUser({ headers: new HttpHeaders({'Authorization': `Basic ${credentials}`, 'Accept': 'text/plain'}), responseType: 'text' }) + this.getCurrentUser({ + headers: new HttpHeaders({'Authorization': `Basic ${credentials}`, 'Accept': 'text/plain'}), + responseType: 'text' + }) .subscribe((response) => { this.currentUser = response.toString(); this.router.navigateByUrl('/alerts-list'); this.onLoginEvent.next(true); this.globalConfigService.get(); - this.dataSource.getDefaultAlertTableColumnNames(); }, error => { onError(error); diff --git a/metron-interface/metron-alerts/src/app/service/cluster-metadata.service.ts b/metron-interface/metron-alerts/src/app/service/cluster-metadata.service.ts index 2a857007c3..d5726d32f4 100644 --- a/metron-interface/metron-alerts/src/app/service/cluster-metadata.service.ts +++ b/metron-interface/metron-alerts/src/app/service/cluster-metadata.service.ts @@ -15,18 +15,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs'; +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; -import {ColumnMetadata} from '../model/column-metadata'; -import {DataSource} from './data-source'; +import { ColumnMetadata } from '../model/column-metadata'; +import { UserSettingsService } from './user-settings.service'; @Injectable() export class ClusterMetaDataService { - constructor(private dataSource: DataSource) {} + private defaultColumnMetadata = [ + new ColumnMetadata('guid', 'string'), + new ColumnMetadata('timestamp', 'date'), + new ColumnMetadata('source:type', 'string'), + new ColumnMetadata('ip_src_addr', 'ip'), + new ColumnMetadata('enrichments:geo:ip_dst_addr:country', 'string'), + new ColumnMetadata('ip_dst_addr', 'ip'), + new ColumnMetadata('host', 'string'), + new ColumnMetadata('alert_status', 'string') + ]; + + constructor( + private userSettingsService: UserSettingsService + ) {} getDefaultColumns(): Observable { - return this.dataSource.getDefaultAlertTableColumnNames(); + return of(JSON.parse(JSON.stringify(this.defaultColumnMetadata))); } } diff --git a/metron-interface/metron-alerts/src/app/service/column-names.service.ts b/metron-interface/metron-alerts/src/app/service/column-names.service.ts index afdcd1cbdb..9589d6fcf5 100644 --- a/metron-interface/metron-alerts/src/app/service/column-names.service.ts +++ b/metron-interface/metron-alerts/src/app/service/column-names.service.ts @@ -15,11 +15,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; -import {ColumnNames} from '../model/column-names'; -import {DataSource} from './data-source'; +import { ColumnNames } from '../model/column-names'; +import { UserSettingsService } from './user-settings.service'; +import { ALERTS_COLUMN_NAMES } from '../utils/constants'; @Injectable() export class ColumnNamesService { @@ -57,9 +58,16 @@ export class ColumnNamesService { }); } - constructor(private dataSource: DataSource) {} + constructor(private userSettingsService: UserSettingsService) {} save(columns: ColumnNames[]): Observable<{}> { - return this.dataSource.saveAlertTableColumnNames(columns); + return new Observable((observer) => { + this.userSettingsService.save({ + [ALERTS_COLUMN_NAMES]: columns + }).subscribe(() => { + observer.next({}); + observer.complete(); + }); + }); } } diff --git a/metron-interface/metron-alerts/src/app/service/configure-table.service.ts b/metron-interface/metron-alerts/src/app/service/configure-table.service.ts index a9309eaf08..c3a1b56e53 100644 --- a/metron-interface/metron-alerts/src/app/service/configure-table.service.ts +++ b/metron-interface/metron-alerts/src/app/service/configure-table.service.ts @@ -15,12 +15,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; import { Subject } from 'rxjs'; -import {ColumnMetadata} from '../model/column-metadata'; -import {TableMetadata} from '../model/table-metadata'; -import {DataSource} from './data-source'; +import { ColumnMetadata } from '../model/column-metadata'; +import { TableMetadata } from '../model/table-metadata'; +import { UserSettingsService } from './user-settings.service'; +import { ALERTS_TABLE_METADATA } from '../utils/constants'; + @Injectable() export class ConfigureTableService { @@ -28,21 +30,49 @@ export class ConfigureTableService { private tableChangedSource = new Subject(); tableChanged$ = this.tableChangedSource.asObservable(); - constructor(private dataSource: DataSource) {} + constructor( + private userSettingsService: UserSettingsService + ) {} fireTableChanged() { this.tableChangedSource.next('table changed'); } - getTableMetadata(): Observable { - return this.dataSource.getAlertTableSettings(); + getTableMetadata(): Observable<{}> { + return new Observable((observer) => { + this.userSettingsService.get(ALERTS_TABLE_METADATA) + .subscribe((tableMetadata) => { + tableMetadata = TableMetadata.fromJSON(tableMetadata); + observer.next(tableMetadata); + observer.complete(); + }); + }); } saveColumnMetaData(columns: ColumnMetadata[]): Observable<{}> { - return this.dataSource.saveColumnMetaDataInAlertTableSettings(columns); + return new Observable((observer) => { + this.userSettingsService.get(ALERTS_TABLE_METADATA) + .subscribe((tableMetadata) => { + tableMetadata = TableMetadata.fromJSON(tableMetadata); + tableMetadata.tableColumns = columns; + this.userSettingsService.save({ + [ALERTS_TABLE_METADATA]: tableMetadata + }).subscribe(() => { + observer.next({}); + observer.complete(); + }); + }); + }); } - saveTableMetaData(tableMetadata: TableMetadata): Observable { - return this.dataSource.saveAlertTableSettings(tableMetadata); + saveTableMetaData(tableMetadata): Observable<{}> { + return new Observable((observer) => { + this.userSettingsService.save({ + [ALERTS_TABLE_METADATA]: tableMetadata + }).subscribe(() => { + observer.next({}); + observer.complete(); + }); + }); } } diff --git a/metron-interface/metron-alerts/src/app/service/data-source.ts b/metron-interface/metron-alerts/src/app/service/data-source.ts deleted file mode 100644 index 70423ebcad..0000000000 --- a/metron-interface/metron-alerts/src/app/service/data-source.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import {Observable} from 'rxjs'; -import {Injectable} from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import {ColumnMetadata} from '../model/column-metadata'; -import {ColumnNames} from '../model/column-names'; -import {TableMetadata} from '../model/table-metadata'; -import {SaveSearch} from '../model/save-search'; - -@Injectable() -export abstract class DataSource { - - constructor(protected http: HttpClient) {} - - // Calls to fetch default alert table column names and all the field names across all indexes - abstract getDefaultAlertTableColumnNames(): Observable - - // Calls to rename field names and to fetch the renamed field names - abstract getAlertTableColumnNames(): Observable - abstract saveAlertTableColumnNames(columns: ColumnNames[]): Observable<{}> - - // Calls to fetch and save alerts table settings like refresh interval, page size, default selected table column names - abstract getAlertTableSettings(): Observable - abstract saveColumnMetaDataInAlertTableSettings(columns: ColumnMetadata[]): Observable<{}> - abstract saveAlertTableSettings(tableMetadata): Observable - - // Calls to save search, last 10 searches, saved searches - abstract deleteRecentSearch(saveSearch: SaveSearch): Observable<{}> - abstract deleteSavedSearch(saveSearch: SaveSearch): Observable<{}> - abstract listRecentSearches(): Observable - abstract listSavedSearches(): Observable - abstract saveRecentSearch(saveSearch: SaveSearch): Observable<{}> - abstract saveSearch(saveSearch: SaveSearch): Observable<{}> - abstract updateSearch(saveSearch: SaveSearch): Observable<{}> -} diff --git a/metron-interface/metron-alerts/src/app/service/elasticsearch-localstorage-impl.ts b/metron-interface/metron-alerts/src/app/service/elasticsearch-localstorage-impl.ts deleted file mode 100644 index 5544e755ab..0000000000 --- a/metron-interface/metron-alerts/src/app/service/elasticsearch-localstorage-impl.ts +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import {Observable} from 'rxjs'; -import {throwError as observableThrowError} from 'rxjs'; -import {catchError, map, onErrorResumeNext} from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import {HttpUtil} from '../utils/httpUtil'; -import {DataSource} from './data-source'; -import {ColumnMetadata} from '../model/column-metadata'; -import {ElasticsearchUtils} from '../utils/elasticsearch-utils'; -import { - ALERTS_COLUMN_NAMES, ALERTS_TABLE_METADATA, ALERTS_RECENT_SEARCH, - ALERTS_SAVED_SEARCH, NUM_SAVED_SEARCH -} from '../utils/constants'; -import {ColumnNames} from '../model/column-names'; -import {ColumnNamesService} from './column-names.service'; -import {TableMetadata} from '../model/table-metadata'; -import {SaveSearch} from '../model/save-search'; -import {SearchResponse} from '../model/search-response'; -import {SearchRequest} from '../model/search-request'; -import {AlertSource} from '../model/alert-source'; -import { RestError } from '../model/rest-error'; - -@Injectable() -export class ElasticSearchLocalstorageImpl extends DataSource { - - globalConfig: {} = {}; - sourceType: 'source:type'; - - private defaultColumnMetadata = [ - new ColumnMetadata('guid', 'string'), - new ColumnMetadata('timestamp', 'date'), - new ColumnMetadata('source:type', 'string'), - new ColumnMetadata('ip_src_addr', 'ip'), - new ColumnMetadata('enrichments:geo:ip_dst_addr:country', 'string'), - new ColumnMetadata('ip_dst_addr', 'ip'), - new ColumnMetadata('host', 'string'), - new ColumnMetadata('alert_status', 'string') - ]; - - getAlerts(searchRequest: SearchRequest): Observable { - let url = '/search/*' + ElasticsearchUtils.excludeIndexName + '/_search'; - let request: any = JSON.parse(JSON.stringify(searchRequest)); - request.query = { query_string: { query: searchRequest.query } }; - - return this.http.post(url, request).pipe( - map(HttpUtil.extractData), - map(ElasticsearchUtils.extractAlertsData), - catchError(HttpUtil.handleError), - onErrorResumeNext()); - } - - getAlert(sourceType: string, alertId: string): Observable { - return observableThrowError('Method not implemented in ElasticSearchLocalstorageImpl'); - } - - updateAlertState(request: any): Observable<{}> { - return this.http.post('/search/_bulk', request).pipe( - map(HttpUtil.extractData), - catchError(HttpUtil.handleError)); - } - - getDefaultAlertTableColumnNames(): Observable { - return Observable.create(observer => { - observer.next(JSON.parse(JSON.stringify(this.defaultColumnMetadata))); - observer.complete(); - }); - } - - getAllFieldNames(): Observable { - let url = '_cluster/state'; - return this.http.get(url).pipe( - map(HttpUtil.extractData), - map(ElasticsearchUtils.extractColumnNameData), - catchError(HttpUtil.handleError)); - } - - getAlertTableColumnNames(): Observable { - return Observable.create(observer => { - let columnNames: ColumnNames[]; - try { - columnNames = JSON.parse(localStorage.getItem(ALERTS_COLUMN_NAMES)); - ColumnNamesService.toMap(columnNames); - } catch (e) {} - - columnNames = columnNames || []; - - observer.next(columnNames); - observer.complete(); - - }); - } - - saveAlertTableColumnNames(columns: ColumnNames[]): Observable<{}> { - return Observable.create(observer => { - try { - localStorage.setItem(ALERTS_COLUMN_NAMES, JSON.stringify(columns)); - } catch (e) {} - ColumnNamesService.toMap(columns); - observer.next({}); - observer.complete(); - - }); - } - - getAlertTableSettings(): Observable { - return Observable.create(observer => { - let tableMetadata: TableMetadata; - try { - tableMetadata = TableMetadata.fromJSON(JSON.parse(localStorage.getItem(ALERTS_TABLE_METADATA))); - } catch (e) {} - - observer.next(tableMetadata); - observer.complete(); - - }); - } - - saveColumnMetaDataInAlertTableSettings(columns: ColumnMetadata[]): Observable<{}> { - return Observable.create(observer => { - try { - let tableMetadata = TableMetadata.fromJSON(JSON.parse(localStorage.getItem(ALERTS_TABLE_METADATA))); - tableMetadata.tableColumns = columns; - localStorage.setItem(ALERTS_TABLE_METADATA, JSON.stringify(tableMetadata)); - } catch (e) {} - - observer.next({}); - observer.complete(); - - }); - } - - saveAlertTableSettings(tableMetadata): Observable { - return Observable.create(observer => { - try { - localStorage.setItem(ALERTS_TABLE_METADATA, JSON.stringify(tableMetadata)); - } catch (e) {} - - observer.next({}); - observer.complete(); - - }); - } - - deleteRecentSearch(saveSearch: SaveSearch): Observable<{}> { - return Observable.create(observer => { - let recentSearches: SaveSearch[] = []; - try { - recentSearches = JSON.parse(localStorage.getItem(ALERTS_RECENT_SEARCH)); - recentSearches = recentSearches.filter(search => search.name !== saveSearch.name); - } catch (e) {} - - localStorage.setItem(ALERTS_RECENT_SEARCH, JSON.stringify(recentSearches)); - - observer.next({}); - observer.complete(); - - }); - } - - deleteSavedSearch(saveSearch: SaveSearch): Observable<{}> { - return Observable.create(observer => { - let savedSearches: SaveSearch[] = []; - try { - savedSearches = JSON.parse(localStorage.getItem(ALERTS_SAVED_SEARCH)); - savedSearches = savedSearches.filter(search => search.name !== saveSearch.name); - } catch (e) {} - - localStorage.setItem(ALERTS_SAVED_SEARCH, JSON.stringify(savedSearches)); - - observer.next({}); - observer.complete(); - - }); - } - - listRecentSearches(): Observable { - return Observable.create(observer => { - let savedSearches: SaveSearch[] = []; - try { - savedSearches = JSON.parse(localStorage.getItem(ALERTS_RECENT_SEARCH)); - } catch (e) {} - - savedSearches = savedSearches || []; - savedSearches = savedSearches.map(tSaveSeacrh => SaveSearch.fromJSON(tSaveSeacrh)); - - observer.next(savedSearches); - observer.complete(); - - }); - } - - listSavedSearches(): Observable { - return Observable.create(observer => { - let savedSearches: SaveSearch[] = []; - try { - savedSearches = JSON.parse(localStorage.getItem(ALERTS_SAVED_SEARCH)); - } catch (e) {} - - savedSearches = savedSearches || []; - savedSearches = savedSearches.map(tSaveSeacrh => SaveSearch.fromJSON(tSaveSeacrh)); - - observer.next(savedSearches); - observer.complete(); - - }); - } - - saveRecentSearch(saveSearch: SaveSearch): Observable<{}> { - return Observable.create(observer => { - let savedSearches: SaveSearch[] = []; - saveSearch.lastAccessed = new Date().getTime(); - - try { - savedSearches = JSON.parse(localStorage.getItem(ALERTS_RECENT_SEARCH)); - } catch (e) {} - - savedSearches = savedSearches || []; - savedSearches = savedSearches.map(tSaveSeacrh => SaveSearch.fromJSON(tSaveSeacrh)); - - if (savedSearches.length === 0) { - savedSearches.push(saveSearch); - } else { - let found = false; - for ( let tSaveSearch of savedSearches) { - if (saveSearch.name === tSaveSearch.name) { - tSaveSearch.lastAccessed = new Date().getTime(); - found = true; - break; - } - } - if (!found ) { - if (savedSearches.length < NUM_SAVED_SEARCH) { - savedSearches.push(saveSearch); - } else { - savedSearches.sort((s1, s2) => s1.lastAccessed - s2.lastAccessed).shift(); - savedSearches.push(saveSearch); - } - } - } - - localStorage.setItem(ALERTS_RECENT_SEARCH, JSON.stringify(savedSearches)); - - observer.next({}); - observer.complete(); - - }); - } - - saveSearch(saveSearch: SaveSearch): Observable<{}> { - return Observable.create(observer => { - let savedSearches: SaveSearch[] = []; - try { - savedSearches = JSON.parse(localStorage.getItem(ALERTS_SAVED_SEARCH)); - } catch (e) {} - - savedSearches = savedSearches || []; - savedSearches.push(saveSearch); - localStorage.setItem(ALERTS_SAVED_SEARCH, JSON.stringify(savedSearches)); - - observer.next({}); - observer.complete(); - - }); - } - - updateSearch(saveSearch: SaveSearch): Observable<{}> { - return Observable.create(observer => { - let savedSearches: SaveSearch[] = []; - try { - savedSearches = JSON.parse(localStorage.getItem(ALERTS_SAVED_SEARCH)); - let savedItem = savedSearches.find(search => search.name === saveSearch.name); - savedItem.lastAccessed = saveSearch.lastAccessed; - savedItem.searchRequest = saveSearch.searchRequest; - } catch (e) {} - - localStorage.setItem(ALERTS_SAVED_SEARCH, JSON.stringify(savedSearches)); - - observer.next({}); - observer.complete(); - - }); - } -} diff --git a/metron-interface/metron-alerts/src/app/service/save-search.service.ts b/metron-interface/metron-alerts/src/app/service/save-search.service.ts index 9fa189189c..84c130461a 100644 --- a/metron-interface/metron-alerts/src/app/service/save-search.service.ts +++ b/metron-interface/metron-alerts/src/app/service/save-search.service.ts @@ -15,13 +15,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Injectable, } from '@angular/core'; -import {Observable} from 'rxjs'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; import { Subject } from 'rxjs'; -import {QueryBuilder} from '../alerts/alerts-list/query-builder'; -import {SaveSearch} from '../model/save-search'; -import {ColumnMetadata} from '../model/column-metadata'; -import {DataSource} from './data-source'; +import { QueryBuilder } from '../alerts/alerts-list/query-builder'; +import { SaveSearch } from '../model/save-search'; +import { ColumnMetadata } from '../model/column-metadata'; +import { UserSettingsService } from './user-settings.service'; +import { ALERTS_RECENT_SEARCH, ALERTS_SAVED_SEARCH, NUM_SAVED_SEARCH } from '../utils/constants'; @Injectable() export class SaveSearchService { @@ -32,14 +33,29 @@ export class SaveSearchService { private loadSavedSearch = new Subject(); loadSavedSearch$ = this.loadSavedSearch.asObservable(); - constructor(private dataSource: DataSource) {} - - deleteRecentSearch(saveSearch: SaveSearch): Observable<{}> { - return this.dataSource.deleteRecentSearch(saveSearch); - } + constructor( + private userSettingsService: UserSettingsService + ) {} deleteSavedSearch(saveSearch: SaveSearch): Observable<{}> { - return this.dataSource.deleteSavedSearch(saveSearch); + return new Observable((observer) => { + this.userSettingsService.get(ALERTS_SAVED_SEARCH) + .subscribe((savedSearches) => { + this.userSettingsService.get(ALERTS_RECENT_SEARCH) + .subscribe((recentSearches) => { + if (recentSearches) { + recentSearches = recentSearches.filter(search => search.name !== saveSearch.name); + } + savedSearches = savedSearches.filter(search => search.name !== saveSearch.name); + this.userSettingsService.save({ + [ALERTS_RECENT_SEARCH]: recentSearches, + [ALERTS_SAVED_SEARCH]: savedSearches + }).subscribe(); + observer.next({}); + observer.complete(); + }); + }); + }); } fireLoadSavedSearch(savedSearch: SaveSearch) { @@ -47,19 +63,82 @@ export class SaveSearchService { } listRecentSearches(): Observable { - return this.dataSource.listRecentSearches(); + return new Observable((observer) => { + this.userSettingsService.get(ALERTS_RECENT_SEARCH) + .subscribe((recentSearches) => { + recentSearches = recentSearches || []; + recentSearches = recentSearches.map(search => SaveSearch.fromJSON(search)); + observer.next(recentSearches); + observer.complete(); + }); + }); } listSavedSearches(): Observable { - return this.dataSource.listSavedSearches(); + return new Observable((observer) => { + this.userSettingsService.get(ALERTS_SAVED_SEARCH) + .subscribe((savedSearches) => { + savedSearches = savedSearches || []; + savedSearches = savedSearches.map(search => SaveSearch.fromJSON(search)); + observer.next(savedSearches); + observer.complete(); + }); + }); } saveAsRecentSearches(saveSearch: SaveSearch): Observable<{}> { - return this.dataSource.saveRecentSearch(saveSearch); + return new Observable((observer) => { + saveSearch.lastAccessed = new Date().getTime(); + this.userSettingsService.get(ALERTS_RECENT_SEARCH) + .subscribe((recentSearches) => { + if (!recentSearches) { + recentSearches = []; + } + if (recentSearches.length === 0) { + recentSearches.push(saveSearch); + } else { + let found = false; + for (let recentSearch of recentSearches) { + if (saveSearch.name === recentSearch.name) { + recentSearch.lastAccessed = new Date().getTime(); + found = true; + break; + } + } + if (!found) { + if (recentSearches.length < NUM_SAVED_SEARCH) { + recentSearches.push(saveSearch); + } else { + recentSearches.sort((s1, s2) => s1.lastAccessed - s2.lastAccessed).shift(); + recentSearches.push(saveSearch); + } + } + } + + this.userSettingsService.save({ + [ALERTS_RECENT_SEARCH]: recentSearches + }).subscribe(); + observer.next({}); + observer.complete(); + }); + }); } saveSearch(saveSearch: SaveSearch): Observable<{}> { - return this.dataSource.saveSearch(saveSearch); + return new Observable((observer) => { + this.userSettingsService.get(ALERTS_SAVED_SEARCH) + .subscribe((savedSearches) => { + if (!savedSearches) { + savedSearches = []; + } + savedSearches.push(saveSearch); + this.userSettingsService.save({ + [ALERTS_SAVED_SEARCH]: savedSearches + }).subscribe(); + observer.next({}); + observer.complete(); + }); + }); } setCurrentQueryBuilderAndTableColumns(queryBuilder: QueryBuilder, tableColumns: ColumnMetadata[]) { @@ -68,6 +147,30 @@ export class SaveSearchService { } updateSearch(saveSearch: SaveSearch): Observable<{}> { - return this.dataSource.updateSearch(saveSearch); + return new Observable((observer) => { + this.userSettingsService.get(ALERTS_SAVED_SEARCH) + .subscribe((savedSearches) => { + debugger; + if (!savedSearches) { + savedSearches = []; + } + savedSearches = savedSearches.map(search => { + if (search.name === saveSearch.name) { + return { + ...search, + lastAccessed: saveSearch.lastAccessed, + searchRequest: saveSearch.searchRequest + }; + } + return search; + }); + this.userSettingsService.save({ + [ALERTS_SAVED_SEARCH]: savedSearches + }).subscribe(() => { + observer.next({}); + observer.complete(); + }); + }); + }); } } diff --git a/metron-interface/metron-alerts/src/app/service/user-settings.service.spec.ts b/metron-interface/metron-alerts/src/app/service/user-settings.service.spec.ts new file mode 100644 index 0000000000..896073038a --- /dev/null +++ b/metron-interface/metron-alerts/src/app/service/user-settings.service.spec.ts @@ -0,0 +1,78 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { AppConfigService } from './app-config.service'; +import { UserSettingsService } from './user-settings.service'; + +describe('UserSettingsService', () => { + let service: UserSettingsService; + let httpTestingController: HttpTestingController; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule ], + providers: [ + UserSettingsService, + { provide: AppConfigService, useClass: () => { + return { + getApiRoot: () => '/api', + } + } }, + ] + }); + service = TestBed.get(UserSettingsService); + httpTestingController = TestBed.get(HttpTestingController); + }); + + it('should load from server', (done) => { + service.load() + .then(() => { + service.get('foo').subscribe((value) => { + expect(value).toBe('bar'); + done(); + }); + }); + + const req = httpTestingController.expectOne('/api/hdfs?path=user-settings'); + expect(req.request.method).toEqual('GET'); + req.flush({ + foo: 'bar' + }); + + httpTestingController.verify(); + }); + + it('should save properly', () => { + service.save({ + foo: 'bar' + }).subscribe(); + + service.get('foo').subscribe((value) => { + expect(value).toBe('bar'); + }); + + const req = httpTestingController.expectOne('/api/hdfs?path=user-settings'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual({ + foo: 'bar' + }); + req.flush({}); + + httpTestingController.verify(); + }); +}); diff --git a/metron-interface/metron-alerts/src/app/service/user-settings.service.ts b/metron-interface/metron-alerts/src/app/service/user-settings.service.ts new file mode 100644 index 0000000000..e7344171e2 --- /dev/null +++ b/metron-interface/metron-alerts/src/app/service/user-settings.service.ts @@ -0,0 +1,58 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Injectable} from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { AppConfigService } from './app-config.service'; +import { Observable } from 'rxjs'; + +let settings = {}; + +@Injectable() +export class UserSettingsService { + + constructor( + private http: HttpClient, + private appConfigService: AppConfigService + ) {} + + load(): Promise { + return this.http.get(this.appConfigService.getApiRoot() + '/hdfs?path=user-settings') + .toPromise() + .then((result) => { + settings = result; + }) + .catch(() => ({})); + } + + save(data: any): Observable { + const newSettings = { + ...settings, + ...data + }; + settings = newSettings; + return this.http.post(this.appConfigService.getApiRoot() + '/hdfs?path=user-settings', newSettings); + } + + get(key: string): Observable { + return new Observable((observer) => { + observer.next(settings[key]); + observer.complete(); + }); + + } +} diff --git a/metron-interface/metron-alerts/src/app/utils/constants.ts b/metron-interface/metron-alerts/src/app/utils/constants.ts index 6e05de0a21..59d561848a 100644 --- a/metron-interface/metron-alerts/src/app/utils/constants.ts +++ b/metron-interface/metron-alerts/src/app/utils/constants.ts @@ -21,8 +21,8 @@ import { environment } from '../../environments/environment'; export const META_ALERTS_SENSOR_TYPE = 'metaalert'; export const NUM_SAVED_SEARCH = 10; -export const ALERTS_RECENT_SEARCH = 'metron-alerts-recent-saved-search'; -export const ALERTS_SAVED_SEARCH = 'metron-alerts-saved-search'; +export const ALERTS_RECENT_SEARCH = 'recentSavedSearches'; +export const ALERTS_SAVED_SEARCH = 'savedSearches'; export const ALERTS_TABLE_METADATA = 'metron-alerts-table-metadata'; export const ALERTS_COLUMN_NAMES = 'metron-alerts-column-names';