This is an automated email from the ASF dual-hosted git repository. oleewere pushed a commit to branch cloudbreak in repository https://gitbox.apache.org/repos/asf/ambari-logsearch.git
commit d8529088869223ee7eee6b33a6b7f9bf6adab92f Author: Istvan Tobias <tobias.ist...@gmail.com> AuthorDate: Fri Oct 5 18:16:16 2018 +0200 [AMBARI-24656] [Log Search UI] Handle the 401 and the 403 response status at login (#2) --- ambari-logsearch-web/karma.conf.js | 1 + ambari-logsearch-web/package.json | 3 + ambari-logsearch-web/src/app/app.module.ts | 16 +- .../src/app/classes/models/app-state.ts | 10 +- .../src/app/classes/models/store.ts | 35 ++-- .../models/user.ts} | 9 +- ambari-logsearch-web/src/app/classes/string.ts | 1 - .../action-menu/action-menu.component.html | 2 +- .../action-menu/action-menu.component.spec.ts | 16 +- .../action-menu/action-menu.component.ts | 12 +- .../src/app/components/app.component.html | 11 +- .../src/app/components/app.component.less | 7 + .../src/app/components/app.component.ts | 59 ++++-- .../audit-logs-entries.component.spec.ts | 14 +- .../audit-logs-table.component.spec.ts | 16 +- .../cluster-filter.component.spec.ts | 5 +- .../cluster-filter/cluster-filter.component.ts | 6 +- .../context-menu/context-menu.component.spec.ts | 10 +- .../filters-panel/filters-panel.component.html | 62 +++++-- .../filters-panel/filters-panel.component.spec.ts | 14 +- .../log-context/log-context.component.spec.ts | 14 +- .../log-index-filter.component.html | 4 +- .../log-index-filter.component.less | 6 + .../log-index-filter.component.spec.ts | 16 +- .../log-index-filter/log-index-filter.component.ts | 4 +- .../login-form/login-form.component.html | 19 +- .../login-form/login-form.component.less | 13 +- .../login-form/login-form.component.spec.ts | 56 ++---- .../components/login-form/login-form.component.ts | 69 +++---- .../logs-container.component.spec.ts | 19 +- .../logs-container/logs-container.component.ts | 2 +- .../main-container/main-container.component.ts | 29 +-- .../service-logs-table.component.spec.ts | 10 +- .../time-range-picker.component.spec.ts | 16 +- .../timezone-picker.component.spec.ts | 10 +- .../components/top-menu/top-menu.component.spec.ts | 10 +- .../app/components/top-menu/top-menu.component.ts | 15 +- .../src/app/modules/app-load/app-load.module.ts | 23 ++- .../modules/app-load/services/app-load.service.ts | 41 +++-- .../dropdown-button/dropdown-button.component.ts | 10 +- .../dropdown-list/dropdown-list.component.html | 8 +- .../dropdown-list/dropdown-list.component.spec.ts | 10 +- .../dropdown-list/dropdown-list.component.ts | 6 +- .../filter-dropdown.component.spec.ts | 10 +- .../modal-dialog/modal-dialog.component.spec.ts | 20 ++- .../shared/interfaces/notification.interface.ts | 7 +- .../shared/services/notification.service.ts | 2 +- .../src/app/modules/shared/variables.less | 3 +- .../src/app/services/auth-guard.service.ts | 12 +- .../src/app/services/auth.service.spec.ts | 84 ++++----- .../src/app/services/auth.service.ts | 143 ++------------- .../services/component-generator.service.spec.ts | 14 +- .../app/services/history-manager.service.spec.ts | 16 +- .../src/app/services/http-client.service.ts | 18 +- .../app/services/log-index-filter.service.spec.ts | 7 +- .../src/app/services/login-screen-guard.service.ts | 11 +- .../app/services/logs-container.service.spec.ts | 14 +- .../src/app/services/logs-container.service.ts | 98 +++++----- .../src/app/services/storage/reducers.service.ts | 5 +- .../src/app/services/user-settings.service.spec.ts | 12 +- .../src/app/store/actions/auth.actions.ts | 101 +++++++++++ .../actions/notification.actions.ts} | 19 +- .../src/app/store/effects/auth.effects.ts | 198 +++++++++++++++++++++ .../effects/notification.effects.ts} | 40 +++-- .../src/app/store/reducers/auth.reducers.ts | 120 +++++++++++++ .../src/app/store/selectors/auth.selectors.ts | 47 +++++ ambari-logsearch-web/src/app/test-config.spec.ts | 41 +++-- ambari-logsearch-web/src/assets/i18n/en.json | 8 +- ambari-logsearch-web/yarn.lock | 31 ++++ 69 files changed, 1256 insertions(+), 544 deletions(-) diff --git a/ambari-logsearch-web/karma.conf.js b/ambari-logsearch-web/karma.conf.js index 08608d8..b7e9d03 100644 --- a/ambari-logsearch-web/karma.conf.js +++ b/ambari-logsearch-web/karma.conf.js @@ -26,6 +26,7 @@ module.exports = function (config) { plugins: [ require('karma-jasmine'), require('karma-phantomjs-launcher'), + require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), require('@angular/cli/plugins/karma') diff --git a/ambari-logsearch-web/package.json b/ambari-logsearch-web/package.json index 3639b54..2b1e7ef 100644 --- a/ambari-logsearch-web/package.json +++ b/ambari-logsearch-web/package.json @@ -23,6 +23,7 @@ "@angular/platform-browser-dynamic": "^4.0.0", "@angular/router": "^4.0.0", "@ngrx/core": "^1.2.0", + "@ngrx/effects": "2.0.5", "@ngrx/store": "^2.2.3", "@ngrx/store-devtools": "3.2.4", "@ngx-translate/core": "^6.0.1", @@ -37,9 +38,11 @@ "d3-scale-chromatic": "^1.1.1", "font-awesome": "^4.7.0", "jquery": "^1.12.4", + "karma-chrome-launcher": "^2.2.0", "moment": "^2.18.1", "moment-timezone": "^0.5.13", "ngx-bootstrap": "^2.0.5", + "reselect": "^3.0.1", "rxjs": "^5.4.3", "zone.js": "^0.8.4" }, diff --git a/ambari-logsearch-web/src/app/app.module.ts b/ambari-logsearch-web/src/app/app.module.ts index b72980e..097bf04 100644 --- a/ambari-logsearch-web/src/app/app.module.ts +++ b/ambari-logsearch-web/src/app/app.module.ts @@ -17,10 +17,9 @@ */ import {BrowserModule} from '@angular/platform-browser'; -import {NgModule, CUSTOM_ELEMENTS_SCHEMA, Injector} from '@angular/core'; +import {NgModule, CUSTOM_ELEMENTS_SCHEMA, APP_INITIALIZER, Injector} from '@angular/core'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {HttpModule, Http, XHRBackend, BrowserXhr, ResponseOptions, XSRFStrategy} from '@angular/http'; -import {InMemoryBackendService} from 'angular-in-memory-web-api'; +import { HttpModule, Http } from '@angular/http'; import {TypeaheadModule, TooltipModule} from 'ngx-bootstrap'; import {TranslateModule, TranslateLoader} from '@ngx-translate/core'; import {StoreModule} from '@ngrx/store'; @@ -30,7 +29,7 @@ import {MomentTimezoneModule} from 'angular-moment-timezone'; import {NgStringPipesModule} from 'angular-pipes'; import {SimpleNotificationsModule} from 'angular2-notifications'; -import {environment} from '@envs/environment'; +import { EffectsModule } from '@ngrx/effects'; import {SharedModule} from '@modules/shared/shared.module'; import {AppLoadModule} from '@modules/app-load/app-load.module'; @@ -115,6 +114,9 @@ import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.serv import {LogsStateService} from '@app/services/storage/logs-state.service'; import {LoginScreenGuardService} from '@app/services/login-screen-guard.service'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + @NgModule({ declarations: [ AppComponent, @@ -186,7 +188,11 @@ import {LoginScreenGuardService} from '@app/services/login-screen-guard.service' maxAge: 5 }), - AppRoutingModule + AppRoutingModule, + + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects) + ], providers: [ HttpClientService, diff --git a/ambari-logsearch-web/src/app/classes/models/app-state.ts b/ambari-logsearch-web/src/app/classes/models/app-state.ts index 2a4d4cc..374e15d 100644 --- a/ambari-logsearch-web/src/app/classes/models/app-state.ts +++ b/ambari-logsearch-web/src/app/classes/models/app-state.ts @@ -16,9 +16,9 @@ * limitations under the License. */ -import {ActiveServiceLogEntry} from '@app/classes/active-service-log-entry'; -import {ListItem} from '@app/classes/list-item'; -import {DataAvailability, DataAvailabilityValues, LogsType} from '@app/classes/string'; +import { ActiveServiceLogEntry } from '@app/classes/active-service-log-entry'; +import { ListItem } from '@app/classes/list-item'; +import { DataAvailabilityValues, LogsType } from '@app/classes/string'; export interface History { items: ListItem[]; @@ -26,10 +26,9 @@ export interface History { } export interface AppState { - isAuthorized: boolean; isInitialLoading: boolean; isLoginInProgress: boolean; - baseDataSetState: DataAvailability; + baseDataSetState: DataAvailabilityValues; activeLogsType?: LogsType; isServiceLogsFileView: boolean; isServiceLogContextView: boolean; @@ -39,7 +38,6 @@ export interface AppState { } export const initialState: AppState = { - isAuthorized: false, isInitialLoading: false, isLoginInProgress: false, baseDataSetState: DataAvailabilityValues.NOT_AVAILABLE, diff --git a/ambari-logsearch-web/src/app/classes/models/store.ts b/ambari-logsearch-web/src/app/classes/models/store.ts index 9e34b14..f106b17 100644 --- a/ambari-logsearch-web/src/app/classes/models/store.ts +++ b/ambari-logsearch-web/src/app/classes/models/store.ts @@ -16,24 +16,26 @@ * limitations under the License. */ -import {ReflectiveInjector} from '@angular/core'; -import {Observable} from 'rxjs/Observable'; -import {Store, Action} from '@ngrx/store'; -import {AppSettings} from '@app/classes/models/app-settings'; -import {AppState} from '@app/classes/models/app-state'; -import {AuditLog} from '@app/classes/models/audit-log'; -import {ServiceLog} from '@app/classes/models/service-log'; -import {BarGraph} from '@app/classes/models/bar-graph'; -import {Graph} from '@app/classes/models/graph'; -import {NodeItem} from '@app/classes/models/node-item'; -import {UserConfig} from '@app/classes/models/user-config'; -import {LogTypeTab} from '@app/classes/models/log-type-tab'; -import {LogField} from '@app/classes/object'; -import {UtilsService} from '@app/services/utils.service'; -import {NotificationInterface} from '@modules/shared/interfaces/notification.interface'; -import {LogsState} from '@app/classes/models/logs-state'; +import { ReflectiveInjector } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Store, Action } from '@ngrx/store'; +import { AppSettings } from '@app/classes/models/app-settings'; +import { AppState } from '@app/classes/models/app-state'; +import { AuditLog } from '@app/classes/models/audit-log'; +import { ServiceLog } from '@app/classes/models/service-log'; +import { BarGraph } from '@app/classes/models/bar-graph'; +import { Graph } from '@app/classes/models/graph'; +import { NodeItem } from '@app/classes/models/node-item'; +import { UserConfig } from '@app/classes/models/user-config'; +import { LogTypeTab } from '@app/classes/models/log-type-tab'; +import { LogField } from '@app/classes/object'; +import { UtilsService } from '@app/services/utils.service'; +import { NotificationInterface } from '@modules/shared/interfaces/notification.interface'; +import { LogsState } from '@app/classes/models/logs-state'; import { DataAvaibilityStatesModel } from '@app/modules/app-load/models/data-availability-state.model'; +import * as auth from '@app/store/reducers/auth.reducers'; + const storeActions = { 'ARRAY.ADD': 'ADD', 'ARRAY.ADD.START': 'ADD_TO_START', @@ -68,6 +70,7 @@ export interface AppStore { notifications: NotificationInterface[]; logsState: LogsState; dataAvailabilityStates: DataAvaibilityStatesModel; + auth: auth.State; } export class ModelService { diff --git a/ambari-logsearch-web/src/app/modules/shared/interfaces/notification.interface.ts b/ambari-logsearch-web/src/app/classes/models/user.ts similarity index 82% copy from ambari-logsearch-web/src/app/modules/shared/interfaces/notification.interface.ts copy to ambari-logsearch-web/src/app/classes/models/user.ts index b8b3d6a..1743865 100644 --- a/ambari-logsearch-web/src/app/modules/shared/interfaces/notification.interface.ts +++ b/ambari-logsearch-web/src/app/classes/models/user.ts @@ -15,10 +15,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Options} from 'angular2-notifications/src/options.type'; -export interface NotificationInterface extends Options { - type: string; - message: string; - title: string; +export interface User { + username: string; + name?: string; + email?: string; } diff --git a/ambari-logsearch-web/src/app/classes/string.ts b/ambari-logsearch-web/src/app/classes/string.ts index db1311f..2e57740 100644 --- a/ambari-logsearch-web/src/app/classes/string.ts +++ b/ambari-logsearch-web/src/app/classes/string.ts @@ -26,7 +26,6 @@ export type ScrollType = 'before' | 'after' | ''; export type LogLevel = 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE' | 'UNKNOWN'; -export type DataAvailability = 'NOT_AVAILABLE' | 'LOADING' | 'AVAILABLE' | 'ERROR'; export enum DataAvailabilityValues { NOT_AVAILABLE = 'NOT_AVAILABLE', LOADING = 'LOADING', diff --git a/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html b/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html index 8970316..e64a89c 100644 --- a/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html +++ b/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html @@ -35,7 +35,7 @@ (buttonClick)="stopCapture()"></menu-button> <menu-button label="{{'topMenu.refresh' | translate}}" iconClass="fa fa-refresh" (buttonClick)="refresh()"></menu-button> -<modal-dialog +<modal-dialog *ngIf="isLogIndexFilterDisplayed$ | async" class="log-index-filter" [visible]="isLogIndexFilterDisplayed$ | async" [showCloseBtn]="false" diff --git a/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts b/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts index 4d84bb7..133c4bb 100644 --- a/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts @@ -56,6 +56,12 @@ import {NotificationService} from '@modules/shared/services/notification.service import { DataAvailabilityStatesStore, dataAvailabilityStates } from '@app/modules/app-load/stores/data-availability-state.store'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { AuthService } from '@app/services/auth.service'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('ActionMenuComponent', () => { let component: ActionMenuComponent; let fixture: ComponentFixture<ActionMenuComponent>; @@ -81,8 +87,11 @@ describe('ActionMenuComponent', () => { hosts, serviceLogsTruncated, tabs, - dataAvailabilityStates - }) + dataAvailabilityStates, + auth: auth.reducer + }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects) ], declarations: [ LogIndexFilterComponent, @@ -116,7 +125,8 @@ describe('ActionMenuComponent', () => { LogsStateService, NotificationsService, NotificationService, - DataAvailabilityStatesStore + DataAvailabilityStatesStore, + AuthService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts b/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts index a293e95..46a0a76 100644 --- a/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts +++ b/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts @@ -63,7 +63,7 @@ export class ActionMenuComponent implements OnInit, OnDestroy { subscriptions: Subscription[] = []; constructor( - private logsContainer: LogsContainerService, + private logsContainerService: LogsContainerService, private historyManager: HistoryManagerService, private settings: UserSettingsService, private route: ActivatedRoute, @@ -98,7 +98,7 @@ export class ActionMenuComponent implements OnInit, OnDestroy { } get captureSeconds(): number { - return this.logsContainer.captureSeconds; + return this.logsContainerService.captureSeconds; } setModalSubmitDisabled(isDisabled: boolean): void { @@ -126,7 +126,7 @@ export class ActionMenuComponent implements OnInit, OnDestroy { } refresh(): void { - this.logsContainer.loadLogs(); + this.logsContainerService.loadLogs(); } onSelectCluster(cluster: string) { @@ -157,15 +157,15 @@ export class ActionMenuComponent implements OnInit, OnDestroy { } startCapture(): void { - this.logsContainer.startCaptureTimer(); + this.logsContainerService.startCaptureTimer(); } stopCapture(): void { - this.logsContainer.stopCaptureTimer(); + this.logsContainerService.stopCaptureTimer(); } cancelCapture(): void { - this.logsContainer.cancelCapture(); + this.logsContainerService.cancelCapture(); } } diff --git a/ambari-logsearch-web/src/app/components/app.component.html b/ambari-logsearch-web/src/app/components/app.component.html index c9f8313..47d461b 100644 --- a/ambari-logsearch-web/src/app/components/app.component.html +++ b/ambari-logsearch-web/src/app/components/app.component.html @@ -22,6 +22,11 @@ <top-menu *ngIf="(isAuthorized$ | async) && (isBaseDataAvailable$ | async)"></top-menu> </nav> </header> -<data-loading-indicator *ngIf="!(isBaseDataAvailable$ | async) && (isAuthorized$ | async)"></data-loading-indicator> -<main-container *ngIf="!(isAuthorized$ | async) || (isBaseDataAvailable$ | async)"></main-container> -<simple-notifications [options]="notificationServiceOptions"></simple-notifications> + +<ng-container *ngIf="(authorizationStatus$ | async) !== authorizationStatuses.LOGGED_OUT"> + <data-loading-indicator *ngIf="!(isBaseDataAvailable$ | async) && (isAuthorized$ | async)"></data-loading-indicator> + + <main-container *ngIf="!(isAuthorized$ | async) || (isBaseDataAvailable$ | async)"></main-container> + + <simple-notifications [options]="notificationServiceOptions"></simple-notifications> +</ng-container> diff --git a/ambari-logsearch-web/src/app/components/app.component.less b/ambari-logsearch-web/src/app/components/app.component.less index b9eb907..3e56671 100644 --- a/ambari-logsearch-web/src/app/components/app.component.less +++ b/ambari-logsearch-web/src/app/components/app.component.less @@ -59,4 +59,11 @@ } } } + .auth-checking-in-progress { + align-content: center; + align-items: center; + display: flex; + justify-content: center; + margin: 1rem 0; + } } diff --git a/ambari-logsearch-web/src/app/components/app.component.ts b/ambari-logsearch-web/src/app/components/app.component.ts index 09a82c2..68d220e 100644 --- a/ambari-logsearch-web/src/app/components/app.component.ts +++ b/ambari-logsearch-web/src/app/components/app.component.ts @@ -16,23 +16,41 @@ * limitations under the License. */ -import {Component} from '@angular/core'; -import {AppStateService} from '@app/services/storage/app-state.service'; -import {Observable} from 'rxjs/Observable'; -import {Options} from 'angular2-notifications/src/options.type'; -import {notificationIcons} from '@modules/shared/services/notification.service'; -import { DataAvailability, DataAvailabilityValues } from '@app/classes/string'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +import { Options } from 'angular2-notifications/src/options.type'; + +import { AppStateService } from '@app/services/storage/app-state.service'; +import { DataAvailabilityValues } from '@app/classes/string'; +import { notificationIcons } from '@modules/shared/services/notification.service'; + +import { Store } from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import { AuthorizationStatuses } from '@app/store/reducers/auth.reducers'; +import { isAuthorizedSelector, authStatusSelector, isCheckingAuthStatusInProgressSelector } from '@app/store/selectors/auth.selectors'; @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.less', '../modules/shared/notifications.less'] + styleUrls: ['./app.component.less', '../modules/shared/notifications.less'], + host: { + '[class]': 'hostCssClasses' + } }) -export class AppComponent { +export class AppComponent implements OnInit, OnDestroy { - isAuthorized$: Observable<boolean> = this.appState.getParameter('isAuthorized'); + authorizationStatuses = AuthorizationStatuses; + + isAuthorized$: Observable<boolean> = this.store.select(isAuthorizedSelector); + authorizationStatus$: Observable<AuthorizationStatuses> = this.store.select(authStatusSelector); + isCheckingAuthStatusInProgress$: Observable<boolean> = this.store.select(isCheckingAuthStatusInProgressSelector); + authorizationCode$: Observable<number> = this.appState.getParameter('authorizationCode'); isBaseDataAvailable$: Observable<boolean> = this.appState.getParameter('baseDataSetState') - .map((dataSetState: DataAvailability) => dataSetState === DataAvailabilityValues.AVAILABLE); + .map((dataSetState: DataAvailabilityValues) => dataSetState === DataAvailabilityValues.AVAILABLE); + + destroyed$ = new Subject(); notificationServiceOptions: Options = { timeOut: 2000, @@ -44,8 +62,27 @@ export class AppComponent { position: ['top', 'left'] }; + hostCssClasses = ''; + constructor( - private appState: AppStateService + private appState: AppStateService, + private store: Store<AppStore> ) {} + ngOnInit() { + this.authorizationStatus$.distinctUntilChanged().takeUntil(this.destroyed$).subscribe(this.onAuthStatusChange); + } + + ngOnDestroy() { + this.destroyed$.next(true); + } + + onAuthStatusChange = (status: AuthorizationStatuses): void => { + this.setHostCssClasses(status ? status.replace(/\s/, '-').toLocaleLowerCase() : ''); + } + + setHostCssClasses(cls: string) { + this.hostCssClasses = cls; + } + } diff --git a/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts b/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts index 51d1fda..e6191f1 100644 --- a/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts @@ -47,6 +47,12 @@ import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.serv import {NotificationService} from '@modules/shared/services/notification.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { AuthService } from '@app/services/auth.service'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('AuditLogsEntriesComponent', () => { let component: AuditLogsEntriesComponent; let fixture: ComponentFixture<AuditLogsEntriesComponent>; @@ -73,8 +79,11 @@ describe('AuditLogsEntriesComponent', () => { components, hosts, serviceLogsTruncated, - tabs + tabs, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects) ], providers: [ ...MockHttpRequestModules, @@ -98,7 +107,8 @@ describe('AuditLogsEntriesComponent', () => { LogsFilteringUtilsService, LogsStateService, NotificationsService, - NotificationService + NotificationService, + AuthService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.spec.ts b/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.spec.ts index f65180d..60c6e9b 100644 --- a/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.spec.ts @@ -52,6 +52,12 @@ import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.serv import {NotificationService} from '@modules/shared/services/notification.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import { AuthService } from '@app/services/auth.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('AuditLogsTableComponent', () => { let component: AuditLogsTableComponent; let fixture: ComponentFixture<AuditLogsTableComponent>; @@ -83,8 +89,11 @@ describe('AuditLogsTableComponent', () => { tabs, clusters, components, - hosts - }) + hosts, + auth: auth.reducer + }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects) ], providers: [ ...MockHttpRequestModules, @@ -108,7 +117,8 @@ describe('AuditLogsTableComponent', () => { LogsFilteringUtilsService, LogsStateService, NotificationsService, - NotificationService + NotificationService, + AuthService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/ambari-logsearch-web/src/app/components/cluster-filter/cluster-filter.component.spec.ts b/ambari-logsearch-web/src/app/components/cluster-filter/cluster-filter.component.spec.ts index 8a6cbc6..ac6efb1 100644 --- a/ambari-logsearch-web/src/app/components/cluster-filter/cluster-filter.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/cluster-filter/cluster-filter.component.spec.ts @@ -52,6 +52,8 @@ import {NotificationService} from '@modules/shared/services/notification.service import {NotificationsService} from 'angular2-notifications/src/notifications.service'; import { DataAvailabilityStatesStore, dataAvailabilityStates } from '@app/modules/app-load/stores/data-availability-state.store'; +import * as auth from '@app/store/reducers/auth.reducers'; + describe('ClusterFilterComponent', () => { let component: ClusterFilterComponent; let fixture: ComponentFixture<ClusterFilterComponent>; @@ -84,7 +86,8 @@ describe('ClusterFilterComponent', () => { clusters, components, hosts, - dataAvailabilityStates + dataAvailabilityStates, + auth: auth.reducer }) ], providers: [ diff --git a/ambari-logsearch-web/src/app/components/cluster-filter/cluster-filter.component.ts b/ambari-logsearch-web/src/app/components/cluster-filter/cluster-filter.component.ts index 9921d41..086160b 100644 --- a/ambari-logsearch-web/src/app/components/cluster-filter/cluster-filter.component.ts +++ b/ambari-logsearch-web/src/app/components/cluster-filter/cluster-filter.component.ts @@ -45,7 +45,7 @@ export class ClusterFilterComponent implements OnInit, OnDestroy { private clusterSelectionStoreKey: BehaviorSubject<string> = new BehaviorSubject(''); - private clustersAsListItems$: Observable<ListItem[]> = this.clusterSelectionStoreKey.distinctUntilChanged() + clustersAsListItems$: Observable<ListItem[]> = this.clusterSelectionStoreKey.distinctUntilChanged() .switchMap((selectionStoreKey: string) => Observable.combineLatest( this.clusterSelectionStoreService.getParameter(selectionStoreKey), this.clusterStoreService.getAll() @@ -58,8 +58,8 @@ export class ClusterFilterComponent implements OnInit, OnDestroy { }) ).startWith([]); - private readonly defaultUseMultiSelection = true; - private useMultiSelection: BehaviorSubject<boolean> = new BehaviorSubject(false); + readonly defaultUseMultiSelection = true; + useMultiSelection: BehaviorSubject<boolean> = new BehaviorSubject(false); private subscriptions: Subscription[] = []; diff --git a/ambari-logsearch-web/src/app/components/context-menu/context-menu.component.spec.ts b/ambari-logsearch-web/src/app/components/context-menu/context-menu.component.spec.ts index afca603..0dfc49a 100644 --- a/ambari-logsearch-web/src/app/components/context-menu/context-menu.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/context-menu/context-menu.component.spec.ts @@ -51,6 +51,11 @@ import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.serv import {NotificationService} from '@modules/shared/services/notification.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('ContextMenuComponent', () => { let component: ContextMenuComponent; let fixture: ComponentFixture<ContextMenuComponent>; @@ -86,8 +91,11 @@ describe('ContextMenuComponent', () => { clusters, components, serviceLogsTruncated, - tabs + tabs, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), FormsModule ], providers: [ diff --git a/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.html b/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.html index 7385305..657d1ea 100644 --- a/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.html +++ b/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.html @@ -32,25 +32,49 @@ <dropdown-button iconClass="fa fa-search-minus" label="{{'filter.exclude' | translate}}" [hideCaret]="true" [showSelectedValue]="false" [showCommonLabelWithSelection]="true" [options]="searchBoxItems$ | async" (selectItem)="proceedWithExclude($event)"></dropdown-button> - <filter-button *ngIf="isFilterConditionDisplayed('hosts')" formControlName="hosts" - label="{{filters.hosts.label | translate}}" [iconClass]="filters.hosts.iconClass" - [subItems]="filters.hosts.options" [isMultipleChoice]="true" [isRightAlign]="true" - additionalLabelComponentSetter="getDataForHostsNodeBar" - [class.disabled]="isServiceLogsFileView$ | async" [isDisabled]="isServiceLogsFileView$ | async" - [useDropDownLocalFilter]="true"></filter-button> - <filter-button *ngIf="isFilterConditionDisplayed('users')" formControlName="users" - label="{{filters.users.label | translate}}" [iconClass]="filters.users.iconClass" - [subItems]="filters.users.options" [isMultipleChoice]="true" [isRightAlign]="true" - [useDropDownLocalFilter]="true"></filter-button> - <filter-button *ngIf="isFilterConditionDisplayed('components')" formControlName="components" [useDropDownLocalFilter]="true" - label="{{filters.components.label | translate}}" [iconClass]="filters.components.iconClass" - [subItems]="filters.components.options" [isMultipleChoice]="true" [isRightAlign]="true" - [class.disabled]="isServiceLogsFileView$ | async" [isDisabled]="isServiceLogsFileView$ | async" - additionalLabelComponentSetter="getDataForComponentsNodeBar"></filter-button> - <filter-button *ngIf="isFilterConditionDisplayed('levels')" formControlName="levels" - label="{{filters.levels.label | translate}}" [iconClass]="filters.levels.iconClass" - [subItems]="filters.levels.options" [isMultipleChoice]="true" [isRightAlign]="true" - [useDropDownLocalFilter]="true"></filter-button> + + <filter-button *ngIf="isFilterConditionDisplayed('hosts')" + formControlName="hosts" + [subItems]="filters.hosts.options" + label="{{filters.hosts.label | translate}}" + [useDropDownLocalFilter]="true" + [isMultipleChoice]="true" + [iconClass]="filters.hosts.iconClass" + [isRightAlign]="true" + [class.disabled]="isServiceLogsFileView$ | async" + [isDisabled]="isServiceLogsFileView$ | async" + additionalLabelComponentSetter="getDataForHostsNodeBar"></filter-button> + + <filter-button *ngIf="isFilterConditionDisplayed('users')" + formControlName="users" + label="{{filters.users.label | translate}}" + [iconClass]="filters.users.iconClass" + [subItems]="filters.users.options" + [isMultipleChoice]="true" + [isRightAlign]="true" + [useDropDownLocalFilter]="true"></filter-button> + + <filter-button *ngIf="isFilterConditionDisplayed('components')" + formControlName="components" + [subItems]="filters.components.options" + label="{{filters.components.label | translate}}" + [useDropDownLocalFilter]="true" + [isMultipleChoice]="true" + [iconClass]="filters.components.iconClass" + [isRightAlign]="true" + [class.disabled]="isServiceLogsFileView$ | async" + [isDisabled]="isServiceLogsFileView$ | async" + additionalLabelComponentSetter="getDataForComponentsNodeBar"></filter-button> + + <filter-button *ngIf="isFilterConditionDisplayed('levels')" + formControlName="levels" + label="{{filters.levels.label | translate}}" + [iconClass]="filters.levels.iconClass" + [subItems]="filters.levels.options" + [isMultipleChoice]="true" + [isRightAlign]="true" + [useDropDownLocalFilter]="true"></filter-button> + <menu-button class="clear-filter-btn" iconClass="fa fa-times" label="{{'filters.clear' | translate}}" (buttonClick)="onClearBtnClick($event)"></menu-button> </div> diff --git a/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.spec.ts b/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.spec.ts index 3b85377..fa73ce0 100644 --- a/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.spec.ts @@ -48,6 +48,12 @@ import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.serv import {NotificationService} from '@modules/shared/services/notification.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { AuthService } from '@app/services/auth.service'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('FiltersPanelComponent', () => { let component: FiltersPanelComponent; let fixture: ComponentFixture<FiltersPanelComponent>; @@ -80,8 +86,11 @@ describe('FiltersPanelComponent', () => { serviceLogsHistogramData, appState, serviceLogsTruncated, - tabs + tabs, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), ...TranslationModules ], providers: [ @@ -106,7 +115,8 @@ describe('FiltersPanelComponent', () => { LogsFilteringUtilsService, LogsStateService, NotificationsService, - NotificationService + NotificationService, + AuthService ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/ambari-logsearch-web/src/app/components/log-context/log-context.component.spec.ts b/ambari-logsearch-web/src/app/components/log-context/log-context.component.spec.ts index 82201ba..e2e10ba 100644 --- a/ambari-logsearch-web/src/app/components/log-context/log-context.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/log-context/log-context.component.spec.ts @@ -47,6 +47,12 @@ import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.serv import {NotificationsService} from 'angular2-notifications/src/notifications.service'; import {NotificationService} from '@modules/shared/services/notification.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { AuthService } from '@app/services/auth.service'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('LogContextComponent', () => { let component: LogContextComponent; let fixture: ComponentFixture<LogContextComponent>; @@ -72,8 +78,11 @@ describe('LogContextComponent', () => { components, hosts, serviceLogsTruncated, - tabs + tabs, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), ...TranslationModules ], providers: [ @@ -98,7 +107,8 @@ describe('LogContextComponent', () => { LogsFilteringUtilsService, LogsStateService, NotificationsService, - NotificationService + NotificationService, + AuthService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.html b/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.html index f5dc84b..2096687 100644 --- a/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.html +++ b/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.html @@ -20,10 +20,10 @@ <tr> <th class="component-column">{{'filter.components' | translate}}</th> <th *ngFor="let column of columns" class="checkbox-column"> - <input type="checkbox" attr.id="{{column.name}}" + <input type="checkbox" attr.id="log-index-filter-component-{{column.name}}" [attr.checked]="isAllComponentsCheckedForLevel(column.name) ? 'checked' : null" (change)="processAllComponentsForLevel(column.name, $event.target.checked)"> - <label attr.for="{{column.name}}"> + <label attr.for="log-index-filter-component-{{column.name}}"> <graph-legend-item label="{{column.label | translate}}" color="{{column.color}}"></graph-legend-item> </label> </th> diff --git a/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.less b/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.less index a5c3957..efdb2c6 100644 --- a/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.less +++ b/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.less @@ -21,12 +21,18 @@ :host { div.log-index-filter-content { table { + td { + vertical-align: middle; + } &.table-header { background-color: #fff; margin-bottom: 0; position: sticky; top: -1px; z-index: 10; + tr { + box-shadow: -2px 2px 2px fadeout(@fluid-gray-1, 50%); + } th { padding: 8px 0; } diff --git a/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.spec.ts b/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.spec.ts index 3b042ae..19d0b57 100644 --- a/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.spec.ts @@ -55,6 +55,12 @@ import {ComponentLabelPipe} from '@app/pipes/component-label'; import { dataAvailabilityStates, DataAvailabilityStatesStore } from '@app/modules/app-load/stores/data-availability-state.store'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { AuthService } from '@app/services/auth.service'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('LogIndexFilterComponent', () => { let component: LogIndexFilterComponent; let fixture: ComponentFixture<LogIndexFilterComponent>; @@ -79,8 +85,11 @@ describe('LogIndexFilterComponent', () => { hosts, serviceLogsTruncated, tabs, - dataAvailabilityStates - }) + dataAvailabilityStates, + auth: auth.reducer + }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects) ], declarations: [ LogIndexFilterComponent, @@ -113,7 +122,8 @@ describe('LogIndexFilterComponent', () => { LogsStateService, NotificationsService, NotificationService, - DataAvailabilityStatesStore + DataAvailabilityStatesStore, + AuthService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.ts b/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.ts index 65c22a4..73c8604 100644 --- a/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.ts +++ b/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.ts @@ -33,7 +33,7 @@ import { UtilsService } from '@app/services/utils.service'; import { ClustersService } from '@app/services/storage/clusters.service'; import { HostsService } from '@app/services/storage/hosts.service'; import { DataAvailabilityStatesStore } from '@app/modules/app-load/stores/data-availability-state.store'; -import { DataAvailabilityValues, DataAvailability } from '@app/classes/string'; +import { DataAvailabilityValues } from '@app/classes/string'; @Component({ selector: 'log-index-filter', @@ -68,7 +68,7 @@ export class LogIndexFilterComponent implements OnInit, OnDestroy, OnChanges, Co configsAvailabilityState$: Observable<string> = this.dataAvailablilityStore.getParameter('logIndexFilter'); configsAreLoading$: Observable<boolean> = this.configsAvailabilityState$.distinctUntilChanged().map( - (state: DataAvailability) => state === DataAvailabilityValues.LOADING + (state: DataAvailabilityValues) => state === DataAvailabilityValues.LOADING ); @Input() diff --git a/ambari-logsearch-web/src/app/components/login-form/login-form.component.html b/ambari-logsearch-web/src/app/components/login-form/login-form.component.html index 3db75c6..344ac0a 100644 --- a/ambari-logsearch-web/src/app/components/login-form/login-form.component.html +++ b/ambari-logsearch-web/src/app/components/login-form/login-form.component.html @@ -16,18 +16,25 @@ --> <div class="login-form well col-md-4 col-md-offset-4 col-sm-offset-4"> - <div class="alert alert-danger" *ngIf="isLoginAlertDisplayed">{{errorMessage}}</div> + <div class="alert alert-danger" *ngIf="authorizationMessage$ | async">{{ (authorizationMessage$ | async) | translate}}</div> <form #loginForm="ngForm" (ngSubmit)="login()"> <div class="form-group"> <label for="username">{{'authorization.name' | translate}}</label> - <input class="form-control" type="text" id="username" name="username" required [(ngModel)]="username"> + <input class="form-control" type="text" id="username" name="username" + [disabled]="isInputDisabled$ | async" required [(ngModel)]="username"> </div> <div class="form-group"> <label for="password">{{'authorization.password' | translate}}</label> - <input class="form-control" type="password" id="password" name="password" required [(ngModel)]="password"> + <input class="form-control" type="password" id="password" name="password" + [disabled]="isInputDisabled$ | async" required [(ngModel)]="password"> </div> - <button class="btn btn-success" [disabled]="!loginForm.form.valid || (isLoginInProgress$ | async)"> - {{'authorization.signIn' | translate}} - </button> + <footer> + <button class="btn btn-success" [disabled]="!loginForm.form.valid || (isInputDisabled$ | async)"> + {{'authorization.signIn' | translate}} + </button> + <span *ngIf="isCheckingAuthStatusInProgress$ | async" class="checking-auth-in-progress"> + <i class="fa fa-spinner fa-spin"></i> {{'authorization.checkingAuthorization' | translate}} + </span> + </footer> </form> </div> diff --git a/ambari-logsearch-web/src/app/components/login-form/login-form.component.less b/ambari-logsearch-web/src/app/components/login-form/login-form.component.less index 19d800d..f7a4265 100644 --- a/ambari-logsearch-web/src/app/components/login-form/login-form.component.less +++ b/ambari-logsearch-web/src/app/components/login-form/login-form.component.less @@ -16,7 +16,14 @@ */ @import '../../modules/shared/variables'; - -.login-form { - margin-top: @block-margin-top; +:host { + footer { + display: flex; + .checking-auth-in-progress { + margin-left: auto; + } + } + .login-form { + margin-top: @block-margin-top; + } } diff --git a/ambari-logsearch-web/src/app/components/login-form/login-form.component.spec.ts b/ambari-logsearch-web/src/app/components/login-form/login-form.component.spec.ts index 3ec55fd..eb6a21e 100644 --- a/ambari-logsearch-web/src/app/components/login-form/login-form.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/login-form/login-form.component.spec.ts @@ -16,19 +16,26 @@ * limitations under the License. */ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {async, inject, ComponentFixture, TestBed} from '@angular/core/testing'; import {FormsModule} from '@angular/forms'; -import {TranslationModules} from '@app/test-config.spec'; +import {TranslationModules, MockHttpRequestModules} from '@app/test-config.spec'; import {StoreModule} from '@ngrx/store'; import {AppStateService, appState} from '@app/services/storage/app-state.service'; import {HttpClientService} from '@app/services/http-client.service'; -import {AuthService} from '@app/services/auth.service'; import {LoginFormComponent} from './login-form.component'; import {RouterTestingModule} from '@angular/router/testing'; import {NotificationsService} from 'angular2-notifications'; import {NotificationService} from '@app/modules/shared/services/notification.service'; +import {Store} from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { AuthService } from '@app/services/auth.service'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('LoginFormComponent', () => { let component: LoginFormComponent; let fixture: ComponentFixture<LoginFormComponent>; @@ -56,17 +63,22 @@ describe('LoginFormComponent', () => { FormsModule, ...TranslationModules, StoreModule.provideStore({ - appState - }) + appState, + auth: auth.reducer + }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects) ], providers: [ + ...MockHttpRequestModules, AppStateService, { provide: AuthService, useValue: AuthServiceMock }, NotificationsService, - NotificationService + NotificationService, + AuthService ] }) .compileComponents(); @@ -82,36 +94,4 @@ describe('LoginFormComponent', () => { expect(component).toBeTruthy(); }); - describe('#login()', () => { - const cases = [ - { - isError: true, - isLoginAlertDisplayed: true, - isAuthorized: false, - title: 'login failure' - }, - { - isError: false, - isLoginAlertDisplayed: false, - isAuthorized: true, - title: 'login success' - } - ]; - - cases.forEach(test => { - describe(test.title, () => { - beforeEach(() => { - authMock.isError = test.isError; - authMock.isAuthorized = test.isAuthorized; - component.login(); - }); - - it('isLoginAlertDisplayed', () => { - expect(component.isLoginAlertDisplayed).toEqual(test.isLoginAlertDisplayed); - }); - - }); - }); - - }); }); diff --git a/ambari-logsearch-web/src/app/components/login-form/login-form.component.ts b/ambari-logsearch-web/src/app/components/login-form/login-form.component.ts index 2f28411..8d5070b 100644 --- a/ambari-logsearch-web/src/app/components/login-form/login-form.component.ts +++ b/ambari-logsearch-web/src/app/components/login-form/login-form.component.ts @@ -16,71 +16,56 @@ * limitations under the License. */ -import {Component, ViewChild, OnInit, OnDestroy} from '@angular/core'; -import {Observable} from 'rxjs/Observable'; +import { Component, ViewChild } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/finally'; -import {Subscription} from 'rxjs/Subscription'; -import {AppStateService} from '@app/services/storage/app-state.service'; -import {AuthService} from '@app/services/auth.service'; -import {TranslateService} from '@ngx-translate/core'; -import {FormGroup} from '@angular/forms'; +import 'rxjs/add/operator/combineLatest'; + +import { Store } from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import { + isLoginInProgressSelector, + isCheckingAuthStatusInProgressSelector, + authMessageSelector +} from '@app/store/selectors/auth.selectors'; +import { LogInAction } from '@app/store/actions/auth.actions'; + + +import { FormGroup } from '@angular/forms'; + @Component({ selector: 'login-form', templateUrl: './login-form.component.html', styleUrls: ['./login-form.component.less'] }) -export class LoginFormComponent implements OnInit, OnDestroy { +export class LoginFormComponent { username: string; password: string; - isLoginAlertDisplayed: boolean; + authorizationMessage$: Observable<string> = this.store.select(authMessageSelector); + isLoginInProgress$: Observable<boolean> = this.store.select(isLoginInProgressSelector); + isCheckingAuthStatusInProgress$: Observable<boolean> = this.store.select(isCheckingAuthStatusInProgressSelector); - isLoginInProgress$: Observable<boolean> = this.appState.getParameter('isLoginInProgress'); + isInputDisabled$: Observable<boolean> = Observable.combineLatest(this.isLoginInProgress$, this.isCheckingAuthStatusInProgress$) + .map(([loginInProgress, checkAuthInProgress]) => loginInProgress || checkAuthInProgress); errorMessage: string; @ViewChild('loginForm') loginForm: FormGroup; - subscriptions: Subscription[] = []; - constructor( - private authService: AuthService, - private appState: AppStateService, - private translateService: TranslateService + private store: Store<AppStore> ) {} - ngOnInit(): void { - this.subscriptions.push( - this.loginForm.valueChanges.subscribe(this.onLoginFormChange) - ); - } - - ngOnDestroy(): void { - this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); - } - - onLoginFormChange = (event) => { - this.isLoginAlertDisplayed = false; - } - - private onLoginSuccess = (result: Boolean): void => { - this.isLoginAlertDisplayed = false; - this.errorMessage = ''; - } - - private onLoginError = (resp: Boolean): void => { - this.translateService.get('authorization.error.401').first().subscribe((message: string) => { - this.errorMessage = message; - this.isLoginAlertDisplayed = true; - }); - } - login() { - this.authService.login(this.username, this.password).subscribe(this.onLoginSuccess, this.onLoginError); + this.store.dispatch(new LogInAction({ + username: this.username, + password: this.password + })); } } diff --git a/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.spec.ts b/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.spec.ts index 78245e4..0051db7 100644 --- a/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.spec.ts @@ -43,12 +43,19 @@ import {TabsComponent} from '@app/components/tabs/tabs.component'; import {LogsContainerComponent} from './logs-container.component'; import {ClusterSelectionService} from '@app/services/storage/cluster-selection.service'; import {RouterTestingModule} from '@angular/router/testing'; -import {LogsStateService} from '@app/services/storage/logs-state.service'; import {RoutingUtilsService} from '@app/services/routing-utils.service'; import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.service'; import {NotificationService} from '@modules/shared/services/notification.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import {LogsStateService} from '@app/services/storage/logs-state.service'; + +import * as auth from '@app/store/reducers/auth.reducers'; +import { AuthService } from '@app/services/auth.service'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('LogsContainerComponent', () => { let component: LogsContainerComponent; let fixture: ComponentFixture<LogsContainerComponent>; @@ -74,8 +81,11 @@ describe('LogsContainerComponent', () => { serviceLogsHistogramData, tabs, hosts, - serviceLogsTruncated + serviceLogsTruncated, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), ...TranslationModules, TooltipModule.forRoot(), ], @@ -99,9 +109,10 @@ describe('LogsContainerComponent', () => { ClusterSelectionService, RoutingUtilsService, LogsFilteringUtilsService, - LogsStateService, NotificationsService, - NotificationService + NotificationService, + LogsStateService, + AuthService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts b/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts index 6b983fc..34eb2a4 100644 --- a/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts +++ b/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts @@ -313,7 +313,7 @@ export class LogsContainerComponent implements OnInit, OnDestroy { filtersParams, tab.appState.activeLogsType ); - // we dont't have to reset the form with the new values when there is tab changes + // we don't have to reset the form with the new values when there is tab changes // because the onActiveTabIdChange will call the setActiveTabById on LogsContainerService // which will reset the form to the tab's activeFilters prop. // If we do reset wvery time then the form will be reseted twice with every tab changes... not a big deal anyway diff --git a/ambari-logsearch-web/src/app/components/main-container/main-container.component.ts b/ambari-logsearch-web/src/app/components/main-container/main-container.component.ts index cd0f1be..adec4a7 100644 --- a/ambari-logsearch-web/src/app/components/main-container/main-container.component.ts +++ b/ambari-logsearch-web/src/app/components/main-container/main-container.component.ts @@ -16,35 +16,10 @@ * limitations under the License. */ -import {Component, OnDestroy, OnInit} from '@angular/core'; -import {AppStateService} from '@app/services/storage/app-state.service'; -import {Subscription} from 'rxjs/Subscription'; +import { Component } from '@angular/core'; @Component({ selector: 'main-container', templateUrl: './main-container.component.html' }) -export class MainContainerComponent implements OnInit, OnDestroy{ - - private subscriptions: Subscription[] = []; - - constructor(private appState: AppStateService) {} - - ngOnInit() { - this.subscriptions.push( - this.appState.getParameter('isAuthorized').subscribe((value: boolean) => this.isAuthorized = value) - ); - this.subscriptions.push( - this.appState.getParameter('isInitialLoading').subscribe((value: boolean) => this.isInitialLoading = value) - ); - } - - ngOnDestroy() { - this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); - } - - isAuthorized: boolean = false; - - isInitialLoading: boolean = false; - -} +export class MainContainerComponent {} diff --git a/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts b/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts index 7745781..5bf2969 100644 --- a/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts @@ -56,6 +56,11 @@ import {RouterTestingModule} from '@angular/router/testing'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; import {NotificationService} from '@modules/shared/services/notification.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('ServiceLogsTableComponent', () => { let component: ServiceLogsTableComponent; let fixture: ComponentFixture<ServiceLogsTableComponent>; @@ -88,8 +93,11 @@ describe('ServiceLogsTableComponent', () => { tabs, clusters, components, - hosts + hosts, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), TooltipModule.forRoot() ], providers: [ diff --git a/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.spec.ts b/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.spec.ts index f076861..d568354 100644 --- a/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.spec.ts @@ -35,7 +35,7 @@ import { } from '@app/services/storage/service-logs-histogram-data.service'; import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service'; import {TabsService, tabs} from '@app/services/storage/tabs.service'; -import {HttpClientService} from '@app/services/http-client.service'; +// import {HttpClientService} from '@app/services/http-client.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {UtilsService} from '@app/services/utils.service'; @@ -48,6 +48,12 @@ import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.serv import {NotificationService} from '@modules/shared/services/notification.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { AuthService } from '@app/services/auth.service'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('TimeRangePickerComponent', () => { let component: TimeRangePickerComponent; let fixture: ComponentFixture<TimeRangePickerComponent>; @@ -70,8 +76,11 @@ describe('TimeRangePickerComponent', () => { serviceLogsFields, serviceLogsHistogramData, serviceLogsTruncated, - tabs + tabs, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), ...TranslationModules ], providers: [ @@ -96,7 +105,8 @@ describe('TimeRangePickerComponent', () => { LogsFilteringUtilsService, LogsStateService, NotificationsService, - NotificationService + NotificationService, + AuthService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.spec.ts b/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.spec.ts index 736c7ef..2de7d33 100644 --- a/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.spec.ts @@ -54,6 +54,11 @@ import {NotificationService} from '@modules/shared/services/notification.service import { dataAvailabilityStates, DataAvailabilityStatesStore } from '@app/modules/app-load/stores/data-availability-state.store'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('TimeZonePickerComponent', () => { let component: TimeZonePickerComponent; let fixture: ComponentFixture<TimeZonePickerComponent>; @@ -81,8 +86,11 @@ describe('TimeZonePickerComponent', () => { serviceLogsHistogramData, serviceLogsTruncated, tabs, - dataAvailabilityStates + dataAvailabilityStates, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), ...TranslationModules ], providers: [ diff --git a/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.spec.ts b/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.spec.ts index 3ae2f97..9f263a4 100644 --- a/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.spec.ts +++ b/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.spec.ts @@ -50,6 +50,11 @@ import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.serv import {NotificationService} from '@modules/shared/services/notification.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('TopMenuComponent', () => { let component: TopMenuComponent; let fixture: ComponentFixture<TopMenuComponent>; @@ -72,8 +77,11 @@ describe('TopMenuComponent', () => { tabs, clusters, components, - hosts + hosts, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), ...TranslationModules ], declarations: [TopMenuComponent], diff --git a/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts b/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts index a87ca2f..76af57b 100644 --- a/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts +++ b/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts @@ -21,10 +21,13 @@ import {FormGroup} from '@angular/forms'; import {FilterCondition, TimeUnitListItem} from '@app/classes/filtering'; import {ListItem} from '@app/classes/list-item'; import {HomogeneousObject} from '@app/classes/object'; -import {AuthService} from '@app/services/auth.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {Router} from '@angular/router'; +import { Store } from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import { LogOutAction } from '@app/store/actions/auth.actions'; + @Component({ selector: 'top-menu', templateUrl: './top-menu.component.html', @@ -32,7 +35,11 @@ import {Router} from '@angular/router'; }) export class TopMenuComponent { - constructor(private authService: AuthService, private logsContainer: LogsContainerService, private router: Router) {} + constructor( + private logsContainer: LogsContainerService, + private router: Router, + private store: Store<AppStore> + ) {} get filtersForm(): FormGroup { return this.logsContainer.filtersForm; @@ -45,10 +52,10 @@ export class TopMenuComponent { openSettings = (): void => {}; /** - * Request a logout action from AuthService + * Dispatch the LogOutAction. */ logout = (): void => { - this.authService.logout(); + this.store.dispatch(new LogOutAction()); } navigateToShipperConfig = (): void => { diff --git a/ambari-logsearch-web/src/app/modules/app-load/app-load.module.ts b/ambari-logsearch-web/src/app/modules/app-load/app-load.module.ts index ade23da..2f93cb3 100644 --- a/ambari-logsearch-web/src/app/modules/app-load/app-load.module.ts +++ b/ambari-logsearch-web/src/app/modules/app-load/app-load.module.ts @@ -22,13 +22,26 @@ import { HttpClientModule } from '@angular/common/http'; import { AppLoadService } from './services/app-load.service'; import { DataAvailabilityStatesStore } from '@app/modules/app-load/stores/data-availability-state.store'; -export function check_if_authorized(appLoadService: AppLoadService) { - return () => appLoadService.syncAuthorizedStateWithBackend(); -} +import { Store } from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import { CheckAuthorizationStatusAction } from '@app/store/actions/auth.actions'; +import { authStatusSelector } from '@app/store/selectors/auth.selectors'; +import { AuthorizationStatuses } from '@app/store/reducers/auth.reducers'; + export function set_translation_service(appLoadService: AppLoadService) { return () => appLoadService.setTranslationService(); } +export function check_auth_status(store: Store<AppStore>) { + return () => new Promise((resolve) => { + store.select(authStatusSelector) + .filter( + (status: AuthorizationStatuses): boolean => (status !== null && AuthorizationStatuses.CHEKCING_AUTHORIZATION_STATUS !== status) + ).first().subscribe(resolve); + store.dispatch(new CheckAuthorizationStatusAction()); + }); +} + @NgModule({ imports: [ HttpClientModule @@ -36,8 +49,8 @@ export function set_translation_service(appLoadService: AppLoadService) { providers: [ AppLoadService, DataAvailabilityStatesStore, -{ provide: APP_INITIALIZER, useFactory: set_translation_service, deps: [AppLoadService], multi: true }, - { provide: APP_INITIALIZER, useFactory: check_if_authorized, deps: [AppLoadService], multi: true } + { provide: APP_INITIALIZER, useFactory: set_translation_service, deps: [AppLoadService], multi: true }, + { provide: APP_INITIALIZER, useFactory: check_auth_status, deps: [Store], multi: true } ] }) export class AppLoadModule { } diff --git a/ambari-logsearch-web/src/app/modules/app-load/services/app-load.service.ts b/ambari-logsearch-web/src/app/modules/app-load/services/app-load.service.ts index 9405c9b..cc107af 100644 --- a/ambari-logsearch-web/src/app/modules/app-load/services/app-load.service.ts +++ b/ambari-logsearch-web/src/app/modules/app-load/services/app-load.service.ts @@ -17,24 +17,28 @@ */ import { Injectable } from '@angular/core'; -import {Response} from '@angular/http'; +import { Response } from '@angular/http'; import 'rxjs/add/operator/toPromise'; -import {TranslateService} from '@ngx-translate/core'; +import { TranslateService } from '@ngx-translate/core'; -import {AppStateService} from 'app/services/storage/app-state.service'; -import {HttpClientService} from 'app/services/http-client.service'; -import {ClustersService} from 'app/services/storage/clusters.service'; -import {ServiceLogsFieldsService} from 'app/services/storage/service-logs-fields.service'; -import {AuditLogsFieldsService} from 'app/services/storage/audit-logs-fields.service'; -import {AuditFieldsDefinitionSet, LogField} from 'app/classes/object'; -import {Observable} from 'rxjs/Observable'; -import {HostsService} from 'app/services/storage/hosts.service'; -import {NodeItem} from 'app/classes/models/node-item'; -import {ComponentsService} from 'app/services/storage/components.service'; -import {DataAvailabilityValues} from 'app/classes/string'; +import { AppStateService } from 'app/services/storage/app-state.service'; +import { HttpClientService } from 'app/services/http-client.service'; +import { ClustersService } from 'app/services/storage/clusters.service'; +import { ServiceLogsFieldsService } from 'app/services/storage/service-logs-fields.service'; +import { AuditLogsFieldsService } from 'app/services/storage/audit-logs-fields.service'; +import { AuditFieldsDefinitionSet, LogField } from 'app/classes/object'; +import { Observable } from 'rxjs/Observable'; +import { HostsService } from 'app/services/storage/hosts.service'; +import { NodeItem } from 'app/classes/models/node-item'; +import { ComponentsService } from 'app/services/storage/components.service'; +import { DataAvailabilityValues } from 'app/classes/string'; import { DataAvaibilityStatesModel } from '@app/modules/app-load/models/data-availability-state.model'; import { DataAvailabilityStatesStore } from '@app/modules/app-load/stores/data-availability-state.store'; +import { Store } from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import { isAuthorizedSelector } from '@app/store/selectors/auth.selectors'; + // @ToDo create a separate data state enrty in the store with keys of the model names export enum DataStateStoreKeys { CLUSTERS_DATA_KEY = 'clustersDataState', @@ -64,9 +68,10 @@ export class AppLoadService { private translationService: TranslateService, private hostStoreService: HostsService, private componentsStorageService: ComponentsService, - private dataAvaibilityStateStore: DataAvailabilityStatesStore + private dataAvaibilityStateStore: DataAvailabilityStatesStore, + private store: Store<AppStore> ) { - this.appStateService.getParameter('isAuthorized').subscribe(this.initOnAuthorization); + this.store.select(isAuthorizedSelector).subscribe(this.initOnAuthorization); this.appStateService.setParameter('isInitialLoading', true); Observable.combineLatest( @@ -83,11 +88,9 @@ export class AppLoadService { let nextDataState: DataAvailabilityValues = DataAvailabilityValues.NOT_AVAILABLE; if (values.indexOf(DataAvailabilityValues.ERROR) > -1) { nextDataState = DataAvailabilityValues.ERROR; - } - if (values.indexOf(DataAvailabilityValues.LOADING) > -1) { + } else if (values.indexOf(DataAvailabilityValues.LOADING) > -1) { nextDataState = DataAvailabilityValues.LOADING; - } - if ( values.filter((value: DataAvailabilityValues) => value !== DataAvailabilityValues.AVAILABLE).length === 0 ) { + } else if ( values.filter((value: DataAvailabilityValues) => value !== DataAvailabilityValues.AVAILABLE).length === 0 ) { nextDataState = DataAvailabilityValues.AVAILABLE; } return nextDataState; diff --git a/ambari-logsearch-web/src/app/modules/shared/components/dropdown-button/dropdown-button.component.ts b/ambari-logsearch-web/src/app/modules/shared/components/dropdown-button/dropdown-button.component.ts index 534b69d..ab519d0 100644 --- a/ambari-logsearch-web/src/app/modules/shared/components/dropdown-button/dropdown-button.component.ts +++ b/ambari-logsearch-web/src/app/modules/shared/components/dropdown-button/dropdown-button.component.ts @@ -16,9 +16,9 @@ * limitations under the License. */ -import {Component, Input, Output, EventEmitter} from '@angular/core'; -import {ListItem} from '@app/classes/list-item'; -import {UtilsService} from '@app/services/utils.service'; +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { ListItem } from '@app/classes/list-item'; +import { UtilsService } from '@app/services/utils.service'; @Component({ selector: 'dropdown-button', @@ -87,7 +87,9 @@ export class DropdownButtonComponent { return this.showSelectedValue && !this.isMultipleChoice && this.selection.length > 0; } - constructor(protected utils: UtilsService) {} + constructor( + protected utils: UtilsService + ) {} updateSelection(updates: ListItem | ListItem[]): void { if (updates && (!Array.isArray(updates) || updates.length)) { diff --git a/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.html b/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.html index a15b1c3..fac626f 100644 --- a/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.html +++ b/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.html @@ -18,15 +18,15 @@ <li [class.divider]="item.isDivider" [class.filtered]="isFiltered(item)" [attr.role]="item.isDivider ? 'separator' : null" [class]="(item.cssClass || '')"> <ng-container *ngIf="!item.isDivider"> - <label class="list-item-label" *ngIf="isMultipleChoice"> - <input type="checkbox" [attr.id]="item.id || item.value" [(ngModel)]="item.isChecked" + <span class="list-item-label" *ngIf="isMultipleChoice"> + <input type="checkbox" [attr.id]="(instanceId) + '-' + (item.id || item.value)" [(ngModel)]="item.isChecked" (change)="changeSelectedItem({value: item.value, isChecked: $event.currentTarget.checked}, $event)"> - <label [attr.for]="item.id || item.value" class="label-container"> + <label [attr.for]="(instanceId) + '-' + (item.id || item.value)" class="label-container"> <span *ngIf="item.iconClass" [ngClass]="item.iconClass"></span> <span class="item-label-text">{{item.label | translate}}</span> <span #additionalComponent></span> </label> - </label> + </span> <span class="list-item-label label-container" *ngIf="!isMultipleChoice" (click)="changeSelectedItem(item, $event)"> <span *ngIf="item.iconClass" [ngClass]="item.iconClass"></span> <span class="item-label-text">{{item.label | translate}}</span> diff --git a/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.spec.ts b/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.spec.ts index 8b3b13b..6f74de0 100644 --- a/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.spec.ts +++ b/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.spec.ts @@ -49,6 +49,11 @@ import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.serv import {NotificationService} from '@modules/shared/services/notification.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('DropdownListComponent', () => { let component: DropdownListComponent; let fixture: ComponentFixture<DropdownListComponent>; @@ -80,8 +85,11 @@ describe('DropdownListComponent', () => { clusters, components, serviceLogsTruncated, - tabs + tabs, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), FormsModule ], providers: [ diff --git a/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.ts b/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.ts index 651578a..1809637 100644 --- a/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.ts +++ b/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.ts @@ -79,10 +79,14 @@ export class DropdownListComponent implements OnInit, OnChanges, AfterViewChecke private subscriptions: Subscription[] = []; + instanceId: string; + constructor( private componentGenerator: ComponentGeneratorService, private changeDetector: ChangeDetectorRef - ) {} + ) { + this.instanceId = `dropdown-list-${Date.now()}`; + } ngOnInit() { this.separateSelections(); diff --git a/ambari-logsearch-web/src/app/modules/shared/components/filter-dropdown/filter-dropdown.component.spec.ts b/ambari-logsearch-web/src/app/modules/shared/components/filter-dropdown/filter-dropdown.component.spec.ts index 1b081c8..c1bde14 100644 --- a/ambari-logsearch-web/src/app/modules/shared/components/filter-dropdown/filter-dropdown.component.spec.ts +++ b/ambari-logsearch-web/src/app/modules/shared/components/filter-dropdown/filter-dropdown.component.spec.ts @@ -47,6 +47,11 @@ import {RouterTestingModule} from '@angular/router/testing'; import {NotificationService} from '@modules/shared/services/notification.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('FilterDropdownComponent', () => { let component: FilterDropdownComponent; let fixture: ComponentFixture<FilterDropdownComponent>; @@ -93,8 +98,11 @@ describe('FilterDropdownComponent', () => { tabs, clusters, components, - hosts + hosts, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), ...TranslationModules ], providers: [ diff --git a/ambari-logsearch-web/src/app/modules/shared/components/modal-dialog/modal-dialog.component.spec.ts b/ambari-logsearch-web/src/app/modules/shared/components/modal-dialog/modal-dialog.component.spec.ts index 19cd74d..f13872c 100644 --- a/ambari-logsearch-web/src/app/modules/shared/components/modal-dialog/modal-dialog.component.spec.ts +++ b/ambari-logsearch-web/src/app/modules/shared/components/modal-dialog/modal-dialog.component.spec.ts @@ -16,12 +16,21 @@ */ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import {StoreModule} from '@ngrx/store'; +import {AppStateService, appState} from '@app/services/storage/app-state.service'; import { getCommonTestingBedConfiguration, MockHttpRequestModules, TranslationModules } from '@app/test-config.spec'; +import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import {NotificationService} from '@modules/shared/services/notification.service'; + +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + import { ModalDialogComponent } from './modal-dialog.component'; describe('ModalDialogComponent', () => { @@ -31,8 +40,15 @@ describe('ModalDialogComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule(getCommonTestingBedConfiguration({ imports: [ - ...TranslationModules + ...TranslationModules, + StoreModule.provideStore({ + appState, + auth: auth.reducer + }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects) ], + providers: [AppStateService, NotificationsService, NotificationService], declarations: [ ModalDialogComponent ] })) .compileComponents(); diff --git a/ambari-logsearch-web/src/app/modules/shared/interfaces/notification.interface.ts b/ambari-logsearch-web/src/app/modules/shared/interfaces/notification.interface.ts index b8b3d6a..096214c 100644 --- a/ambari-logsearch-web/src/app/modules/shared/interfaces/notification.interface.ts +++ b/ambari-logsearch-web/src/app/modules/shared/interfaces/notification.interface.ts @@ -15,10 +15,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Options} from 'angular2-notifications/src/options.type'; +import { Options } from 'angular2-notifications/src/options.type'; +import { NotificationType } from '@modules/shared/services/notification.service'; export interface NotificationInterface extends Options { - type: string; + type: NotificationType | string; message: string; - title: string; + title?: string; } diff --git a/ambari-logsearch-web/src/app/modules/shared/services/notification.service.ts b/ambari-logsearch-web/src/app/modules/shared/services/notification.service.ts index df6ca2a..ef340bd 100644 --- a/ambari-logsearch-web/src/app/modules/shared/services/notification.service.ts +++ b/ambari-logsearch-web/src/app/modules/shared/services/notification.service.ts @@ -70,7 +70,7 @@ export class NotificationService { } const icon = notificationIcons[method] || notificationIcons['info']; const htmlMsg = messageTemplate - .replace(/{{title}}/gi, this.translateService.instant(title)) + .replace(/{{title}}/gi, title ? this.translateService.instant(title) : '') .replace(/{{message}}/gi, this.translateService.instant(message)) .replace(/{{icon}}/gi, icon); return this.notificationService.html(htmlMsg, method, {icon, ...config}); diff --git a/ambari-logsearch-web/src/app/modules/shared/variables.less b/ambari-logsearch-web/src/app/modules/shared/variables.less index 7ffd20c..b917527 100644 --- a/ambari-logsearch-web/src/app/modules/shared/variables.less +++ b/ambari-logsearch-web/src/app/modules/shared/variables.less @@ -19,6 +19,7 @@ // Variables @blue: #1491C1; @grey: #DDD; +@white: rgba(255, 255, 255, 1); @fluid-gray-1: #ccc; @fluid-gray-2: #999; @@ -39,7 +40,7 @@ @grey-color: #DDD; @default-line-height: 1.42857143; @main-background-color: #ECECEC; -@filters-panel-background-color: #FFF; +@filters-panel-background-color: @white; @filters-panel-padding: 10px 0; @list-header-background-color: #F2F2F2; @checkbox-top: 4px; diff --git a/ambari-logsearch-web/src/app/services/auth-guard.service.ts b/ambari-logsearch-web/src/app/services/auth-guard.service.ts index 8b56239..f8f8e5c 100644 --- a/ambari-logsearch-web/src/app/services/auth-guard.service.ts +++ b/ambari-logsearch-web/src/app/services/auth-guard.service.ts @@ -22,16 +22,24 @@ import {Observable} from 'rxjs/Observable'; import {AuthService} from '@app/services/auth.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import { isAuthorizedSelector } from '@app/store/selectors/auth.selectors'; + /** * This guard goal is to prevent to display screens where authorization needs. */ @Injectable() export class AuthGuardService implements CanActivate { - constructor(private authService: AuthService, private router: Router) {} + constructor( + private authService: AuthService, + private router: Router, + private store: Store<AppStore> + ) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { - return this.authService.isAuthorized().map((isAuthorized: boolean) => { + return this.store.select(isAuthorizedSelector).map((isAuthorized: boolean) => { this.authService.redirectUrl = state.url; if (!isAuthorized) { this.router.navigate(['/login']); diff --git a/ambari-logsearch-web/src/app/services/auth.service.spec.ts b/ambari-logsearch-web/src/app/services/auth.service.spec.ts index 74a7125..0d30281 100644 --- a/ambari-logsearch-web/src/app/services/auth.service.spec.ts +++ b/ambari-logsearch-web/src/app/services/auth.service.spec.ts @@ -23,6 +23,8 @@ import 'rxjs/add/operator/first'; import 'rxjs/add/operator/last'; import 'rxjs/add/operator/take'; import {StoreModule} from '@ngrx/store'; +import {Store} from '@ngrx/store'; +import {AppStore} from '@app/classes/models/store'; import {AppStateService, appState} from '@app/services/storage/app-state.service'; import {AuthService} from '@app/services/auth.service'; import {HttpClientService} from '@app/services/http-client.service'; @@ -30,6 +32,11 @@ import {RouterTestingModule} from '@angular/router/testing'; import {Routes} from '@angular/router'; import {Component} from '@angular/core'; + +import * as auth from '@app/store/reducers/auth.reducers'; +import { isAuthorizedSelector } from '@app/store/selectors/auth.selectors'; +import { LogInAction, LogOutAction, AuthorizedAction } from '@app/store/actions/auth.actions'; + describe('AuthService', () => { const successResponse = { @@ -41,8 +48,8 @@ describe('AuthService', () => { bytesLoaded: 100, totalBytes: 100, headers: null - }, - errorResponse = { + }; + const errorResponse = { type: 'error', ok: false, url: '/', @@ -54,11 +61,20 @@ describe('AuthService', () => { }; // Note: We add delay to help the isLoginInProgress test case. - let httpServiceStub = { + const httpServiceStub = { isError: false, - postFormData: function () { + request: function () { const isError = this.isError; return Observable.create(observer => observer.next(isError ? errorResponse : successResponse)).delay(1); + }, + postFormData: function () { + return this.request(); + }, + post: function () { + return this.request(); + }, + get: function () { + return this.request(); } }; @@ -74,7 +90,8 @@ describe('AuthService', () => { imports: [ HttpModule, StoreModule.provideStore({ - appState + appState, + auth: auth.reducer }), RouterTestingModule.withRoutes(testRoutes) ], @@ -90,54 +107,27 @@ describe('AuthService', () => { expect(service).toBeTruthy(); })); - it('should set the isAuthorized state to true in appState when the login is success', async(inject( - [AuthService, AppStateService, HttpClientService], - (authService: AuthService, appStateService: AppStateService, httpClientService) => { - httpClientService.isError = false; - authService.login('test', 'test') - .subscribe(() => { - appStateService.getParameter('isAuthorized').subscribe((value: Boolean): void => { - expect(value).toBe(true); - }); - }, value => { - throw value; - }); - } - ))); - - - it('should set the isAuthorized state to false in appState when the login is failed', async(inject( - [AuthService, AppStateService, HttpClientService], - (authService: AuthService, appStateService: AppStateService, httpClientService) => { - httpClientService.isError = true; - authService.login('test', 'test') - .subscribe(() => { - appStateService.getParameter('isAuthorized').subscribe((value: Boolean): void => { - expect(value).toBe(false); - }); - }); + it('should return with Observable<Response> when login called', async(inject( + [AuthService, Store], + (authService: AuthService) => { + const response = authService.login('test', 'test'); + expect(response instanceof Observable).toBe(true); } ))); - it('should set the isLoginInProgress state to true when the login started', async(inject( - [AuthService, AppStateService, HttpClientService], - (authService: AuthService, appStateService: AppStateService, httpClientService) => { - httpClientService.isError = false; - authService.login('test', 'test'); - appStateService.getParameter('isLoginInProgress').first().subscribe((value: Boolean): void => { - expect(value).toBe(true); - }); + it('should return with Observable<Response> when logout called', async(inject( + [AuthService, Store], + (authService: AuthService) => { + const response = authService.logout(); + expect(response instanceof Observable).toBe(true); } ))); - it('should set the isLoginInProgress state to true after the login is success', async(inject( - [AuthService, AppStateService, HttpClientService], - (authService: AuthService, appStateService: AppStateService, httpClientService) => { - httpClientService.isError = false; - authService.login('test', 'test'); - appStateService.getParameter('isLoginInProgress').take(2).last().subscribe((value: Boolean): void => { - expect(value).toBe(false); - }); + it('should return with Observable<Response> when checkAuthorizationState called', async(inject( + [AuthService, Store], + (authService: AuthService) => { + const response = authService.checkAuthorizationState(); + expect(response instanceof Observable).toBe(true); } ))); diff --git a/ambari-logsearch-web/src/app/services/auth.service.ts b/ambari-logsearch-web/src/app/services/auth.service.ts index 1bf1875..c7c74ba 100644 --- a/ambari-logsearch-web/src/app/services/auth.service.ts +++ b/ambari-logsearch-web/src/app/services/auth.service.ts @@ -16,19 +16,14 @@ * limitations under the License. */ -import {Injectable} from '@angular/core'; -import {Response} from '@angular/http'; +import { Injectable } from '@angular/core'; +import { Response } from '@angular/http'; -import {Observable} from 'rxjs/Observable'; +import { Observable } from 'rxjs/Observable'; -import {HttpClientService} from '@app/services/http-client.service'; -import {AppStateService} from '@app/services/storage/app-state.service'; -import {Router} from '@angular/router'; -import {Subscription} from 'rxjs/Subscription'; -import { Observer } from 'rxjs/Observer'; - -export const IS_AUTHORIZED_APP_STATE_KEY: string = 'isAuthorized'; -export const IS_LOGIN_IN_PROGRESS_APP_STATE_KEY: string = 'isLoginInProgress'; +import { HttpClientService } from '@app/services/http-client.service'; +import { AppStateService } from '@app/services/storage/app-state.service'; +import { Subscription } from 'rxjs/Subscription'; /** * This service meant to be a single place where the authorization should happen. @@ -47,140 +42,32 @@ export class AuthService { constructor( private httpClient: HttpClientService, - private appState: AppStateService, - private router: Router - ) { - this.subscriptions.push(this.appState.getParameter(IS_AUTHORIZED_APP_STATE_KEY).subscribe( - this.onAppStateIsAuthorizedChanged - )); - } + private appState: AppStateService + ) {} - onAppStateIsAuthorizedChanged = (isAuthorized): void => { - if (isAuthorized) { - const redirectTo = this.redirectUrl || (this.router.routerState.snapshot.url === '/login' ? '/' : null); - if (redirectTo) { - if (Array.isArray(redirectTo)) { - this.router.navigate(redirectTo); - } else { - this.router.navigateByUrl(redirectTo); - } - } - this.redirectUrl = ''; - } else { - this.router.navigate(['/login']); - } - } /** * The single entry point to request a login action. * @param {string} username * @param {string} password * @returns {Observable<Response>} */ - login(username: string, password: string): Observable<Boolean> { - this.setLoginInProgressAppState(true); - const response$ = this.httpClient.postFormData('login', { + login(username: string, password: string): Observable<Response> { + return this.httpClient.postFormData('login', { username: username, password: password }); - response$.subscribe( - (resp: Response) => this.onLoginResponse(resp), - (resp: Response) => this.onLoginError(resp) - ); - return response$.switchMap((resp: Response) => { - return Observable.create((observer: Observer<boolean>) => { - if (resp.ok) { - observer.next(resp.ok); - } else { - observer.error(resp); - } - observer.complete(); - }); - }); } /** * The single unique entry point to request a logout action - * @returns {Observable<boolean | Error>} - */ - logout(): Observable<Boolean> { - const response$ = this.httpClient.get('logout'); - response$.subscribe( - (resp: Response) => this.onLogoutResponse(resp), - (resp: Response) => this.onLogoutError(resp) - ); - return response$.switchMap((resp: Response) => { - return Observable.create((observer) => { - if (resp.ok) { - observer.next(resp.ok); - } else { - observer.error(resp); - } - observer.complete(); - }); - }); - } - - /** - * Set the isLoginInProgress state in AppState. The reason behind create a function for this is that we set this app - * state from two different places so let's do always the same way. - * @param {boolean} state the new value of the isLoginInProgress app state. - */ - private setLoginInProgressAppState(state: boolean) { - this.appState.setParameter(IS_LOGIN_IN_PROGRESS_APP_STATE_KEY, state); - } - - /** - * Set the isAuthorized state in AppState. The reason behind create a function for this is that we set this app - * state from two different places so let's do always the same way. - * @param {boolean} state The new value of the isAuthorized app state. - */ - private setAuthorizedAppState(state: boolean) { - this.appState.setParameter(IS_AUTHORIZED_APP_STATE_KEY, state); - } - - /** - * Handling the login success response. The goal is to set the authorized property of the appState. - * @param resp - */ - private onLoginResponse(resp: Response): void { - this.setLoginInProgressAppState(false); - if (resp && resp.ok) { - this.setAuthorizedAppState(resp.ok); - } - } - - /** - * Handling the login error response. The goal is to set the authorized property correctly of the appState. - * @ToDo decide if we should have a loginError app state. - * @param {Reponse} resp - */ - private onLoginError(resp: Response): void { - this.setLoginInProgressAppState(false); - this.setAuthorizedAppState(false); - } - - /** - * Handling the logout success response. The goal is to set the authorized property of the appState. - * @param {Response} resp + * @returns {Observable<Response>} */ - private onLogoutResponse(resp: Response): void { - if (resp && resp.ok) { - this.setAuthorizedAppState(false); - } + logout(): Observable<Response> { + return this.httpClient.get('logout'); } - /** - * Handling the logout error response. - * @ToDo decide if we should create a logoutError app state or not - * @param {Response} resp - */ - private onLogoutError(resp: Response): void {} - - /** - * Simply return with the boolean value of the isAuthorized application state key. - */ - public isAuthorized(): Observable<boolean> { - return this.appState.getParameter(IS_AUTHORIZED_APP_STATE_KEY); + checkAuthorizationState(): Observable<Response> { + return this.httpClient.get('status'); } } diff --git a/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts b/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts index e5584e3..989994c 100644 --- a/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts +++ b/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts @@ -47,6 +47,12 @@ import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.serv import {NotificationsService} from 'angular2-notifications/src/notifications.service'; import {NotificationService} from '@modules/shared/services/notification.service'; +import { AuthService } from '@app/services/auth.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('ComponentGeneratorService', () => { beforeEach(() => { TestBed.configureTestingModule({ @@ -65,8 +71,11 @@ describe('ComponentGeneratorService', () => { clusters, components, serviceLogsTruncated, - tabs + tabs, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), ...TranslationModules ], providers: [ @@ -93,7 +102,8 @@ describe('ComponentGeneratorService', () => { LogsFilteringUtilsService, LogsStateService, NotificationsService, - NotificationService + NotificationService, + AuthService ] }); }); diff --git a/ambari-logsearch-web/src/app/services/history-manager.service.spec.ts b/ambari-logsearch-web/src/app/services/history-manager.service.spec.ts index ccfe611..70f05ad 100644 --- a/ambari-logsearch-web/src/app/services/history-manager.service.spec.ts +++ b/ambari-logsearch-web/src/app/services/history-manager.service.spec.ts @@ -47,6 +47,12 @@ import {LogsStateService} from '@app/services/storage/logs-state.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; import {NotificationService} from '@modules/shared/services/notification.service'; +import { AuthService } from '@app/services/auth.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('HistoryManagerService', () => { beforeEach(() => { @@ -67,8 +73,11 @@ describe('HistoryManagerService', () => { components, hosts, serviceLogsTruncated, - tabs - }) + tabs, + auth: auth.reducer + }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects) ], providers: [ ...MockHttpRequestModules, @@ -93,7 +102,8 @@ describe('HistoryManagerService', () => { LogsFilteringUtilsService, LogsStateService, NotificationsService, - NotificationService + NotificationService, + AuthService ] }); }); diff --git a/ambari-logsearch-web/src/app/services/http-client.service.ts b/ambari-logsearch-web/src/app/services/http-client.service.ts index c65278b..2d68977 100644 --- a/ambari-logsearch-web/src/app/services/http-client.service.ts +++ b/ambari-logsearch-web/src/app/services/http-client.service.ts @@ -32,6 +32,11 @@ import {ServiceLogsHistogramQueryParams} from '@app/classes/queries/service-logs import {ServiceLogsTruncatedQueryParams} from '@app/classes/queries/service-logs-truncated-query-params'; import {AppStateService} from '@app/services/storage/app-state.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import { HttpAuthorizationErrorResponseAction } from '@app/store/actions/auth.actions'; +import { isAuthorizedSelector } from '@app/store/selectors/auth.selectors'; + @Injectable() export class HttpClientService extends Http { @@ -100,7 +105,12 @@ export class HttpClientService extends Http { private readonly unauthorizedStatuses = [401, 403, 419]; - constructor(backend: XHRBackend, defaultOptions: RequestOptions, private appState: AppStateService) { + constructor( + backend: XHRBackend, + defaultOptions: RequestOptions, + private appState: AppStateService, + private store: Store<AppStore> + ) { super(backend, defaultOptions); } @@ -166,7 +176,11 @@ export class HttpClientService extends Http { const handleResponseError = (error) => { let handled = false; if (this.unauthorizedStatuses.indexOf(error.status) > -1) { - this.appState.setParameter('isAuthorized', false); + this.store.select(isAuthorizedSelector).first().subscribe((isAuthorized: boolean) => { + if (isAuthorized) { + this.store.dispatch(new HttpAuthorizationErrorResponseAction({response: error})); + } + }); handled = true; } return handled; diff --git a/ambari-logsearch-web/src/app/services/log-index-filter.service.spec.ts b/ambari-logsearch-web/src/app/services/log-index-filter.service.spec.ts index 924deee..eb4bf66 100644 --- a/ambari-logsearch-web/src/app/services/log-index-filter.service.spec.ts +++ b/ambari-logsearch-web/src/app/services/log-index-filter.service.spec.ts @@ -24,6 +24,9 @@ import { import { AppStateService } from '@app/services/storage/app-state.service'; +import {NotificationService} from '@modules/shared/services/notification.service'; +import {NotificationsService} from 'angular2-notifications/src/notifications.service'; + import { LogIndexFilterService } from './log-index-filter.service'; describe('LogIndexFilterService', () => { @@ -34,7 +37,9 @@ describe('LogIndexFilterService', () => { ], providers: [ AppStateService, - LogIndexFilterService + LogIndexFilterService, + NotificationService, + NotificationsService ] })); }); diff --git a/ambari-logsearch-web/src/app/services/login-screen-guard.service.ts b/ambari-logsearch-web/src/app/services/login-screen-guard.service.ts index 8dbe1d7..99e886e 100644 --- a/ambari-logsearch-web/src/app/services/login-screen-guard.service.ts +++ b/ambari-logsearch-web/src/app/services/login-screen-guard.service.ts @@ -22,16 +22,23 @@ import {Observable} from 'rxjs/Observable'; import {AuthService} from '@app/services/auth.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import { isAuthorizedSelector } from '@app/store/selectors/auth.selectors'; + /** * The goal of this guard service is to prevent to display the login screen when the user is logged in. */ @Injectable() export class LoginScreenGuardService implements CanActivate { - constructor(private authService: AuthService, private router: Router) {} + constructor( + private router: Router, + private store: Store<AppStore> + ) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { - return this.authService.isAuthorized().map((isAuthorized: boolean) => { + return this.store.select(isAuthorizedSelector).map((isAuthorized: boolean) => { if (isAuthorized && state.url === '/login') { this.router.navigate(['/']); } diff --git a/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts b/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts index 3e70644..ee3f5da 100644 --- a/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts +++ b/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts @@ -45,6 +45,12 @@ import {LogsStateService} from '@app/services/storage/logs-state.service'; import {NotificationService} from '@modules/shared/services/notification.service'; import {NotificationsService} from 'angular2-notifications/src/notifications.service'; +import { AuthService } from '@app/services/auth.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + describe('LogsContainerService', () => { beforeEach(() => { TestBed.configureTestingModule({ @@ -63,8 +69,11 @@ describe('LogsContainerService', () => { components, hosts, serviceLogsTruncated, - tabs + tabs, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), ...TranslationModules ], providers: [ @@ -89,7 +98,8 @@ describe('LogsContainerService', () => { LogsFilteringUtilsService, LogsStateService, NotificationsService, - NotificationService + NotificationService, + AuthService ] }); }); diff --git a/ambari-logsearch-web/src/app/services/logs-container.service.ts b/ambari-logsearch-web/src/app/services/logs-container.service.ts index 9fba951..d550fbb 100644 --- a/ambari-logsearch-web/src/app/services/logs-container.service.ts +++ b/ambari-logsearch-web/src/app/services/logs-container.service.ts @@ -16,11 +16,11 @@ * limitations under the License. */ -import {Injectable} from '@angular/core'; -import {FormGroup, FormControl} from '@angular/forms'; -import {Response} from '@angular/http'; -import {Subject} from 'rxjs/Subject'; -import {Observable} from 'rxjs/Observable'; +import { Injectable } from '@angular/core'; +import { FormGroup, FormControl } from '@angular/forms'; +import { Response } from '@angular/http'; +import { Subject } from 'rxjs/Subject'; +import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/timer'; import 'rxjs/add/observable/combineLatest'; import 'rxjs/add/operator/distinctUntilChanged'; @@ -28,42 +28,46 @@ import 'rxjs/add/operator/first'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/takeUntil'; import * as moment from 'moment-timezone'; -import {HttpClientService} from '@app/services/http-client.service'; -import {UtilsService} from '@app/services/utils.service'; -import {AuditLogsService} from '@app/services/storage/audit-logs.service'; -import {AuditLogsFieldsService, ResponseRootProperties} from '@app/services/storage/audit-logs-fields.service'; -import {AuditLogsGraphDataService} from '@app/services/storage/audit-logs-graph-data.service'; -import {ServiceLogsService} from '@app/services/storage/service-logs.service'; -import {ServiceLogsFieldsService} from '@app/services/storage/service-logs-fields.service'; -import {ServiceLogsHistogramDataService} from '@app/services/storage/service-logs-histogram-data.service'; -import {ServiceLogsTruncatedService} from '@app/services/storage/service-logs-truncated.service'; -import {AppStateService} from '@app/services/storage/app-state.service'; -import {AppSettingsService} from '@app/services/storage/app-settings.service'; -import {TabsService} from '@app/services/storage/tabs.service'; -import {ClustersService} from '@app/services/storage/clusters.service'; -import {ComponentsService} from '@app/services/storage/components.service'; -import {HostsService} from '@app/services/storage/hosts.service'; -import {ActiveServiceLogEntry} from '@app/classes/active-service-log-entry'; +import { HttpClientService } from '@app/services/http-client.service'; +import { UtilsService } from '@app/services/utils.service'; +import { AuditLogsService } from '@app/services/storage/audit-logs.service'; +import { AuditLogsFieldsService, ResponseRootProperties } from '@app/services/storage/audit-logs-fields.service'; +import { AuditLogsGraphDataService } from '@app/services/storage/audit-logs-graph-data.service'; +import { ServiceLogsService } from '@app/services/storage/service-logs.service'; +import { ServiceLogsFieldsService } from '@app/services/storage/service-logs-fields.service'; +import { ServiceLogsHistogramDataService } from '@app/services/storage/service-logs-histogram-data.service'; +import { ServiceLogsTruncatedService } from '@app/services/storage/service-logs-truncated.service'; +import { AppStateService } from '@app/services/storage/app-state.service'; +import { AppSettingsService } from '@app/services/storage/app-settings.service'; +import { TabsService } from '@app/services/storage/tabs.service'; +import { ClustersService } from '@app/services/storage/clusters.service'; +import { ComponentsService } from '@app/services/storage/components.service'; +import { HostsService } from '@app/services/storage/hosts.service'; +import { ActiveServiceLogEntry } from '@app/classes/active-service-log-entry'; import { FilterCondition, TimeUnitListItem, SearchBoxParameter, SearchBoxParameterTriggered } from '@app/classes/filtering'; -import {ListItem} from '@app/classes/list-item'; -import {HomogeneousObject, LogLevelObject} from '@app/classes/object'; -import {DataAvailability, DataAvailabilityValues, LogsType, ScrollType} from '@app/classes/string'; -import {LogTypeTab} from '@app/classes/models/log-type-tab'; -import {AuditFieldsDefinitionSet} from '@app/classes/object'; -import {AuditLog} from '@app/classes/models/audit-log'; -import {ServiceLog} from '@app/classes/models/service-log'; -import {BarGraph} from '@app/classes/models/bar-graph'; -import {NodeItem} from '@app/classes/models/node-item'; -import {CommonEntry} from '@app/classes/models/common-entry'; -import {ClusterSelectionService} from '@app/services/storage/cluster-selection.service'; -import {ActivatedRoute, Router} from '@angular/router'; -import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.service'; -import {BehaviorSubject} from 'rxjs/BehaviorSubject'; -import {LogsStateService} from '@app/services/storage/logs-state.service'; -import {LogLevelComponent} from '@app/components/log-level/log-level.component'; -import {NotificationService, NotificationType} from '@modules/shared/services/notification.service'; +import { ListItem } from '@app/classes/list-item'; +import { HomogeneousObject, LogLevelObject } from '@app/classes/object'; +import { DataAvailabilityValues, LogsType, ScrollType } from '@app/classes/string'; +import { LogTypeTab } from '@app/classes/models/log-type-tab'; +import { AuditFieldsDefinitionSet } from '@app/classes/object'; +import { AuditLog } from '@app/classes/models/audit-log'; +import { ServiceLog } from '@app/classes/models/service-log'; +import { BarGraph } from '@app/classes/models/bar-graph'; +import { NodeItem } from '@app/classes/models/node-item'; +import { CommonEntry } from '@app/classes/models/common-entry'; +import { ClusterSelectionService } from '@app/services/storage/cluster-selection.service'; +import { Router } from '@angular/router'; +import { LogsFilteringUtilsService } from '@app/services/logs-filtering-utils.service'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { LogsStateService } from '@app/services/storage/logs-state.service'; +import { LogLevelComponent } from '@app/components/log-level/log-level.component'; +import { NotificationService, NotificationType } from '@modules/shared/services/notification.service'; + +import { Store } from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import { isAuthorizedSelector } from '@app/store/selectors/auth.selectors'; @Injectable() export class LogsContainerService { @@ -377,11 +381,11 @@ export class LogsContainerService { private serviceLogsTruncatedStorage: ServiceLogsTruncatedService, private appSettings: AppSettingsService, private clusterSelectionStoreService: ClusterSelectionService, private router: Router, - private activatedRoute: ActivatedRoute, private logsFilteringUtilsService: LogsFilteringUtilsService, private logsStateService: LogsStateService, private notificationService: NotificationService, - private componentsService: ComponentsService + private componentsService: ComponentsService, + private store: Store<AppStore> ) { const formItems = Object.keys(this.filters).reduce((currentObject: any, key: string): HomogeneousObject<FormControl> => { const formControl = new FormControl(); @@ -402,10 +406,18 @@ export class LogsContainerService { appState.getParameter('activeLogsType').subscribe((value: LogsType) => { if (this.isLogsTypeSupported(value)) { this.activeLogsType = value; - this.loadLogs(this.activeLogsType); } }); + Observable.combineLatest( + this.store.select(isAuthorizedSelector), + this.appState.getParameter('baseDataSetState') + .map((dataSetState: DataAvailabilityValues) => dataSetState === DataAvailabilityValues.AVAILABLE), + appState.getParameter('activeLogsType') + ).filter(([isAuthorized, dataAvailable, activeLogsType]) => isAuthorized && dataAvailable) + .map(([isAuthorized, dataAvailable, activeLogsType]) => activeLogsType) + .subscribe(this.loadLogs); + appSettings.getParameter('timeZone').subscribe((value: string) => this.timeZone = value || this.defaultTimeZone); tabsStorage.mapCollection((tab: LogTypeTab): LogTypeTab => { return Object.assign({}, tab, { @@ -445,7 +457,7 @@ export class LogsContainerService { resetFiltersForms(filters): void { this.appState.getParameter('baseDataSetState') // do it only when the base data set is available so that the dropdowns can set the selections - .filter((dataSetState: DataAvailability) => dataSetState === DataAvailabilityValues.AVAILABLE) + .filter((dataSetState: DataAvailabilityValues) => dataSetState === DataAvailabilityValues.AVAILABLE) .first() .subscribe(() => { this.filtersFormSyncInProgress.next(true); @@ -510,7 +522,7 @@ export class LogsContainerService { */ setActiveTabById(tabId: string): void { this.tabsStorage.findInCollection((tab: LogTypeTab) => tab.id === tabId).first().subscribe((tab: LogTypeTab | null) => { - if (tab) { + if (tab && !tab.isActive) { this.switchTab(tab); this.logsStateService.setParameter('activeTabId', tabId); } diff --git a/ambari-logsearch-web/src/app/services/storage/reducers.service.ts b/ambari-logsearch-web/src/app/services/storage/reducers.service.ts index cd67461..1d18406 100644 --- a/ambari-logsearch-web/src/app/services/storage/reducers.service.ts +++ b/ambari-logsearch-web/src/app/services/storage/reducers.service.ts @@ -36,6 +36,8 @@ import {clusterSelections} from '@app/services/storage/cluster-selection.service import {logsState} from '@app/services/storage/logs-state.service'; import {dataAvailabilityStates} from '@app/modules/app-load/stores/data-availability-state.store'; +import * as auth from '@app/store/reducers/auth.reducers'; + export const reducers = { appSettings, appState, @@ -54,7 +56,8 @@ export const reducers = { tabs, clusterSelections, logsState, - dataAvailabilityStates + dataAvailabilityStates, + auth: auth.reducer }; export function reducer(state: any, action: any) { diff --git a/ambari-logsearch-web/src/app/services/user-settings.service.spec.ts b/ambari-logsearch-web/src/app/services/user-settings.service.spec.ts index 8dce161..953d7fe 100644 --- a/ambari-logsearch-web/src/app/services/user-settings.service.spec.ts +++ b/ambari-logsearch-web/src/app/services/user-settings.service.spec.ts @@ -48,6 +48,11 @@ import {NotificationService} from '@modules/shared/services/notification.service import { dataAvailabilityStates, DataAvailabilityStatesStore } from '@app/modules/app-load/stores/data-availability-state.store'; +import { AuthService } from '@app/services/auth.service'; +import * as auth from '@app/store/reducers/auth.reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; + describe('UserSettingsService', () => { beforeEach(() => { TestBed.configureTestingModule({ @@ -67,8 +72,10 @@ describe('UserSettingsService', () => { hosts, serviceLogsTruncated, tabs, - dataAvailabilityStates + dataAvailabilityStates, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), ...TranslationModules ], providers: [ @@ -95,7 +102,8 @@ describe('UserSettingsService', () => { LogsStateService, NotificationsService, NotificationService, - DataAvailabilityStatesStore + DataAvailabilityStatesStore, + AuthService ] }); }); diff --git a/ambari-logsearch-web/src/app/store/actions/auth.actions.ts b/ambari-logsearch-web/src/app/store/actions/auth.actions.ts new file mode 100644 index 0000000..4bfc7ca --- /dev/null +++ b/ambari-logsearch-web/src/app/store/actions/auth.actions.ts @@ -0,0 +1,101 @@ +/** + * 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 { Response } from '@angular/http'; +import { Action } from '@ngrx/store'; + + +export enum AuthActionTypes { + LOGIN = '[Auth] Login', + AUTHORIZED = '[Auth] Authorized', + UNAUTHORIZED = '[Auth] Unauthorized', + AUTHORIZATION_ERROR = '[Auth] Authorization Error', + FORBIDDEN = '[Auth] Forbidden', + LOGOUT = '[Auth] Logout', + LOGGED_OUT = '[Auth] Logged out', + LOGOUT_ERROR = '[Auth] Logout error', + CHECK_AUTHORIZATION_STATUS = '[Auth] Check status', + AUTHORIZATION_TIMEOUT = '[Auth] Authorization Timeout', + HTTP_AUTHORIZATION_ERROR_RESPONSE = '[Auth] HTTP Authorization Error Response' +} + +export class LogInAction implements Action { + readonly type = AuthActionTypes.LOGIN; + constructor(public payload: any) {} +} + +export class LogOutAction implements Action { + readonly type = AuthActionTypes.LOGOUT; + constructor() {} +} + +export class AuthorizedAction implements Action { + readonly type = AuthActionTypes.AUTHORIZED; + constructor(public payload: any) {} +} + +export class UnauthorizedAction implements Action { + readonly type = AuthActionTypes.UNAUTHORIZED; + constructor(public payload: any) {} +} + +export class AuthorizationErrorAction implements Action { + readonly type = AuthActionTypes.AUTHORIZATION_ERROR; + constructor(public payload: any) {} +} + +export class ForbiddenAction implements Action { + readonly type = AuthActionTypes.FORBIDDEN; + constructor(public payload: {response?: Response, [key: string]: any}) {} +} + +export class LoggedOutAction implements Action { + readonly type = AuthActionTypes.LOGGED_OUT; + constructor(public payload?: any) {} +} + +export class LogoutErrorAction implements Action { + readonly type = AuthActionTypes.LOGOUT_ERROR; + constructor(public payload: any) {} +} + +export class CheckAuthorizationStatusAction implements Action { + readonly type = AuthActionTypes.CHECK_AUTHORIZATION_STATUS; + constructor() {} +} + +export class AuthorizationTimeoutAction implements Action { + readonly type = AuthActionTypes.AUTHORIZATION_TIMEOUT; + constructor(public payload: any) {} +} + +export class HttpAuthorizationErrorResponseAction implements Action { + readonly type = AuthActionTypes.HTTP_AUTHORIZATION_ERROR_RESPONSE; + constructor(public payload: {response: Response}) {} +} + +export type AuthActions = + | LogInAction + | LogOutAction + | AuthorizedAction + | UnauthorizedAction + | ForbiddenAction + | LoggedOutAction + | LogoutErrorAction + | HttpAuthorizationErrorResponseAction + | CheckAuthorizationStatusAction; diff --git a/ambari-logsearch-web/src/app/modules/shared/interfaces/notification.interface.ts b/ambari-logsearch-web/src/app/store/actions/notification.actions.ts similarity index 64% copy from ambari-logsearch-web/src/app/modules/shared/interfaces/notification.interface.ts copy to ambari-logsearch-web/src/app/store/actions/notification.actions.ts index b8b3d6a..c807343 100644 --- a/ambari-logsearch-web/src/app/modules/shared/interfaces/notification.interface.ts +++ b/ambari-logsearch-web/src/app/store/actions/notification.actions.ts @@ -15,10 +15,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Options} from 'angular2-notifications/src/options.type'; -export interface NotificationInterface extends Options { - type: string; - message: string; - title: string; +import { Action } from '@ngrx/store'; + +import { NotificationInterface } from '@modules/shared/interfaces/notification.interface'; + +export enum NotificationActionTypes { + ADD_NOTIFICATION = '[Notification] Add' } + +export class AddNotificationAction implements Action { + readonly type = NotificationActionTypes.ADD_NOTIFICATION; + constructor(public payload: NotificationInterface) {} +} + +export type NotificationActions = + | AddNotificationAction; diff --git a/ambari-logsearch-web/src/app/store/effects/auth.effects.ts b/ambari-logsearch-web/src/app/store/effects/auth.effects.ts new file mode 100644 index 0000000..99d9bbb --- /dev/null +++ b/ambari-logsearch-web/src/app/store/effects/auth.effects.ts @@ -0,0 +1,198 @@ +/** + * 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 { Response } from '@angular/http'; +import { Actions, Effect } from '@ngrx/effects'; +import { Observable } from 'rxjs/Observable'; +import { Router } from '@angular/router'; + +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/catch'; + +import { AuthService } from '@app/services/auth.service'; +import { + AuthActionTypes, + LogInAction, + AuthorizedAction, + UnauthorizedAction, + AuthorizationErrorAction, + AuthorizationTimeoutAction, + ForbiddenAction, + LoggedOutAction, + LogoutErrorAction, + HttpAuthorizationErrorResponseAction +} from '../actions/auth.actions'; + +import { AddNotificationAction } from '@app/store/actions/notification.actions'; +import { NotificationType } from '@modules/shared/services/notification.service'; + + +@Injectable() +export class AuthEffects { + + @Effect() + CheckAuthorizationStatus: Observable<any> = this.actions$ + .ofType(AuthActionTypes.CHECK_AUTHORIZATION_STATUS) + .switchMap(() => { + return this.authService.checkAuthorizationState() + .map((response: Response) => { + return response.ok ? new AuthorizedAction({response}) : new UnauthorizedAction({response}); + }) + .catch((error) => { + return Observable.of(new UnauthorizedAction({error})); + }); + }); + + @Effect() + LogIn: Observable<any> = this.actions$ + .ofType(AuthActionTypes.LOGIN) + .map((action: LogInAction) => action.payload) + .switchMap(payload => { + return this.authService.login(payload.username, payload.password) + .map((response: Response) => { + let message = ''; + switch (response.status) { + case 401 : + message = 'authorization.error.unauthorized'; + break; + case 403 : + message = 'authorization.error.forbidden'; + break; + case 419 : + message = 'authorization.error.authorizationTimeout'; + break; + } + const nextPayload = { message, response, user: {username: payload.username} }; + return response.ok ? new AuthorizedAction(nextPayload) : ( + response.status === 401 ? new UnauthorizedAction(nextPayload) : ( + response.status === 403 ? new ForbiddenAction(nextPayload) : new AuthorizationErrorAction(nextPayload) + ) + ); + }) + .catch((error) => { + return Observable.of(new AuthorizationErrorAction({error: error})); + }); + }); + + @Effect({ dispatch: false }) + Authorized: Observable<any> = this.actions$ + .ofType(AuthActionTypes.AUTHORIZED) + .do(() => { + if (this.router.url === '/login') { + const url = this.authService.redirectUrl || '/'; + if (typeof url === 'string') { + this.router.navigateByUrl(url); + } else if (Array.isArray(url)) { + this.router.navigate(url); + } + } + }); + + @Effect() + LogOut: Observable<any> = this.actions$ + .ofType(AuthActionTypes.LOGOUT) + .switchMap(payload => { + return this.authService.logout() + .map((response: Response) => { + return response.ok ? new LoggedOutAction() : new LogoutErrorAction({response}); + }) + .catch((error) => { + return Observable.of(new LogoutErrorAction({error: error})); + }); + }); + + @Effect({ dispatch: false }) + LoggedOut: Observable<any> = this.actions$ + .ofType(AuthActionTypes.LOGGED_OUT) + .do(() => { + window.location.reload(true); + }); + + @Effect() + AuthorizationError: Observable<any> = this.actions$ + .ofType(AuthActionTypes.AUTHORIZATION_ERROR) + .map((action: AuthorizationErrorAction) => action.payload) + .switchMap((payload) => { + const response: Response = payload.response; + let message = 'authorization.error.authorizationError'; + if (response) { + const body = response.json(); + message = body.message || message; + } + return Observable.of( + new AddNotificationAction({ + type: NotificationType.ERROR, + message: message + }) + ); + }); + + @Effect() + HttpAuthorizationErrorReponse: Observable<any> = this.actions$ + .ofType(AuthActionTypes.HTTP_AUTHORIZATION_ERROR_RESPONSE) + .map((action: HttpAuthorizationErrorResponseAction) => action.payload) + .switchMap(payload => { + const response = payload.response; + let action; + switch (response.status) { + case 401 : + action = new LoggedOutAction({response}); + break; + case 403 : + action = new ForbiddenAction({response}); + break; + case 419 : + action = new AuthorizationTimeoutAction({response}); + break; + } + return Observable.of(action); + }); + + @Effect() + Forbidden: Observable<any> = this.actions$ + .ofType(AuthActionTypes.FORBIDDEN) + .map((action: ForbiddenAction) => action.payload.response) + .switchMap((response: Response) => Observable.of( + new AddNotificationAction({ + type: NotificationType.ERROR, + message: 'authorization.error.forbidden' + }) + )); + + @Effect() + AuthorizationTimeoutAction: Observable<any> = this.actions$ + .ofType(AuthActionTypes.AUTHORIZATION_TIMEOUT) + .map((action: AuthorizationTimeoutAction) => action.payload.response) + .switchMap((response: Response) => Observable.of( + new AddNotificationAction({ + type: NotificationType.ERROR, + message: 'authorization.error.authorizationTimeout' + }) + )); + + constructor( + private actions$: Actions, + private authService: AuthService, + private router: Router + ) {} + +} diff --git a/ambari-logsearch-web/src/app/services/auth-guard.service.ts b/ambari-logsearch-web/src/app/store/effects/notification.effects.ts similarity index 51% copy from ambari-logsearch-web/src/app/services/auth-guard.service.ts copy to ambari-logsearch-web/src/app/store/effects/notification.effects.ts index 8b56239..a4bf62e 100644 --- a/ambari-logsearch-web/src/app/services/auth-guard.service.ts +++ b/ambari-logsearch-web/src/app/store/effects/notification.effects.ts @@ -16,28 +16,32 @@ * limitations under the License. */ -import {Injectable} from '@angular/core'; -import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; -import {Observable} from 'rxjs/Observable'; +import { Injectable } from '@angular/core'; +import { Actions, Effect } from '@ngrx/effects'; +import { Observable } from 'rxjs/Observable'; -import {AuthService} from '@app/services/auth.service'; +import 'rxjs/add/operator/do'; -/** - * This guard goal is to prevent to display screens where authorization needs. - */ -@Injectable() -export class AuthGuardService implements CanActivate { +import { NotificationService } from '@modules/shared/services/notification.service'; +import { + NotificationActionTypes, + AddNotificationAction +} from '../actions/notification.actions'; - constructor(private authService: AuthService, private router: Router) {} - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { - return this.authService.isAuthorized().map((isAuthorized: boolean) => { - this.authService.redirectUrl = state.url; - if (!isAuthorized) { - this.router.navigate(['/login']); - } - return isAuthorized; +@Injectable() +export class NotificationEffects { + + @Effect({ dispatch: false }) + AddNotificationAction: Observable<any> = this.actions$ + .ofType(NotificationActionTypes.ADD_NOTIFICATION) + .do((action: AddNotificationAction) => { + this.notificationService.addNotification(action.payload); }); - } + + constructor( + private actions$: Actions, + private notificationService: NotificationService + ) {} } diff --git a/ambari-logsearch-web/src/app/store/reducers/auth.reducers.ts b/ambari-logsearch-web/src/app/store/reducers/auth.reducers.ts new file mode 100644 index 0000000..09ba8d9 --- /dev/null +++ b/ambari-logsearch-web/src/app/store/reducers/auth.reducers.ts @@ -0,0 +1,120 @@ +/** + * 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 { User } from '../../classes/models/user'; + +import { AuthActionTypes, AuthActions } from '../actions/auth.actions'; + +export enum AuthorizationStatuses { + UNAUTHORIZED = 'Unauthorized', // no authorization attempt yet + NOT_AUTHORIZED = 'Not Authorized', // there were authorization attempt(s) but it was unsuccessful + CHEKCING_AUTHORIZATION_STATUS = 'Checking Authorization Status', // checking if the user already authenticated (eg. refreshed the page) + FORBIDDEN = 'Forbidden', // no access to Log Search + LOGGING_IN = 'Logging In', // login in progress + AUTHORIZED = 'Authorized', // authorized + LOGGING_OUT = 'Logging Out', // logout in progress + LOGGED_OUT = 'Logged Out', // logged out, the user was authorized before + LOGOUT_ERROR = 'Logout Error', // logged out, the user was authorized before + AUTHORIZATION_ERROR = 'Authorization Error' // there were some server error during the authorization +}; + +export interface State { + status: AuthorizationStatuses; // the status of the authorization + code?: number; // the last status code of the authorization response + user?: User | null; // the user model from the app or from the server + message?: string | null; // message from the server after the authentication attempt +} + +export const initialState: State = { + status: null, + user: null +}; + +export function reducer(state = initialState, action: AuthActions): State { +switch (action.type) { + case AuthActionTypes.LOGIN: { + return { + ...state, + status: AuthorizationStatuses.LOGGING_IN + }; + } + case AuthActionTypes.CHECK_AUTHORIZATION_STATUS: { + return { + ...state, + status: AuthorizationStatuses.CHEKCING_AUTHORIZATION_STATUS + }; + } + case AuthActionTypes.AUTHORIZED: { + const payload = action.payload; + return { + ...state, + status: AuthorizationStatuses.AUTHORIZED, + message: payload.message || '', + user: payload.user || null + }; + } + case AuthActionTypes.UNAUTHORIZED: { + const payload = action.payload; + return { + ...state, + message: payload.message || '', + status: AuthorizationStatuses.UNAUTHORIZED + }; + } + case AuthActionTypes.FORBIDDEN: { + const payload = action.payload; + return { + ...state, + status: AuthorizationStatuses.FORBIDDEN, + message: payload.message || '' + }; + } + case AuthActionTypes.LOGOUT: { + return { + ...state, + status: AuthorizationStatuses.LOGGING_OUT + }; + } + case AuthActionTypes.LOGGED_OUT: { + return { + ...state, + status: AuthorizationStatuses.LOGGED_OUT + }; + } + case AuthActionTypes.LOGOUT_ERROR: { + const payload = action.payload; + return { + ...state, + status: AuthorizationStatuses.LOGOUT_ERROR, + message: payload.message || '' + }; + } + default: { + return state; + } + } +}; + +export const getStatus = (state: State): AuthorizationStatuses => state.status; +export const getMessage = (state: State): string => (state.message || ''); +export const isAuthorized = (status: AuthorizationStatuses): boolean => AuthorizationStatuses.AUTHORIZED === status; +export const isLoginInProgress = (status: AuthorizationStatuses): boolean => AuthorizationStatuses.LOGGING_IN === status; +export const isLoggedOut = (status: AuthorizationStatuses): boolean => AuthorizationStatuses.LOGGED_OUT === status; +export const isCheckingAuthInProgress = (status: AuthorizationStatuses): boolean => ( + AuthorizationStatuses.CHEKCING_AUTHORIZATION_STATUS === status +); diff --git a/ambari-logsearch-web/src/app/store/selectors/auth.selectors.ts b/ambari-logsearch-web/src/app/store/selectors/auth.selectors.ts new file mode 100644 index 0000000..23decd9 --- /dev/null +++ b/ambari-logsearch-web/src/app/store/selectors/auth.selectors.ts @@ -0,0 +1,47 @@ +/** + * 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 { createSelector } from 'reselect'; + +import * as fromAuth from '@app/store/reducers/auth.reducers'; +import { AppStore } from '@app/classes/models/store'; + +export const getAuthState = (state: AppStore): fromAuth.State => state.auth; + +export const authStatusSelector = createSelector( getAuthState, fromAuth.getStatus ); +export const authMessageSelector = createSelector( getAuthState, fromAuth.getMessage ); + +export const isAuthorizedSelector = createSelector( + authStatusSelector, + fromAuth.isAuthorized +); + +export const isLoginInProgressSelector = createSelector( + authStatusSelector, + fromAuth.isLoginInProgress +); + +export const isLoggedOutSelector = createSelector( + authStatusSelector, + fromAuth.isLoggedOut +); + +export const isCheckingAuthStatusInProgressSelector = createSelector( + authStatusSelector, + fromAuth.isCheckingAuthInProgress +); diff --git a/ambari-logsearch-web/src/app/test-config.spec.ts b/ambari-logsearch-web/src/app/test-config.spec.ts index 9a53a37..0bad5f8 100644 --- a/ambari-logsearch-web/src/app/test-config.spec.ts +++ b/ambari-logsearch-web/src/app/test-config.spec.ts @@ -16,20 +16,27 @@ * limitations under the License. */ -import {HttpModule, Http, BrowserXhr, XSRFStrategy, ResponseOptions, XHRBackend} from '@angular/http'; -import {TranslateModule, TranslateLoader} from '@ngx-translate/core'; -import {TranslateHttpLoader} from '@ngx-translate/http-loader'; -import {Injector} from '@angular/core'; -import {InMemoryBackendService} from 'angular-in-memory-web-api'; -import {MockApiDataService} from '@app/services/mock-api-data.service'; -import {HttpClientService} from '@app/services/http-client.service'; -import {RouterTestingModule} from '@angular/router/testing'; -import {clusters, ClustersService} from '@app/services/storage/clusters.service'; -import {StoreModule} from '@ngrx/store'; -import {UtilsService} from '@app/services/utils.service'; -import {ComponentGeneratorService} from '@app/services/component-generator.service'; -import {HostsService} from '@app/services/storage/hosts.service'; -import {ComponentsService} from '@app/services/storage/components.service'; +import { HttpModule, Http, BrowserXhr, XSRFStrategy, ResponseOptions, XHRBackend } from '@angular/http'; +import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { Injector } from '@angular/core'; +import { InMemoryBackendService } from 'angular-in-memory-web-api'; +import { MockApiDataService } from '@app/services/mock-api-data.service'; +import { HttpClientService } from '@app/services/http-client.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { clusters, ClustersService } from '@app/services/storage/clusters.service'; +import { StoreModule } from '@ngrx/store'; +import { UtilsService } from '@app/services/utils.service'; +import { ComponentGeneratorService } from '@app/services/component-generator.service'; +import { HostsService } from '@app/services/storage/hosts.service'; +import { ComponentsService } from '@app/services/storage/components.service'; + +import { AuthService } from '@app/services/auth.service'; +import { EffectsModule } from '@ngrx/effects'; +import { AuthEffects } from '@app/store/effects/auth.effects'; +import { NotificationEffects } from '@app/store/effects/notification.effects'; + +import * as auth from '@app/store/reducers/auth.reducers'; function HttpLoaderFactory(http: Http) { return new TranslateHttpLoader(http, 'assets/i18n/', '.json'); @@ -62,8 +69,11 @@ export const getCommonTestingBedConfiguration = ( ...TranslationModules, RouterTestingModule, StoreModule.provideStore({ - clusters + clusters, + auth: auth.reducer }), + EffectsModule.run(AuthEffects), + EffectsModule.run(NotificationEffects), ...imports ], providers: [ @@ -73,6 +83,7 @@ export const getCommonTestingBedConfiguration = ( HostsService, ComponentsService, UtilsService, + AuthService, ...providers ], declarations: [ diff --git a/ambari-logsearch-web/src/assets/i18n/en.json b/ambari-logsearch-web/src/assets/i18n/en.json index a0796ac..d8b3801 100644 --- a/ambari-logsearch-web/src/assets/i18n/en.json +++ b/ambari-logsearch-web/src/assets/i18n/en.json @@ -25,7 +25,13 @@ "authorization.name": "Username", "authorization.password": "Password", "authorization.signIn": "Sign In", - "authorization.error.401": "Unable to sign in. Invalid username/password combination.", + "authorization.checkingAuthorization": "Checking authorization...", + "authorization.authorized": "Successful authorization.", + "authorization.loggedOut": "Successfuly logged out.", + "authorization.error.authorizationError": "Error during authorization. Maybe your session expired.", + "authorization.error.unauthorized": "Unable to sign in. Invalid username/password combination.", + "authorization.error.forbidden": "Access forbidden.", + "authorization.error.authorizationTimeout": "Authorization timeout.", "login.title": "Login", diff --git a/ambari-logsearch-web/yarn.lock b/ambari-logsearch-web/yarn.lock index ae4bb5a..32e3a25 100644 --- a/ambari-logsearch-web/yarn.lock +++ b/ambari-logsearch-web/yarn.lock @@ -158,6 +158,10 @@ version "1.2.0" resolved "https://registry.yarnpkg.com/@ngrx/core/-/core-1.2.0.tgz#882b46abafa2e0e6d887cb71a1b2c2fa3e6d0dc6" +"@ngrx/effects@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-2.0.5.tgz#10986923b7193af9b08944e80c5a661ba93a7936" + "@ngrx/store-devtools@3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-3.2.4.tgz#2ce4d13bf34848a9e51ec87e3b125ed67b51e550" @@ -2674,6 +2678,12 @@ fresh@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" +fs-access@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" + dependencies: + null-check "^1.0.0" + fs-extra@^0.23.1: version "0.23.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.23.1.tgz#6611dba6adf2ab8dc9c69fab37cddf8818157e3d" @@ -3667,6 +3677,13 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.3.6" +karma-chrome-launcher@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf" + dependencies: + fs-access "^1.0.0" + which "^1.2.1" + karma-cli@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/karma-cli/-/karma-cli-1.0.1.tgz#ae6c3c58a313a1d00b45164c455b9b86ce17f960" @@ -4376,6 +4393,10 @@ nth-check@~1.0.1: dependencies: boolbase "~1.0.0" +null-check@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" + num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -5391,6 +5412,10 @@ requires-port@1.0.x, requires-port@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" +reselect@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" + resolve@^1.1.6, resolve@^1.1.7: version "1.3.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" @@ -6601,6 +6626,12 @@ which@1, which@^1.2.9: dependencies: isexe "^2.0.0" +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + dependencies: + isexe "^2.0.0" + which@^1.2.8, which@~1.2.10: version "1.2.14" resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"