This is an automated email from the ASF dual-hosted git repository. tobiasistvan pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/ambari-logsearch.git
The following commit(s) were added to refs/heads/master by this push: new 81228a7 [AMBARI-24896] [Log Search UI] Turn off features in the client when it is not available on backend (#56) 81228a7 is described below commit 81228a70899c8ace045cb1921f644033668313a9 Author: Istvan Tobias <tobias.ist...@gmail.com> AuthorDate: Fri Dec 7 16:14:14 2018 +0100 [AMBARI-24896] [Log Search UI] Turn off features in the client when it is not available on backend (#56) * [AMBARI-24896] [Log Search UI] Turn off features in the client when it is not available on backend - getting information from server * [AMBARI-24896] [Log Search UI] Turn off features in the client when it is not available on backend - Disable buttons and menu items * [AMBARI-24896] [Log Search UI] Turn off features in the client when it is not available on backend - disable routes routes --- ambari-logsearch-web/src/app/app.module.ts | 8 ++- ambari-logsearch-web/src/app/classes/list-item.ts | 2 + .../src/app/classes/models/store.ts | 2 + .../action-menu/action-menu.component.html | 10 +-- .../action-menu/action-menu.component.spec.ts | 4 +- .../action-menu/action-menu.component.ts | 37 ++++++++--- .../filter-history-manager.component.html | 6 +- .../filters-panel/filters-panel.component.html | 6 +- .../menu-button/menu-button.component.html | 2 +- .../menu-button/menu-button.component.ts | 4 +- .../components/top-menu/top-menu.component.spec.ts | 4 +- .../app/components/top-menu/top-menu.component.ts | 60 ++++++++++++------ .../modules/app-load/services/app-load.service.ts | 6 ++ .../dropdown-list/dropdown-list.component.html | 5 +- .../dropdown-list/dropdown-list.component.less | 9 +++ .../dropdown-list/dropdown-list.component.ts | 2 +- .../app/modules/shipper/shipper-routing.module.ts | 22 ++++--- .../src/app/services/api-feature.guard.ts | 48 +++++++++++++++ .../src/app/services/http-client.service.ts | 4 ++ .../meta-data-api-feature.guard.ts} | 23 ++++--- .../src/app/services/storage/reducers.service.ts | 4 +- .../src/app/store/actions/api-features.actions.ts | 53 ++++++++++++++++ .../src/app/store/effects/api-features.effects.ts | 72 ++++++++++++++++++++++ ...-repos.reducers.ts => api-features.reducers.ts} | 21 ++++--- .../app/store/reducers/audit-log-repos.reducers.ts | 4 +- .../api-features.selectors.ts} | 32 +++++----- ambari-logsearch-web/src/assets/i18n/en.json | 4 ++ 27 files changed, 359 insertions(+), 95 deletions(-) diff --git a/ambari-logsearch-web/src/app/app.module.ts b/ambari-logsearch-web/src/app/app.module.ts index c38924d..78e3a02 100644 --- a/ambari-logsearch-web/src/app/app.module.ts +++ b/ambari-logsearch-web/src/app/app.module.ts @@ -116,12 +116,14 @@ import { LogsFilteringUtilsService } from '@app/services/logs-filtering-utils.se import { LogsStateService } from '@app/services/storage/logs-state.service'; import { LoginScreenGuardService } from '@app/services/login-screen-guard.service'; import { UserSettingsService } from '@app/services/user-settings.service'; +import { MetaDataApiFeatureGuard } from '@app/services/meta-data-api-feature.guard'; import { AuthEffects } from '@app/store/effects/auth.effects'; import { NotificationEffects } from '@app/store/effects/notification.effects'; import { UserSettingsEffects } from '@app/store/effects/user-settings.effects'; import { FilterHistoryManagerComponent } from './components/filter-history-manager/filter-history-manager.component'; import { AuditLogReposEffects } from './store/effects/audit-log-repos.effects'; +import { ApiFeaturesEffects } from './store/effects/api-features.effects'; import { HostNamePipe } from '@app/pipes/host-name.pipe'; @@ -205,7 +207,8 @@ import { HostNamePipe } from '@app/pipes/host-name.pipe'; EffectsModule.run(AuthEffects), EffectsModule.run(AuditLogReposEffects), EffectsModule.run(UserSettingsEffects), - EffectsModule.run(NotificationEffects) + EffectsModule.run(NotificationEffects), + EffectsModule.run(ApiFeaturesEffects) ], providers: [ @@ -239,7 +242,8 @@ import { HostNamePipe } from '@app/pipes/host-name.pipe'; LogsFilteringUtilsService, LogsStateService, LoginScreenGuardService, - UserSettingsService + UserSettingsService, + MetaDataApiFeatureGuard ], bootstrap: [AppComponent], entryComponents: [ diff --git a/ambari-logsearch-web/src/app/classes/list-item.ts b/ambari-logsearch-web/src/app/classes/list-item.ts index 3a9b72c..5a1a6c0 100644 --- a/ambari-logsearch-web/src/app/classes/list-item.ts +++ b/ambari-logsearch-web/src/app/classes/list-item.ts @@ -19,10 +19,12 @@ export interface ListItem { id?: string | number; label?: string; + secondaryLabel?: string; value: any; iconClass?: string; cssClass?: string; isChecked?: boolean; onSelect?: Function; isDivider?: boolean; + disabled?: boolean; } diff --git a/ambari-logsearch-web/src/app/classes/models/store.ts b/ambari-logsearch-web/src/app/classes/models/store.ts index b1d1bd9..980944c 100644 --- a/ambari-logsearch-web/src/app/classes/models/store.ts +++ b/ambari-logsearch-web/src/app/classes/models/store.ts @@ -38,6 +38,7 @@ import * as auth from '@app/store/reducers/auth.reducers'; import * as filterHistory from '@app/store/reducers/filter-history.reducers'; import * as auditLogRepos from '@app/store/reducers/audit-log-repos.reducers'; import * as userSettings from '@app/store/reducers/user-settings.reducers'; +import * as apiFeatures from '@app/store/reducers/api-features.reducers'; const storeActions = { 'ARRAY.ADD': 'ADD', @@ -77,6 +78,7 @@ export interface AppStore { auth: auth.State; filterHistory: filterHistory.FilterHistoryState; auditLogRepos: auditLogRepos.AuditLogRepo[]; + apiFeatures: apiFeatures.ApiFeatureSet; } export class ModelService { 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 acd6504..553cd19 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 @@ -16,13 +16,15 @@ --> <filter-history-manager></filter-history-manager> <menu-button label="{{'topMenu.filter' | translate}}" iconClass="fa fa-filter" - (buttonClick)="openLogIndexFilter()"></menu-button> + (buttonClick)="openLogIndexFilter()" + [disabled]="!(logLevelFiltersFeatureState$ | async)" [class.disabled]="!(logLevelFiltersFeatureState$ | async)" + [tooltip]="(logLevelFiltersFeatureTooltip$ | async) | translate"></menu-button> <menu-button *ngIf="!captureSeconds" label="{{'filter.capture' | translate}}" iconClass="fa fa-caret-right" - (buttonClick)="startCapture()"></menu-button> + (buttonClick)="startCapture()"></menu-button> <menu-button *ngIf="captureSeconds" label="{{captureSeconds | timerSeconds}}" iconClass="fa fa-stop stop-icon" - (buttonClick)="stopCapture()"></menu-button> + (buttonClick)="stopCapture()"></menu-button> <menu-button label="{{'topMenu.refresh' | translate}}" iconClass="fa fa-refresh" - (buttonClick)="refresh()"></menu-button> + (buttonClick)="refresh()"></menu-button> <modal-dialog *ngIf="isLogIndexFilterDisplayed$ | async" class="log-index-filter" [visible]="isLogIndexFilterDisplayed$ | async" 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 69df733..e9a0101 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 @@ -57,6 +57,7 @@ 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 * as apiFeatures from '@app/store/reducers/api-features.reducers'; import { AuthService } from '@app/services/auth.service'; import { EffectsModule } from '@ngrx/effects'; import { AuthEffects } from '@app/store/effects/auth.effects'; @@ -90,7 +91,8 @@ describe('ActionMenuComponent', () => { tabs, dataAvailabilityStates, auth: auth.reducer, - userSettings + userSettings, + apiFeatures: apiFeatures.reducer }), EffectsModule.run(AuthEffects), EffectsModule.run(NotificationEffects) 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 f00eb2d..3ddb0ae 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 @@ -22,6 +22,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import 'rxjs/operator/do'; import { LogsContainerService } from '@app/services/logs-container.service'; import { ServerSettingsService } from '@app/services/server-settings.service'; @@ -30,6 +31,13 @@ import { ClustersService } from '@app/services/storage/clusters.service'; import { UtilsService } from '@app/services/utils.service'; import { Subject } from 'rxjs/Subject'; +import { Store } from '@ngrx/store'; +import { AppStore } from '@app/classes/models/store'; +import { selectLogLevelFiltersFeatureState } from '@app/store/selectors/api-features.selectors'; + +import { AddNotificationAction, NotificationActions } from '@app/store/actions/notification.actions'; +import { NotificationType } from '@modules/shared/services/notification.service'; + @Component({ selector: 'action-menu', templateUrl: './action-menu.component.html', @@ -37,12 +45,26 @@ import { Subject } from 'rxjs/Subject'; }) export class ActionMenuComponent implements OnInit, OnDestroy { - isLogIndexFilterDisplayed$: Observable<boolean> = this.route.queryParams - .map((params) => { - return params; - }) - .map((params): boolean => /^(show|yes|true|1)$/.test(params.logIndexFilterSettings)) - .distinctUntilChanged(); + logLevelFiltersFeatureState$: Observable<any> = this.store.select(selectLogLevelFiltersFeatureState); + logLevelFiltersFeatureTooltip$: Observable<string> = this.logLevelFiltersFeatureState$.map((state: boolean) => ( + state ? '' : 'apiFeatures.disabled' + )); + + isLogIndexFilterDisplayed$: Observable<boolean> = Observable.combineLatest( + this.route.queryParams + .map((params) => { + return params; + }) + .map((params): boolean => /^(show|yes|true|1)$/.test(params.logIndexFilterSettings)), + this.logLevelFiltersFeatureState$ + ).do(([show, enabled]) => { + if (show && !enabled) { + this.store.dispatch(new AddNotificationAction({ + type: NotificationType.ERROR, + message: 'apiFeatures.disabled' + })) + } + }).map(([show, enabled]) => show && enabled).distinctUntilChanged(); settingsForm: FormGroup = this.settings.settingsFormGroup; @@ -66,7 +88,8 @@ export class ActionMenuComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private router: Router, private clustersService: ClustersService, - private utilsService: UtilsService + private utilsService: UtilsService, + private store: Store<AppStore> ) { } diff --git a/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.html b/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.html index c34a46e..3dd50b3 100644 --- a/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.html +++ b/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.html @@ -15,15 +15,15 @@ limitations under the License. --> <menu-button label="{{'topMenu.undo' | translate}}" [subItems]="activeUndoHistoryListItems$ | async" iconClass="fa fa-arrow-left" - class="history-menu" [class.disabled]="!(hasActiveUndoHistoryItems$ | async)" [isDisabled]="!(hasActiveUndoHistoryItems$ | async)" + class="history-menu" [class.disabled]="!(hasActiveUndoHistoryItems$ | async)" [disabled]="!(hasActiveUndoHistoryItems$ | async)" listClass="history-dropdown" [isRightAlign]="true" (buttonClick)="undo()" (selectItem)="onListItemClick($event)"> </menu-button> <menu-button label="{{'topMenu.redo' | translate}}" [subItems]="activeRedoHistoryListItems$ | async" iconClass="fa fa-arrow-right" - class="history-menu" [class.disabled]="!(hasActiveRedoHistoryItems$ | async)" [isDisabled]="!(hasActiveRedoHistoryItems$ | async)" + class="history-menu" [class.disabled]="!(hasActiveRedoHistoryItems$ | async)" [disabled]="!(hasActiveRedoHistoryItems$ | async)" listClass="history-dropdown" [isRightAlign]="true" (buttonClick)="redo()" (selectItem)="onListItemClick($event)"> </menu-button> <menu-button label="{{'topMenu.history' | translate}}" [subItems]="activeHistoryListItems$ | async" iconClass="fa fa-history" - class="history-menu" [class.disabled]="!(hasActiveHistoryItems$ | async)" [isDisabled]="!(hasActiveHistoryItems$ | async)" + class="history-menu" [class.disabled]="!(hasActiveHistoryItems$ | async)" [disabled]="!(hasActiveHistoryItems$ | async)" listClass="history-dropdown" [isRightAlign]="true" (selectItem)="onListItemClick($event)"></menu-button> 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 016ac02..9550c73 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 @@ -44,7 +44,7 @@ [iconClass]="filters.hosts.iconClass" [isRightAlign]="true" [class.disabled]="isServiceLogsFileView$ | async" - [isDisabled]="isServiceLogsFileView$ | async" + [disabled]="isServiceLogsFileView$ | async" additionalLabelComponentSetter="getDataForHostsNodeBar"></filter-button> <filter-button *ngIf="isFilterConditionDisplayed('users')" @@ -65,7 +65,7 @@ [iconClass]="filters.components.iconClass" [isRightAlign]="true" [class.disabled]="isServiceLogsFileView$ | async" - [isDisabled]="isServiceLogsFileView$ | async" + [disabled]="isServiceLogsFileView$ | async" additionalLabelComponentSetter="getDataForComponentsNodeBar"></filter-button> <filter-button *ngIf="isFilterConditionDisplayed('repos')" @@ -77,7 +77,7 @@ [iconClass]="filters.repos.iconClass" [isRightAlign]="true" [class.disabled]="isServiceLogsFileView$ | async" - [isDisabled]="isServiceLogsFileView$ | async"></filter-button> + [disabled]="isServiceLogsFileView$ | async"></filter-button> <filter-button *ngIf="isFilterConditionDisplayed('levels')" formControlName="levels" diff --git a/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html b/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html index cb63d44..d2d2f6a 100644 --- a/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html +++ b/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -<div #dropdown [ngClass]="{'dropdown': hasSubItems, 'text-center': true, 'open': dropdownIsOpen, 'disabled': isDisabled, 'has-selection': hasSelection}"> +<div #dropdown [ngClass]="{'dropdown': hasSubItems, 'text-center': true, 'open': dropdownIsOpen, 'disabled': disabled, 'has-selection': hasSelection}"> <a class="dropdown-toggle" [ngClass]="(labelClass || '') + (hasCaret ? ' has-caret' : '')" (click)="onMouseClick($event)" (mousedown)="onMouseDown($event)"> diff --git a/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts b/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts index 3d510fc..79ca5e7 100644 --- a/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts +++ b/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts @@ -87,7 +87,7 @@ export class MenuButtonComponent { maxLongClickDelay = 0; @Input() - isDisabled = false; + disabled = false; @Input() listClass = ''; @@ -155,7 +155,7 @@ export class MenuButtonComponent { * @param {MouseEvent} event */ onMouseClick(event: MouseEvent): void { - if (!this.isDisabled) { + if (!this.disabled) { const el = <HTMLElement>event.target; const now = Date.now(); const mdt = this.mouseDownTimestamp; // mousedown time 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 a340dd6..3d35ba9 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 @@ -51,6 +51,7 @@ 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 * as apiFeatures from '@app/store/reducers/api-features.reducers'; import { EffectsModule } from '@ngrx/effects'; import { AuthEffects } from '@app/store/effects/auth.effects'; import { NotificationEffects } from '@app/store/effects/notification.effects'; @@ -80,7 +81,8 @@ describe('TopMenuComponent', () => { components, hosts, auth: auth.reducer, - userSettings: userSettings.reducer + userSettings: userSettings.reducer, + apiFeatures: apiFeatures.reducer }), EffectsModule.run(AuthEffects), EffectsModule.run(NotificationEffects), 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 bff40da..d80fbc6 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 @@ -16,7 +16,7 @@ * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { FilterCondition, TimeUnitListItem } from '@app/classes/filtering'; import { ListItem } from '@app/classes/list-item'; @@ -25,15 +25,26 @@ import { LogsContainerService } from '@app/services/logs-container.service'; import { Router, ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; +import { Subject } from 'rxjs/Subject'; +import { Observable } from 'rxjs/Observable'; + import { AppStore } from '@app/classes/models/store'; import { LogOutAction } from '@app/store/actions/auth.actions'; +import { selectMetadataPatternsFeatureState } from '@app/store/selectors/api-features.selectors'; + @Component({ selector: 'top-menu', templateUrl: './top-menu.component.html', styleUrls: ['./top-menu.component.less'] }) -export class TopMenuComponent { +export class TopMenuComponent implements OnInit, OnDestroy { + + private items; + + metadataPatternsFeatureState$ = this.store.select(selectMetadataPatternsFeatureState).startWith(true); + + destroyed$: Subject<boolean> = new Subject(); constructor( private logsContainer: LogsContainerService, @@ -42,6 +53,15 @@ export class TopMenuComponent { private store: Store<AppStore> ) {} + ngOnInit() { + this.metadataPatternsFeatureState$.takeUntil(this.destroyed$).subscribe(this.onMetadataFeatureStateChange); + } + + ngOnDestroy() { + this.destroyed$.next(true); + this.destroyed$.complete(); + } + get filtersForm(): FormGroup { return this.logsContainer.filtersForm; }; @@ -57,20 +77,9 @@ export class TopMenuComponent { relativeTo: this.route.root.firstChild }); } - - /** - * Dispatch the LogOutAction. - */ - logout = (): void => { - this.store.dispatch(new LogOutAction()); - } - navigateToShipperConfig = (): void => { - this.router.navigate(['/shipper']); - } - - readonly items = [ - { + onMetadataFeatureStateChange = (state: boolean) => { + this.items = [{ iconClass: 'fa fa-user grey', hideCaret: true, isRightAlign: true, @@ -83,8 +92,10 @@ export class TopMenuComponent { { label: 'topMenu.shipperConfiguration', + secondaryLabel: state ? '' : 'apiFeatures.disabled', onSelect: this.navigateToShipperConfig, - iconClass: 'fa fa-file-code-o' + iconClass: 'fa fa-file-code-o', + disabled: !state }, { isDivider: true @@ -95,8 +106,21 @@ export class TopMenuComponent { iconClass: 'fa fa-sign-out' } ] - } - ]; + }]; + } + + openSettings = (): void => {}; + + /** + * Dispatch the LogOutAction. + */ + logout = (): void => { + this.store.dispatch(new LogOutAction()); + } + + navigateToShipperConfig = (): void => { + this.router.navigate(['/shipper']); + } get clusters(): (ListItem | TimeUnitListItem[])[] { return this.filters.clusters.options; 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 44d22be..cf1a365 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 @@ -39,6 +39,7 @@ import { Store } from '@ngrx/store'; import { AppStore } from '@app/classes/models/store'; import { isAuthorizedSelector } from '@app/store/selectors/auth.selectors'; import { LoadAuditLogsReposAction } from '@app/store/actions/audit-log-repos.actions'; +import { LoadApiFeaturesAction } from '@app/store/actions/api-features.actions'; // @ToDo create a separate data state enrty in the store with keys of the model names export enum DataStateStoreKeys { @@ -104,6 +105,7 @@ export class AppLoadService { }); this.baseDataAvailibilityState$.subscribe(this.onBaseDataAvailabilityChange); + this.loadApiFeaturesInfo(); } onDataAvailibilityChange = (dataAvailabilityStates: DataAvailabilityValues[]): void => { @@ -238,6 +240,10 @@ export class AppLoadService { return responses$; } + loadApiFeaturesInfo() { + this.store.dispatch( new LoadApiFeaturesAction() ); + } + initOnAuthorization = (isAuthorized): void => { if (isAuthorized) { this.loadClusters(); 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 cc790ab..ae5c748 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 @@ -16,7 +16,8 @@ --> <ng-template #listItem let-item let-isMultipleChoice="isMultipleChoice"> <li [class.divider]="item.isDivider" [class.filtered]="isFiltered(item)" - [attr.role]="item.isDivider ? 'separator' : null" [ngClass]="(item.cssClass || '')" (click)="onItemClick($event)"> + [attr.role]="item.isDivider ? 'separator' : null" [ngClass]="(item.cssClass || '')" (click)="onItemClick($event)" + [class.disabled]="item.disabled"> <ng-container *ngIf="!item.isDivider"> <span class="list-item-label" *ngIf="isMultipleChoice"> <input type="checkbox" [attr.id]="(instanceId) + '-' + (item.id || item.value)" [ngModel]="item.isChecked" @@ -24,12 +25,14 @@ <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 *ngIf="item.secondaryLabel" class="item-secondary-label-text">{{item.secondaryLabel | translate}}</span> <span #additionalComponent></span> </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> + <span *ngIf="item.secondaryLabel" class="item-secondary-label-text">{{item.secondaryLabel | translate}}</span> <span #additionalComponent></span> </span> </ng-container> diff --git a/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.less b/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.less index 415a8fd..c432a0d 100644 --- a/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.less +++ b/ambari-logsearch-web/src/app/modules/shared/components/dropdown-list/dropdown-list.component.less @@ -33,6 +33,10 @@ opacity: 0; height: 0; } + &.disabled { + color: @fluid-gray-2; + cursor: default; + } .list-item-label { .dropdown-item-child-default; @@ -49,6 +53,11 @@ width: 100%; } } + .item-secondary-label-text { + color: @fluid-gray-2; + display: block; + font-size: .8em; + } &.filter { padding: 0 .25em; position: relative; 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 4db1bc5..fb937f0 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 @@ -222,7 +222,7 @@ export class DropdownListComponent implements OnInit, OnChanges, AfterViewChecke changeSelectedItem(item: ListItem | ListItem[], event?: MouseEvent): void { (Array.isArray(item) ? item : [item]).forEach((currentItem: ListItem) => { - if (currentItem.onSelect) { + if (currentItem.onSelect && !currentItem.disabled) { currentItem.onSelect(...this.actionArguments) } }); diff --git a/ambari-logsearch-web/src/app/modules/shipper/shipper-routing.module.ts b/ambari-logsearch-web/src/app/modules/shipper/shipper-routing.module.ts index 5c92de0..784bd03 100644 --- a/ambari-logsearch-web/src/app/modules/shipper/shipper-routing.module.ts +++ b/ambari-logsearch-web/src/app/modules/shipper/shipper-routing.module.ts @@ -16,14 +16,16 @@ * limitations under the License. */ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; -import {AuthGuardService} from '@app/services/auth-guard.service'; -import {CanDeactivateGuardService} from '@modules/shared/services/can-deactivate-guard.service'; +import { AuthGuardService } from '@app/services/auth-guard.service'; +import { CanDeactivateGuardService } from '@modules/shared/services/can-deactivate-guard.service'; -import {ShipperConfigurationComponent} from './components/shipper-configuration/shipper-configuration.component'; -import {ShipperGuard} from '@modules/shipper/services/shipper.guard'; +import { ShipperConfigurationComponent } from './components/shipper-configuration/shipper-configuration.component'; +import { ShipperGuard } from '@modules/shipper/services/shipper.guard'; + +import { MetaDataApiFeatureGuard } from '@app/services/meta-data-api-feature.guard'; const shipperRoutes: Routes = [{ path: 'shipper/:cluster/add', @@ -32,7 +34,7 @@ const shipperRoutes: Routes = [{ breadcrumbs: ['shipperConfiguration.breadcrumbs.title', 'shipperConfiguration.breadcrumbs.add'], multiClusterFilter: false }, - canActivate: [AuthGuardService], + canActivate: [AuthGuardService, MetaDataApiFeatureGuard], canDeactivate: [CanDeactivateGuardService] }, { path: 'shipper/:cluster/:service', @@ -41,7 +43,7 @@ const shipperRoutes: Routes = [{ breadcrumbs: ['shipperConfiguration.breadcrumbs.title', 'shipperConfiguration.breadcrumbs.update'], multiClusterFilter: false }, - canActivate: [AuthGuardService, ShipperGuard], + canActivate: [AuthGuardService, ShipperGuard, MetaDataApiFeatureGuard], canDeactivate: [CanDeactivateGuardService] }, { path: 'shipper/:cluster', @@ -50,7 +52,7 @@ const shipperRoutes: Routes = [{ breadcrumbs: 'shipperConfiguration.breadcrumbs.title', multiClusterFilter: false }, - canActivate: [AuthGuardService, ShipperGuard] + canActivate: [AuthGuardService, ShipperGuard, MetaDataApiFeatureGuard] }, { path: 'shipper', component: ShipperConfigurationComponent, @@ -58,7 +60,7 @@ const shipperRoutes: Routes = [{ breadcrumbs: 'shipperConfiguration.breadcrumbs.title', multiClusterFilter: false }, - canActivate: [AuthGuardService, ShipperGuard] + canActivate: [AuthGuardService, ShipperGuard, MetaDataApiFeatureGuard] }]; @NgModule({ diff --git a/ambari-logsearch-web/src/app/services/api-feature.guard.ts b/ambari-logsearch-web/src/app/services/api-feature.guard.ts new file mode 100644 index 0000000..85c172b --- /dev/null +++ b/ambari-logsearch-web/src/app/services/api-feature.guard.ts @@ -0,0 +1,48 @@ +/** + * 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 { CanActivate } from '@angular/router'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/operator/do'; +import { Store } from '@ngrx/store'; + +import { AppStore } from '@app/classes/models/store'; +import { AddNotificationAction, NotificationActions } from '@app/store/actions/notification.actions'; +import { NotificationType } from '@modules/shared/services/notification.service'; + +@Injectable() +export class ApiFeatureGuard implements CanActivate { + + protected selector; + + constructor(private store: Store<AppStore>){} + + canActivate(): Observable<boolean> { + const canActivate$: Observable<boolean> = (this.selector ? this.store.select(this.selector) : Observable.of(true)); + return canActivate$.do((state) => { + if (!state) { + this.store.dispatch(new AddNotificationAction({ + type: NotificationType.ERROR, + message: 'apiFeatures.disabled' + })); + } + }); + } +} \ No newline at end of file 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 abdf9cc..e06ee1e 100644 --- a/ambari-logsearch-web/src/app/services/http-client.service.ts +++ b/ambari-logsearch-web/src/app/services/http-client.service.ts @@ -36,6 +36,7 @@ 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'; +import { selectLogLevelFiltersFeatureState } from '@app/store/selectors/api-features.selectors'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; @@ -110,6 +111,9 @@ export class HttpClientService extends Http { userSettings: { url: 'metadata/list', params: () => ({type: 'user_settings'}) + }, + apiFeatures: { + url: 'info/features' } }; diff --git a/ambari-logsearch-web/src/app/classes/list-item.ts b/ambari-logsearch-web/src/app/services/meta-data-api-feature.guard.ts similarity index 66% copy from ambari-logsearch-web/src/app/classes/list-item.ts copy to ambari-logsearch-web/src/app/services/meta-data-api-feature.guard.ts index 3a9b72c..c7536b2 100644 --- a/ambari-logsearch-web/src/app/classes/list-item.ts +++ b/ambari-logsearch-web/src/app/services/meta-data-api-feature.guard.ts @@ -16,13 +16,16 @@ * limitations under the License. */ -export interface ListItem { - id?: string | number; - label?: string; - value: any; - iconClass?: string; - cssClass?: string; - isChecked?: boolean; - onSelect?: Function; - isDivider?: boolean; -} +import { Injectable } from '@angular/core'; +import { CanActivate } from '@angular/router'; + +import { ApiFeatureGuard } from '@app/services/api-feature.guard'; + +import { selectMetadataPatternsFeatureState } from '@app/store/selectors/api-features.selectors'; + +@Injectable() +export class MetaDataApiFeatureGuard extends ApiFeatureGuard { + + protected selector = selectMetadataPatternsFeatureState; + +} \ No newline at end of file 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 68dc186..0d6c03c 100644 --- a/ambari-logsearch-web/src/app/services/storage/reducers.service.ts +++ b/ambari-logsearch-web/src/app/services/storage/reducers.service.ts @@ -40,6 +40,7 @@ import * as auth from '@app/store/reducers/auth.reducers'; import * as filterHistory from '@app/store/reducers/filter-history.reducers'; import * as auditLogRepos from '@app/store/reducers/audit-log-repos.reducers'; import * as userSettings from '@app/store/reducers/user-settings.reducers'; +import * as apiFeatures from '@app/store/reducers/api-features.reducers'; export const reducers = { appSettings, @@ -63,7 +64,8 @@ export const reducers = { auth: auth.reducer, filterHistory: filterHistory.reducer, auditLogRepos: auditLogRepos.reducer, - userSettings: userSettings.reducer + userSettings: userSettings.reducer, + apiFeatures: apiFeatures.reducer }; export function reducer(state: any, action: any) { diff --git a/ambari-logsearch-web/src/app/store/actions/api-features.actions.ts b/ambari-logsearch-web/src/app/store/actions/api-features.actions.ts new file mode 100644 index 0000000..8452174 --- /dev/null +++ b/ambari-logsearch-web/src/app/store/actions/api-features.actions.ts @@ -0,0 +1,53 @@ +/** + * 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 { Action } from '@ngrx/store'; + +import { ApiFeatureSet } from '../reducers/api-features.reducers'; + +export enum ApiFeaturesActionTypes { + SET = '[Api Features] Set', + LOAD = '[Api Features] Load', + LOAD_SUCCESS = '[Api Features] Load success', + LOAD_FAILED = '[Api Features] Load failed' +} + +export class LoadApiFeaturesAction implements Action { + readonly type = ApiFeaturesActionTypes.LOAD; + constructor() {} +} + +export class LoadSuccessApiFeaturesAction implements Action { + readonly type = ApiFeaturesActionTypes.LOAD_SUCCESS; + constructor(public payload: ApiFeatureSet) {} +} + +export class LoadFailedApiFeaturesAction implements Action { + readonly type = ApiFeaturesActionTypes.LOAD_FAILED; + constructor(public payload: any) {} +} + +export class SetApiFeaturesAction implements Action { + readonly type = ApiFeaturesActionTypes.SET; + constructor(public payload: ApiFeatureSet) {} +} + +export type ApiFeaturesActions = LoadApiFeaturesAction + | LoadSuccessApiFeaturesAction + | LoadFailedApiFeaturesAction + | SetApiFeaturesAction; \ No newline at end of file diff --git a/ambari-logsearch-web/src/app/store/effects/api-features.effects.ts b/ambari-logsearch-web/src/app/store/effects/api-features.effects.ts new file mode 100644 index 0000000..8644e3d --- /dev/null +++ b/ambari-logsearch-web/src/app/store/effects/api-features.effects.ts @@ -0,0 +1,72 @@ +/** + * 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 { Actions, Effect } from '@ngrx/effects'; +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; +import 'rxjs/add/operator/mapTo'; + +import { AppStore } from '@app/classes/models/store'; +import { HttpClientService } from '@app/services/http-client.service'; + +import { AddNotificationAction, NotificationActions } from '@app/store/actions/notification.actions'; +import { NotificationType } from '@modules/shared/services/notification.service'; + +import { + ApiFeaturesActions, + LoadSuccessApiFeaturesAction, + LoadFailedApiFeaturesAction, + SetApiFeaturesAction, + ApiFeaturesActionTypes +} from '@app/store/actions/api-features.actions'; +import { ApiFeatureSet } from '@app/store/reducers/api-features.reducers'; + +@Injectable() +export class ApiFeaturesEffects { + constructor( + private actions$: Actions, + private store: Store<AppStore>, + private httpClient: HttpClientService + ) {} + + @Effect() + loadAction: Observable<ApiFeaturesActions> = this.actions$ + .ofType(ApiFeaturesActionTypes.LOAD) + .switchMap((action) => { + return this.httpClient.get('apiFeatures').map((response) => { + const responseBody = response.json(); + return response.ok ? new LoadSuccessApiFeaturesAction(responseBody) : new LoadFailedApiFeaturesAction(responseBody); + }); + }); + + @Effect() + loadSuccessAction: Observable<ApiFeaturesActions> = this.actions$ + .ofType(ApiFeaturesActionTypes.LOAD_SUCCESS) + .map((action: LoadSuccessApiFeaturesAction): ApiFeatureSet => action.payload) + .map((apiFeatures: ApiFeatureSet) => new SetApiFeaturesAction(apiFeatures)); + + @Effect() + loadFailedAction: Observable<NotificationActions> = this.actions$ + .ofType(ApiFeaturesActionTypes.LOAD_FAILED) + .mapTo(new AddNotificationAction({ + type: NotificationType.ERROR, + message: 'apiFeatures.loadFailed' + })); + +} \ No newline at end of file diff --git a/ambari-logsearch-web/src/app/store/reducers/audit-log-repos.reducers.ts b/ambari-logsearch-web/src/app/store/reducers/api-features.reducers.ts similarity index 68% copy from ambari-logsearch-web/src/app/store/reducers/audit-log-repos.reducers.ts copy to ambari-logsearch-web/src/app/store/reducers/api-features.reducers.ts index c04ff3b..6c89e62 100644 --- a/ambari-logsearch-web/src/app/store/reducers/audit-log-repos.reducers.ts +++ b/ambari-logsearch-web/src/app/store/reducers/api-features.reducers.ts @@ -1,5 +1,3 @@ -import { AuditLogReposActions, AuditLogReposActionTypes } from "../actions/audit-log-repos.actions"; - /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -18,18 +16,21 @@ import { AuditLogReposActions, AuditLogReposActionTypes } from "../actions/audit * limitations under the License. */ -export interface AuditLogRepo { - name: string; - label: string; +import { ApiFeaturesActions, ApiFeaturesActionTypes } from '../actions/api-features.actions'; + +export interface ApiFeatureSet { + [key: string]: boolean | {[key: string]: boolean}; }; -export const initialState = []; +export const initialState = { + 'metadata_patterns': true, + 'log_level_filters': true +}; -export function reducer(state = initialState, action: AuditLogReposActions) { +export function reducer(state = initialState, action: ApiFeaturesActions) { switch (action.type) { - case AuditLogReposActionTypes.SET: { - const components: AuditLogRepo[] = action.payload; - return components || []; + case ApiFeaturesActionTypes.SET: { + return action.payload; } default: { return state; diff --git a/ambari-logsearch-web/src/app/store/reducers/audit-log-repos.reducers.ts b/ambari-logsearch-web/src/app/store/reducers/audit-log-repos.reducers.ts index c04ff3b..ddeb9a6 100644 --- a/ambari-logsearch-web/src/app/store/reducers/audit-log-repos.reducers.ts +++ b/ambari-logsearch-web/src/app/store/reducers/audit-log-repos.reducers.ts @@ -1,5 +1,3 @@ -import { AuditLogReposActions, AuditLogReposActionTypes } from "../actions/audit-log-repos.actions"; - /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -18,6 +16,8 @@ import { AuditLogReposActions, AuditLogReposActionTypes } from "../actions/audit * limitations under the License. */ +import { AuditLogReposActions, AuditLogReposActionTypes } from "../actions/audit-log-repos.actions"; + export interface AuditLogRepo { name: string; label: string; diff --git a/ambari-logsearch-web/src/app/store/reducers/audit-log-repos.reducers.ts b/ambari-logsearch-web/src/app/store/selectors/api-features.selectors.ts similarity index 58% copy from ambari-logsearch-web/src/app/store/reducers/audit-log-repos.reducers.ts copy to ambari-logsearch-web/src/app/store/selectors/api-features.selectors.ts index c04ff3b..2d75be1 100644 --- a/ambari-logsearch-web/src/app/store/reducers/audit-log-repos.reducers.ts +++ b/ambari-logsearch-web/src/app/store/selectors/api-features.selectors.ts @@ -1,5 +1,3 @@ -import { AuditLogReposActions, AuditLogReposActionTypes } from "../actions/audit-log-repos.actions"; - /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -18,21 +16,19 @@ import { AuditLogReposActions, AuditLogReposActionTypes } from "../actions/audit * limitations under the License. */ -export interface AuditLogRepo { - name: string; - label: string; -}; +import { createSelector, Selector } from 'reselect'; + +import { AppStore } from '@app/classes/models/store'; +import { ApiFeatureSet } from '@app/store/reducers/api-features.reducers'; + +export const selectApiFeaturesState = (state: AppStore): ApiFeatureSet => state.apiFeatures; -export const initialState = []; +export const selectMetadataPatternsFeatureState = createSelector( + selectApiFeaturesState, + (state: ApiFeatureSet) => state.metadata_patterns +); -export function reducer(state = initialState, action: AuditLogReposActions) { - switch (action.type) { - case AuditLogReposActionTypes.SET: { - const components: AuditLogRepo[] = action.payload; - return components || []; - } - default: { - return state; - } - } -} +export const selectLogLevelFiltersFeatureState = createSelector( + selectApiFeaturesState, + (state: ApiFeatureSet) => state.log_level_filters +); \ No newline at end of file diff --git a/ambari-logsearch-web/src/assets/i18n/en.json b/ambari-logsearch-web/src/assets/i18n/en.json index 6ae7a44..9d21f8d 100644 --- a/ambari-logsearch-web/src/assets/i18n/en.json +++ b/ambari-logsearch-web/src/assets/i18n/en.json @@ -393,6 +393,10 @@ "success": "The user settings has been saved.", "error": "Some error happened saving the user settings." } + }, + "apiFeatures": { + "loadFailed": "We were not able to explore the available features from the server. Some of the features maybe not disabled in the client but disabled on the server.", + "disabled": "This feature is disabled on the server." } }