You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by GitBox <gi...@apache.org> on 2018/11/13 15:26:04 UTC
[GitHub] tobias-istvan closed pull request #22: [AMBARI-24551] [Log Search
UI] get rid of redundant requests after undoing or redoing several history
steps
tobias-istvan closed pull request #22: [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps
URL: https://github.com/apache/ambari-logsearch/pull/22
This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:
As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):
diff --git a/ambari-logsearch-web/src/app/app-routing.module.ts b/ambari-logsearch-web/src/app/app-routing.module.ts
index a55e51a81f..5c8c4a1c19 100644
--- a/ambari-logsearch-web/src/app/app-routing.module.ts
+++ b/ambari-logsearch-web/src/app/app-routing.module.ts
@@ -16,14 +16,15 @@
* limitations under the License.
*/
-import {NgModule} from '@angular/core';
-import {RouterModule, Routes} from '@angular/router';
-import {LogsContainerComponent} from '@app/components/logs-container/logs-container.component';
-import {LoginFormComponent} from '@app/components/login-form/login-form.component';
-import {AuthGuardService} from '@app/services/auth-guard.service';
-import {TabGuard} from '@app/services/tab.guard';
-import {LogsBreadcrumbsResolverService} from '@app/services/logs-breadcrumbs-resolver.service';
-import {LoginScreenGuardService} from '@app/services/login-screen-guard.service';
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { LogsContainerComponent } from '@app/components/logs-container/logs-container.component';
+import { LoginFormComponent } from '@app/components/login-form/login-form.component';
+import { AuthGuardService } from '@app/services/auth-guard.service';
+import { TabGuard } from '@app/services/tab.guard';
+import { FilterHistoryIndexGuard } from '@app/services/filter-history.guard';
+import { LogsBreadcrumbsResolverService } from '@app/services/logs-breadcrumbs-resolver.service';
+import { LoginScreenGuardService } from '@app/services/login-screen-guard.service';
const appRoutes: Routes = [{
path: 'login',
@@ -43,7 +44,7 @@ const appRoutes: Routes = [{
resolve: {
breadcrumbs: LogsBreadcrumbsResolverService
},
- canActivate: [AuthGuardService, TabGuard]
+ canActivate: [AuthGuardService, TabGuard, FilterHistoryIndexGuard]
}, {
path: 'logs',
redirectTo: '/logs/serviceLogs',
diff --git a/ambari-logsearch-web/src/app/app.module.ts b/ambari-logsearch-web/src/app/app.module.ts
index 097bf04402..9026f67c07 100644
--- a/ambari-logsearch-web/src/app/app.module.ts
+++ b/ambari-logsearch-web/src/app/app.module.ts
@@ -59,7 +59,6 @@ import {ServiceLogsFieldsService} from '@app/services/storage/service-logs-field
import {AuditLogsFieldsService} from '@app/services/storage/audit-logs-fields.service';
import {TabsService} from '@app/services/storage/tabs.service';
import {AuthService} from '@app/services/auth.service';
-import {HistoryManagerService} from '@app/services/history-manager.service';
import {reducer} from '@app/services/storage/reducers.service';
import {AppComponent} from '@app/components/app.component';
@@ -109,6 +108,7 @@ import {ClusterSelectionService} from '@app/services/storage/cluster-selection.s
import {TranslateService as AppTranslateService} from '@app/services/translate.service';
import {RoutingUtilsService} from '@app/services/routing-utils.service';
import {TabGuard} from '@app/services/tab.guard';
+import {FilterHistoryIndexGuard} from '@app/services/filter-history.guard';
import {LogsBreadcrumbsResolverService} from '@app/services/logs-breadcrumbs-resolver.service';
import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.service';
import {LogsStateService} from '@app/services/storage/logs-state.service';
@@ -116,6 +116,7 @@ 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';
+import { FilterHistoryManagerComponent } from './components/filter-history-manager/filter-history-manager.component';
@NgModule({
declarations: [
@@ -158,7 +159,8 @@ import { NotificationEffects } from '@app/store/effects/notification.effects';
TimerSecondsPipe,
ComponentLabelPipe,
BreadcrumbsComponent,
- ClusterFilterComponent
+ ClusterFilterComponent,
+ FilterHistoryManagerComponent
],
imports: [
BrowserModule,
@@ -217,10 +219,10 @@ import { NotificationEffects } from '@app/store/effects/notification.effects';
AuditLogsFieldsService,
TabsService,
TabGuard,
+ FilterHistoryIndexGuard,
LogsBreadcrumbsResolverService,
AuthService,
AuthGuardService,
- HistoryManagerService,
ClusterSelectionService,
LogsFilteringUtilsService,
LogsStateService,
diff --git a/ambari-logsearch-web/src/app/classes/models/filter-url-param-change.interface.ts b/ambari-logsearch-web/src/app/classes/models/filter-url-param-change.interface.ts
new file mode 100644
index 0000000000..2428fc0da6
--- /dev/null
+++ b/ambari-logsearch-web/src/app/classes/models/filter-url-param-change.interface.ts
@@ -0,0 +1,23 @@
+/**
+ * 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.
+ */
+
+export interface FilterUrlParamChange {
+ previousPath?: string | null;
+ currentPath: string;
+ time?: Date;
+}
diff --git a/ambari-logsearch-web/src/app/classes/models/store.ts b/ambari-logsearch-web/src/app/classes/models/store.ts
index f106b17a2b..3811de1e48 100644
--- a/ambari-logsearch-web/src/app/classes/models/store.ts
+++ b/ambari-logsearch-web/src/app/classes/models/store.ts
@@ -35,6 +35,7 @@ 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';
+import * as filterHistory from '@app/store/reducers/filter-history.reducers';
const storeActions = {
'ARRAY.ADD': 'ADD',
@@ -71,6 +72,7 @@ export interface AppStore {
logsState: LogsState;
dataAvailabilityStates: DataAvaibilityStatesModel;
auth: auth.State;
+ filterHistory: filterHistory.FilterHistoryState;
}
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 e64a89c8c1..f8c65de82c 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
@@ -14,19 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<!-- TODO use listClass="history-dropdown" for custom styling -->
-<menu-button label="{{'topMenu.undo' | translate}}" [subItems]="undoItems" iconClass="fa fa-arrow-left"
- class="history-menu" [class.disabled]="!undoItems.length" [isDisabled]="!undoItems.length"
- listClass="history-dropdown" (buttonClick)="undoLatest()" (selectItem)="undo($event)">
-</menu-button>
-<menu-button label="{{'topMenu.redo' | translate}}" [subItems]="redoItems" iconClass="fa fa-arrow-right"
- class="history-menu" [class.disabled]="!redoItems.length" [isDisabled]="!redoItems.length"
- listClass="history-dropdown" (buttonClick)="redoLatest()" (selectItem)="redo($event)">
-</menu-button>
-<menu-button label="{{'topMenu.history' | translate}}" [subItems]="historyItems" iconClass="fa fa-history"
- class="history-menu" [class.disabled]="!historyItems.length" [isDisabled]="!historyItems.length"
- listClass="history-dropdown" [isRightAlign]="true"></menu-button>
+<filter-history-manager></filter-history-manager>
<menu-button label="{{'topMenu.filter' | translate}}" iconClass="fa fa-filter"
(buttonClick)="openLogIndexFilter()"></menu-button>
<menu-button *ngIf="!captureSeconds" label="{{'filter.capture' | translate}}" iconClass="fa fa-caret-right"
diff --git a/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.less b/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.less
index a8c6e05d61..db9a86bc2b 100644
--- a/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.less
+++ b/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.less
@@ -19,81 +19,11 @@
:host {
display: block;
- menu-button {
+ /deep/ menu-button {
margin: 0 1em;
/deep/ .stop-icon {
color: @exclude-color;
}
- &.history-menu {
- /deep/ ul {
- li:not(.selection-all) {
- margin: 0;
- overflow: hidden;
- position: relative;
- transition: background-color 300ms ease-in, opacity 300ms ease-in, height 100ms 400ms ease-in;
- &:before {
- border-left: 1px solid darken(@unknown-color, 25%);
- bottom: 0;
- content: "";
- display: block;
- left: 12px;
- position: absolute;
- top: 0;
- }
- &:after {
- background: #fff;
- border: 1px solid darken(@unknown-color, 25%);
- border-radius: 100%;
- content: "";
- height: 12px;
- left: 7px;
- position: absolute;
- top: 6px;
- transition: background-color 300ms;
- width: 12px;
- }
-
- .list-item-label.label-container {
- border-radius: 3px;
- display: flex;
- margin: 0 3px 0 25px;
- padding: 3px 25px 3px 1em;
- .item-label-text {
- flex-grow: 1;
- padding-right: 1em;
- }
- /deep/ history-item-controls {
- float: none;
- justify-self: right;
- }
- }
-
- &.active > a, &:hover {
- color: #262626;
- text-decoration: none;
- background-color: transparent;
- .list-item-label.label-container {
- background-color: #f5f5f5;
- }
- }
- }
- li:not(.selection-all):first-child {
- &:before {
- top: 50%;
- }
- }
- li:not(.selection-all):last-child {
- &:before {
- bottom: 50%;
- }
- }
- li:not(.selection-all):hover {
- &:after {
- background: @unknown-color;
- }
- }
- }
- }
}
/deep/ .modal-body {
min-height: 25vh;
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 46a0a765bf..721ae93d74 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,14 +22,13 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
-import { Subscription } from 'rxjs/Subscription';
import { LogsContainerService } from '@app/services/logs-container.service';
-import { HistoryManagerService } from '@app/services/history-manager.service';
import { UserSettingsService } from '@app/services/user-settings.service';
import { ListItem } from '@app/classes/list-item';
import { ClustersService } from '@app/services/storage/clusters.service';
import { UtilsService } from '@app/services/utils.service';
+import { Subject } from 'rxjs/Subject';
@Component({
selector: 'action-menu',
@@ -60,11 +59,10 @@ export class ActionMenuComponent implements OnInit, OnDestroy {
selectedClusterName$: BehaviorSubject<string> = new BehaviorSubject('');
- subscriptions: Subscription[] = [];
+ destroyed$ = new Subject();
constructor(
private logsContainerService: LogsContainerService,
- private historyManager: HistoryManagerService,
private settings: UserSettingsService,
private route: ActivatedRoute,
private router: Router,
@@ -74,27 +72,13 @@ export class ActionMenuComponent implements OnInit, OnDestroy {
}
ngOnInit() {
- this.subscriptions.push(
- this.selectedClusterName$.subscribe(
- (clusterName: string) => this.setModalSubmitDisabled(!(!!clusterName))
- )
+ this.selectedClusterName$.takeUntil(this.destroyed$).subscribe(
+ (clusterName: string) => this.setModalSubmitDisabled(!clusterName)
);
}
ngOnDestroy() {
- this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
- }
-
- get undoItems(): ListItem[] {
- return this.historyManager.undoItems;
- }
-
- get redoItems(): ListItem[] {
- return this.historyManager.redoItems;
- }
-
- get historyItems(): ListItem[] {
- return this.historyManager.activeHistory;
+ this.destroyed$.next(true);
}
get captureSeconds(): number {
@@ -105,26 +89,6 @@ export class ActionMenuComponent implements OnInit, OnDestroy {
this.isModalSubmitDisabled = isDisabled;
}
- undoLatest(): void {
- if (this.undoItems.length) {
- this.historyManager.undo(this.undoItems[0]);
- }
- }
-
- redoLatest(): void {
- if (this.redoItems.length) {
- this.historyManager.redo(this.redoItems[0]);
- }
- }
-
- undo(item: ListItem): void {
- this.historyManager.undo(item);
- }
-
- redo(item: ListItem): void {
- this.historyManager.redo(item);
- }
-
refresh(): void {
this.logsContainerService.loadLogs();
}
diff --git a/ambari-logsearch-web/src/app/components/app.component.html b/ambari-logsearch-web/src/app/components/app.component.html
index 47d461b60f..f788ab760f 100644
--- a/ambari-logsearch-web/src/app/components/app.component.html
+++ b/ambari-logsearch-web/src/app/components/app.component.html
@@ -29,4 +29,8 @@ <h1>{{'common.title' | translate}}</h1>
<main-container *ngIf="!(isAuthorized$ | async) || (isBaseDataAvailable$ | async)"></main-container>
<simple-notifications [options]="notificationServiceOptions"></simple-notifications>
+ <div class="request-indicator" [class.open]="httpClient.requestInProgress | async">
+ <i class="fa fa-spin fa-gear"></i>
+ {{ 'common.loading' | translate }}
+ </div>
</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 3e566719ed..5d51fa8340 100644
--- a/ambari-logsearch-web/src/app/components/app.component.less
+++ b/ambari-logsearch-web/src/app/components/app.component.less
@@ -66,4 +66,29 @@
justify-content: center;
margin: 1rem 0;
}
+
+ .request-indicator {
+ background: rgba(255,255,255,.7);
+ top: calc(-1 * (3em + .3em));
+ color: @info-color;
+ left: 50%;
+ margin-left: auto;
+ margin-right: auto;
+ opacity: .7;
+ padding: .3em;
+ position: fixed;
+ transition: top 500ms ease-in-out;
+ transform: translateX(-50%);
+ z-index: 1200;
+ &.open {
+ top: 0;
+ }
+ // &:before {
+ // .circle-spinner(1em, 2px, @info-color);
+ // content: ' ';
+ // display: inline-block;
+ // line-height: 1.1em;
+ // }
+ }
+
}
diff --git a/ambari-logsearch-web/src/app/components/app.component.ts b/ambari-logsearch-web/src/app/components/app.component.ts
index 68d220e464..1d6e29ef50 100644
--- a/ambari-logsearch-web/src/app/components/app.component.ts
+++ b/ambari-logsearch-web/src/app/components/app.component.ts
@@ -29,7 +29,9 @@ 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';
+import { isAuthorizedSelector, selectAuthStatus, isCheckingAuthStatusInProgressSelector } from '@app/store/selectors/auth.selectors';
+
+import { HttpClientService } from '@app/services/http-client.service';
@Component({
selector: 'app-root',
@@ -44,7 +46,7 @@ export class AppComponent implements OnInit, OnDestroy {
authorizationStatuses = AuthorizationStatuses;
isAuthorized$: Observable<boolean> = this.store.select(isAuthorizedSelector);
- authorizationStatus$: Observable<AuthorizationStatuses> = this.store.select(authStatusSelector);
+ authorizationStatus$: Observable<AuthorizationStatuses> = this.store.select(selectAuthStatus);
isCheckingAuthStatusInProgress$: Observable<boolean> = this.store.select(isCheckingAuthStatusInProgressSelector);
authorizationCode$: Observable<number> = this.appState.getParameter('authorizationCode');
isBaseDataAvailable$: Observable<boolean> = this.appState.getParameter('baseDataSetState')
@@ -66,6 +68,7 @@ export class AppComponent implements OnInit, OnDestroy {
constructor(
private appState: AppStateService,
+ public httpClient: HttpClientService,
private store: Store<AppStore>
) {}
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 086160be17..391d1171f9 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
@@ -128,10 +128,10 @@ export class ClusterFilterComponent implements OnInit, OnDestroy {
.filter((state: DataAvailabilityValues) => state === DataAvailabilityValues.AVAILABLE)
.first()
.subscribe(() => {
- this.filterDropdown.updateSelection(clusterSelection);
+ this.filterDropdown.writeValue(clusterSelection);
});
} else {
- this.filterDropdown.updateSelection(null);
+ this.filterDropdown.clearSelection();
}
}
diff --git a/ambari-logsearch-web/src/app/components/filter-button/filter-button.component.ts b/ambari-logsearch-web/src/app/components/filter-button/filter-button.component.ts
index af14925bd2..d6f24e5291 100644
--- a/ambari-logsearch-web/src/app/components/filter-button/filter-button.component.ts
+++ b/ambari-logsearch-web/src/app/components/filter-button/filter-button.component.ts
@@ -19,7 +19,6 @@
import {Component, forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {ListItem} from '@app/classes/list-item';
-import {UtilsService} from '@app/services/utils.service';
import {MenuButtonComponent} from '@app/components/menu-button/menu-button.component';
@Component({
@@ -36,65 +35,23 @@ import {MenuButtonComponent} from '@app/components/menu-button/menu-button.compo
})
export class FilterButtonComponent extends MenuButtonComponent implements ControlValueAccessor {
- private selectedItems: ListItem[] = [];
-
private onChange: (fn: any) => void;
- constructor(private utils: UtilsService) {
- super();
- }
-
- get selection(): ListItem[] {
- return this.selectedItems;
- }
-
- set selection(items: ListItem[]) {
- this.selectedItems = items;
- if (this.onChange) {
- this.onChange(items);
+ updateSelection(items: ListItem | ListItem[], callOnChange = true): void {
+ super.updateSelection(items);
+ if (callOnChange) {
+ this._onChange(this.selection);
}
}
- updateSelection(updates: ListItem | ListItem[]): void {
- if (updates && (!Array.isArray(updates) || updates.length)) {
- const items: ListItem[] = Array.isArray(updates) ? updates : [updates];
- if (this.isMultipleChoice) {
- items.forEach((item: ListItem) => {
- if (this.subItems && this.subItems.length) {
- const itemToUpdate: ListItem = this.subItems.find((option: ListItem) => this.utils.isEqual(option.value, item.value));
- if (itemToUpdate) {
- itemToUpdate.isChecked = item.isChecked;
- }
- }
- });
- } else {
- const selectedItem: ListItem = items.find((item: ListItem) => item.isChecked);
- this.subItems.forEach((item: ListItem) => {
- item.isChecked = !!selectedItem && this.utils.isEqual(item.value, selectedItem.value);
- });
- }
- } else {
- this.subItems.forEach((item: ListItem) => item.isChecked = false);
- }
- const checkedItems = this.subItems.filter((option: ListItem): boolean => option.isChecked);
- this.selection = checkedItems;
- this.selectItem.emit(checkedItems.map((option: ListItem): any => option.value));
- if (this.dropdownList) {
- this.dropdownList.doItemsCheck();
+ private _onChange(value) {
+ if (this.onChange) {
+ this.onChange(value);
}
}
writeValue(items: ListItem[]) {
- let listItems: ListItem[] = [];
- if (items && items.length) {
- listItems = items.map((item: ListItem) => {
- return {
- ...item,
- isChecked: true
- };
- });
- }
- this.updateSelection(listItems);
+ this.selection = items;
}
registerOnChange(callback: any): void {
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
new file mode 100644
index 0000000000..c34a46e5e2
--- /dev/null
+++ b/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.html
@@ -0,0 +1,29 @@
+<!--
+ 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.
+-->
+<menu-button label="{{'topMenu.undo' | translate}}" [subItems]="activeUndoHistoryListItems$ | async" iconClass="fa fa-arrow-left"
+ class="history-menu" [class.disabled]="!(hasActiveUndoHistoryItems$ | async)" [isDisabled]="!(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)"
+ 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)"
+ listClass="history-dropdown" [isRightAlign]="true" (selectItem)="onListItemClick($event)"></menu-button>
diff --git a/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.less b/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.less
new file mode 100644
index 0000000000..1a0cd46950
--- /dev/null
+++ b/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.less
@@ -0,0 +1,114 @@
+/**
+ * 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 '../../modules/shared/variables';
+
+ @current-history-item-hover-color: @unknown-color;
+ @current-history-item-highlight-color: @form-success-color;
+
+ :host {
+ /deep/ menu-button .item-label-text {
+ display: inline-block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ width: 100%;
+ &:first-letter {
+ text-transform: uppercase;
+ }
+ }
+
+ .history-menu {
+ /deep/ ul {
+ li:not(.selection-all) {
+ margin: 0;
+ overflow: hidden;
+ position: relative;
+ transition: background-color 300ms ease-in, opacity 300ms ease-in, height 100ms 400ms ease-in;
+ &:before {
+ border-left: 1px solid darken(@unknown-color, 25%);
+ bottom: 0;
+ content: "";
+ display: block;
+ left: 12px;
+ position: absolute;
+ top: 0;
+ }
+ &:after {
+ background: #fff;
+ border: 1px solid darken(@unknown-color, 25%);
+ border-radius: 100%;
+ content: "";
+ height: 12px;
+ left: 7px;
+ position: absolute;
+ top: 6px;
+ transition: background-color 300ms;
+ width: 12px;
+ }
+
+ .list-item-label.label-container {
+ border-radius: 3px;
+ display: flex;
+ margin: 0 3px 0 25px;
+ padding: 3px 25px 3px 1em;
+ .item-label-text {
+ flex-grow: 1;
+ padding-right: 1em;
+ }
+ /deep/ history-item-controls {
+ float: none;
+ justify-self: right;
+ }
+ }
+
+ &.active > a, &:hover {
+ color: #262626;
+ text-decoration: none;
+ background-color: transparent;
+ .list-item-label.label-container {
+ background-color: #f5f5f5;
+ }
+ }
+ &.initial {
+ color: @unknown-color;
+ }
+ }
+ li:not(.selection-all):first-child {
+ &:before {
+ top: 50%;
+ }
+ }
+ li:not(.selection-all):last-child {
+ &:before {
+ bottom: 50%;
+ }
+ }
+ li:not(.selection-all):hover {
+ &:after {
+ background: @current-history-item-hover-color;
+ }
+ }
+ li:not(.selection-all).active {
+ &:after {
+ background: @current-history-item-highlight-color;
+ }
+ }
+ }
+ }
+
+ }
diff --git a/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.spec.ts b/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.spec.ts
new file mode 100644
index 0000000000..2f2e738582
--- /dev/null
+++ b/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.spec.ts
@@ -0,0 +1,296 @@
+/**
+ * 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 { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import {
+ async,
+ ComponentFixture,
+ TestBed,
+ fakeAsync
+} from '@angular/core/testing';
+
+import {
+ getCommonTestingBedConfiguration,
+ TranslationModules
+} from '@app/test-config.spec';
+import { StoreModule } from '@ngrx/store';
+
+import {
+ appState,
+ 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 { LogsFilteringUtilsService } from '@app/services/logs-filtering-utils.service';
+
+import { FilterHistoryManagerComponent } from './filter-history-manager.component';
+import { FilterUrlParamChange } from '@app/classes/models/filter-url-param-change.interface';
+import { Router, Routes, NavigationEnd } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('FilterHistoryManagerComponent', () => {
+ let component: FilterHistoryManagerComponent;
+ let fixture: ComponentFixture<FilterHistoryManagerComponent>;
+ let router: Router;
+ const getValueLabelTestCases = {
+ level: [{
+ input: 'ERROR',
+ expectation: 'Error'
+ }, {
+ input: 'ERROR,FATAL',
+ expectation: 'Error, Fatal'
+ }],
+ levels: [{
+ input: 'ERROR',
+ expectation: 'Error'
+ }, {
+ input: 'ERROR,FATAL',
+ expectation: 'Error, Fatal'
+ }],
+ log_message: [{
+ input: 'Exception',
+ expectation: '"Exception"'
+ }],
+ type: [{
+ input: 'infra_solr',
+ expectation: 'Infra Solr'
+ }],
+ components: [{
+ input: 'infra_solr',
+ expectation: 'Infra Solr'
+ }]
+ };
+ const componentNameLabes = {
+ infra_solr: 'Infra Solr'
+ };
+
+ const getParametersFromUrlTestCases = {
+ 'logs/serviceLogs': {},
+ 'logs/serviceLogs;a=1;b=2': {a: '1', b: '2'},
+ 'logs/serviceLogs;a=1;b=2?c=3': {a: '1', b: '2'}
+ };
+
+ const getParameterDifferencesFromUrlsTestCases = [{
+ previousUrl: 'logs/serviceLogs;a=1',
+ currentUrl: 'logs/serviceLogs;a=1;b=2',
+ expectation: [{
+ type: 'add',
+ name: 'b',
+ from: undefined,
+ to: '2'
+ }]
+ }, {
+ previousUrl: 'logs/serviceLogs;a=1;b=2',
+ currentUrl: 'logs/serviceLogs;a=1',
+ expectation: [{
+ type: 'remove',
+ name: 'b',
+ from: '2',
+ to: undefined
+ }]
+ }, {
+ previousUrl: 'logs/serviceLogs;a=1',
+ currentUrl: 'logs/serviceLogs;a=2',
+ expectation: [{
+ type: 'change',
+ name: 'a',
+ from: '1',
+ to: '2'
+ }]
+ }];
+
+ const extractParametersFromUrlSegmentGroupUseCases = [{
+ caseLabel: 'Single level parameters',
+ url: 'logs/serviceLogs;a=1;b=2',
+ expectation: {
+ a: '1',
+ b: '2'
+ }
+ }, {
+ caseLabel: 'Multi level parameters',
+ url: 'logs;a=1/serviceLogs;b=2',
+ expectation: {
+ a: '1',
+ b: '2'
+ }
+ }];
+
+ const routes: Routes = [
+ {
+ path: 'logs/:activeTab',
+ component: FilterHistoryManagerComponent
+ }
+ ];
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ ...getCommonTestingBedConfiguration({
+ imports: [
+ RouterTestingModule.withRoutes(routes),
+ ...TranslationModules,
+ StoreModule.provideStore({
+ appState
+ })
+ ],
+ providers: [
+ AppStateService,
+ NotificationsService,
+ NotificationService,
+ LogsFilteringUtilsService
+ ],
+ declarations: [FilterHistoryManagerComponent]
+ }),
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ router = TestBed.get(Router);
+ fixture = TestBed.createComponent(FilterHistoryManagerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should call the router`s `navigateByUrl` method when calling `undo` with a FilterUrlParamChange item', () => {
+ const path = '/logs/serviceLogs;_fhi=0';
+ const historyItem: FilterUrlParamChange = {
+ currentPath: path
+ };
+ spyOn(router, 'navigateByUrl').and.callThrough();
+ component.undo(historyItem);
+ expect(router.navigateByUrl).toHaveBeenCalledWith(path);
+ });
+
+ it('should navigate to the currentPath when calling `undo` with a FilterUrlParamChange item', fakeAsync(() => {
+ const path = '/logs/serviceLogs;_fhi=0';
+ const historyItem: FilterUrlParamChange = {
+ currentPath: path
+ };
+ router.events.filter(event => event instanceof NavigationEnd).first().subscribe((event) => {
+ expect(router.url).toEqual(path);
+ });
+ component.undo(historyItem);
+ }));
+
+ it('should call the router`s `navigateByUrl` method when calling `redo` with a FilterUrlParamChange item', () => {
+ const path = '/logs/serviceLogs;_fhi=0';
+ const historyItem: FilterUrlParamChange = {
+ currentPath: path
+ };
+ spyOn(router, 'navigateByUrl').and.callThrough();
+ component.redo(historyItem);
+ expect(router.navigateByUrl).toHaveBeenCalledWith(path);
+ });
+
+ it('should navigate to the currentPath when calling `redo` with a FilterUrlParamChange item', fakeAsync(() => {
+ const path = '/logs/serviceLogs;_fhi=0';
+ const historyItem: FilterUrlParamChange = {
+ currentPath: path
+ };
+ router.events.filter(event => event instanceof NavigationEnd).first().subscribe((event) => {
+ expect(router.url).toEqual(path);
+ });
+ component.redo(historyItem);
+ }));
+
+ it(
+ 'should call the router`s `navigateByUrl` method when calling `navigateToFilterUrlParamChangeItem with a FilterUrlParamChange item',
+ () => {
+ const path = '/logs/serviceLogs;_fhi=0';
+ const historyItem: FilterUrlParamChange = {
+ currentPath: path
+ };
+ spyOn(router, 'navigateByUrl').and.callThrough();
+ component.navigateToFilterUrlParamChangeItem(historyItem);
+ expect(router.navigateByUrl).toHaveBeenCalledWith(path);
+ });
+
+ it(
+ 'should navigate to the currentPath when calling `navigateToFilterUrlParamChangeItem` with a FilterUrlParamChange item',
+ fakeAsync(() => {
+ const path = '/logs/serviceLogs;_fhi=0';
+ const historyItem: FilterUrlParamChange = {
+ currentPath: path
+ };
+ router.events.filter(event => event instanceof NavigationEnd).first().subscribe((event) => {
+ expect(router.url).toEqual(path);
+ });
+ component.navigateToFilterUrlParamChangeItem(historyItem);
+ }));
+
+ describe('testing `getValueLabel`', () => {
+ Object.keys(getValueLabelTestCases).forEach((key) => {
+ const cases: {input: any, expectation: any}[] = getValueLabelTestCases[key];
+ cases.forEach((currentCase: {input: any, expectation: any}) => {
+ it(`should give correct value label for ${key} field when the value is ${currentCase.input}`, () => {
+ component.componentsLabelsLocalCopy$.next(componentNameLabes);
+ const valueLabel = component.getValueLabel(key, currentCase.input);
+ expect(valueLabel).toEqual(currentCase.expectation);
+ });
+ });
+ });
+ });
+
+ describe('testing `getParametersFromUrl`', () => {
+ Object.keys(getParametersFromUrlTestCases).forEach((url: string) => {
+ const expectation = getParametersFromUrlTestCases[url];
+ Object.keys(expectation).forEach((paramKey) => {
+ it(`should parse parameter ${paramKey} with value ${expectation[paramKey]}`, () => {
+ const result = component.getParametersFromUrl(url);
+ expect(result[paramKey]).toEqual(expectation[paramKey]);
+ });
+ });
+ });
+ });
+
+ describe('testing `getParameterDifferencesFromUrls', () => {
+ getParameterDifferencesFromUrlsTestCases.forEach((useCase) => {
+ describe(`should return with correct diff for ${useCase.previousUrl} vs ${useCase.currentUrl}`, () => {
+ const expectation = useCase.expectation;
+ expectation.forEach((expectationDiff) => {
+ it(`should find difference ${expectationDiff.type} - ${expectationDiff.from} -> ${expectationDiff.to}`, fakeAsync(() => {
+ const diff = component.getParameterDifferencesFromUrls(useCase.currentUrl, useCase.previousUrl, 'serviceLogs');
+ const found = diff.some((foundDiff) => (
+ foundDiff.type === expectationDiff.type
+ && foundDiff.from === expectationDiff.from
+ && foundDiff.to === expectationDiff.to
+ && foundDiff.name === expectationDiff.name
+ ));
+ expect(found).toEqual(true);
+ }));
+ });
+ });
+ });
+ });
+
+ describe('testing `extractParametersFromUrlSegmentGroup`', () => {
+ extractParametersFromUrlSegmentGroupUseCases.forEach((useCase) => {
+ it(`should find parameters for ${useCase.caseLabel}`, () => {
+ const urlSegmentGroup = router.parseUrl(useCase.url);
+ const foundParameters = component.extractParametersFromUrlSegmentGroup(urlSegmentGroup.root);
+ Object.keys(useCase.expectation).forEach((paramKey) => {
+ expect(foundParameters[paramKey]).toEqual(useCase.expectation[paramKey]);
+ });
+ });
+ });
+ });
+
+});
diff --git a/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.ts b/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.ts
new file mode 100644
index 0000000000..ddf0df5f35
--- /dev/null
+++ b/ambari-logsearch-web/src/app/components/filter-history-manager/filter-history-manager.component.ts
@@ -0,0 +1,381 @@
+import { Component, OnInit, OnDestroy, Input } from '@angular/core';
+import { Subject } from 'rxjs/Subject';
+import { Observable } from 'rxjs/Observable';
+import { Store } from '@ngrx/store';
+import { AppStore } from '@app/classes/models/store';
+import {
+ selectActiveFilterHistoryChangesUndoItems,
+ selectActiveFilterHistoryChangesRedoItems,
+ selectActiveFilterHistoryChanges,
+ selectActiveFilterHistoryChangeIndex
+} from '@app/store/selectors/filter-history.selectors';
+import { FilterUrlParamChange } from '@app/classes/models/filter-url-param-change.interface';
+import { Router, UrlTree, UrlSegmentGroup } from '@angular/router';
+import {
+ LogsFilteringUtilsService,
+ defaultUrlParamsForFiltersByLogsType,
+ UrlParamDifferences,
+ UrlParamsDifferenceType
+} from '@app/services/logs-filtering-utils.service';
+import { selectActiveLogsType } from '@app/store/selectors/app-state.selectors';
+import { LogsType } from '@app/classes/string';
+import { TranslateService } from '@ngx-translate/core';
+import { selectComponentsLabels } from '@app/store/selectors/components.selectors';
+import { selectDefaultAuditLogsFields } from '@app/store/selectors/audit-logs-fields.selectors';
+import { selectServiceLogsFieldState } from '@app/store/selectors/service-logs-fields.selectors';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import { ListItem } from '@app/classes/list-item';
+
+import * as moment from 'moment';
+import { SearchBoxParameter } from '@app/classes/filtering';
+import { LogField } from '@app/classes/object';
+
+export const urlParamsActionType = {
+ clusters: 'multiple',
+ timeRange: 'single',
+ components: 'multiple',
+ levels: 'multiple',
+ hosts: 'multiple',
+ sortingKey: 'single',
+ sortingType: 'single',
+ pageSize: 'single',
+ page: 'single',
+ query: 'query',
+ users: 'multiple'
+};
+
+@Component({
+ selector: 'filter-history-manager',
+ templateUrl: './filter-history-manager.component.html',
+ styleUrls: ['./filter-history-manager.component.less']
+})
+export class FilterHistoryManagerComponent implements OnInit, OnDestroy {
+
+ @Input()
+ labelSeparator = ' | ';
+
+ activeLogsType$: Observable<LogsType> = this.store.select(selectActiveLogsType);
+
+ componentsLabels$: Observable<{[key: string]: string}> = this.store.select(selectComponentsLabels);
+ componentsLabelsLocalCopy$: BehaviorSubject<{[key: string]: string}> = new BehaviorSubject({});
+
+ activeHistoryChangeIndex$: Observable<number> = this.store.select(selectActiveFilterHistoryChangeIndex);
+
+ activeHistoryItems$: Observable<FilterUrlParamChange[]> = this.store.select(selectActiveFilterHistoryChanges);
+ hasActiveHistoryItems$: Observable<boolean> = this.activeHistoryItems$
+ .map(items => items && items.length > 0).startWith(false);
+ activeHistoryItemLabels$: Observable<{[key: string]: string}[]> = Observable.combineLatest(
+ this.activeHistoryItems$,
+ this.activeLogsType$,
+ this.componentsLabels$ // this is just to recalculate the labels when the components arrived
+ ).map(result => this.mapHistoryItemsToHistoryItemLabels(result));
+ activeHistoryListItems$: Observable<ListItem[]> = Observable.combineLatest(
+ this.activeHistoryItemLabels$.map((items) => this.mapHistoryItemLabelsToListItems(items, this.labelSeparator)),
+ this.store.select(selectActiveFilterHistoryChangeIndex)
+ ).map(([listItems, changeIndex]: [ListItem[], number]): ListItem[] => listItems.map((item, index) => {
+ item.cssClass = index === changeIndex ? 'active' : (index === 0 ? 'initial' : '');
+ return item;
+ }));
+
+ activeUndoHistoryItems$: Observable<FilterUrlParamChange[]> = this.store.select(selectActiveFilterHistoryChangesUndoItems);
+ hasActiveUndoHistoryItems$: Observable<boolean> = this.activeUndoHistoryItems$
+ .map(items => items && items.length > 0).startWith(false);
+ activeUndoHistoryListItems$: Observable<ListItem[]> = Observable.combineLatest(
+ this.activeHistoryListItems$,
+ this.store.select(selectActiveFilterHistoryChangeIndex)
+ ).map(([listItems, activeChangeIndex]: [ListItem[], number]): ListItem[] => listItems.slice(0, activeChangeIndex).reverse());
+
+ activeRedoHistoryItems$: Observable<FilterUrlParamChange[]> = this.store.select(selectActiveFilterHistoryChangesRedoItems);
+ hasActiveRedoHistoryItems$: Observable<boolean> = this.activeRedoHistoryItems$
+ .map(items => items && items.length > 0).startWith(false);
+ activeRedoHistoryListItems$: Observable<ListItem[]> = Observable.combineLatest(
+ this.activeHistoryListItems$,
+ this.store.select(selectActiveFilterHistoryChangeIndex)
+ ).map(([listItems, activeChangeIndex]: [ListItem[], number]): ListItem[] => listItems.slice(activeChangeIndex + 1));
+
+ activeQueryFieldsLabels$: Observable<{[key: string]: string}> = Observable.combineLatest(
+ this.store.select(selectServiceLogsFieldState),
+ this.store.select(selectDefaultAuditLogsFields),
+ this.activeLogsType$
+ ).map(
+ (
+ [serviceLogsFields, auditLogsFields, activeLogsType]: [LogField[], LogField[], LogsType]
+ ) => activeLogsType === 'serviceLogs' ? serviceLogsFields : auditLogsFields
+ ).map(
+ (fields: LogField[]) => fields ? fields.reduce(
+ (fieldLabels: {[key: string]: string}, field: LogField): {[key: string]: string} => ({
+ ...fieldLabels,
+ [field.name]: field.label || field.name
+ }),
+ {}
+ ) : []
+ );
+ activeQueryFieldsLocalCopy$: BehaviorSubject<{[key: string]: string}> = new BehaviorSubject({});
+
+ destroyed$ = new Subject();
+
+ constructor(
+ private store: Store<AppStore>,
+ private router: Router,
+ private logsFilteringUtilsService: LogsFilteringUtilsService,
+ private translateService: TranslateService
+ ) { }
+
+ ngOnInit() {
+ this.componentsLabels$.takeUntil(this.destroyed$).subscribe(componentsLabels => this.componentsLabelsLocalCopy$.next(componentsLabels));
+ this.activeQueryFieldsLabels$.takeUntil(this.destroyed$).subscribe(fieldLabels => this.activeQueryFieldsLocalCopy$.next(fieldLabels));
+
+ this.activeHistoryItemLabels$.takeUntil(this.destroyed$).map((historyLabels) => {
+ return historyLabels.map((historyLabel) => {
+ return {
+ value: historyLabel.url,
+ label: Object.keys(historyLabel.labels).map((url) => historyLabel.labels[url]).join(this.labelSeparator)
+ };
+ });
+ });
+ }
+
+ ngOnDestroy() {
+ this.destroyed$.next(true);
+ }
+
+ onListItemClick(item: ListItem) {
+ this.navigateToFilterUrlParamChangeItem({
+ currentPath: item.value
+ });
+ }
+
+ navigateToFilterUrlParamChangeItem = (item: FilterUrlParamChange) => {
+ if (item) {
+ this.router.navigateByUrl(item.currentPath);
+ }
+ }
+
+ undo(item?: FilterUrlParamChange): void {
+ ( item ?
+ Observable.of(item)
+ : this.activeUndoHistoryItems$.map((changes: FilterUrlParamChange[]) => changes[changes.length - 1])
+ ).first().subscribe(this.navigateToFilterUrlParamChangeItem);
+ }
+
+ redo(item?) {
+ ( item ?
+ Observable.of(item)
+ : this.activeRedoHistoryItems$.map((changes: FilterUrlParamChange[]) => changes[0])
+ ).first().subscribe(this.navigateToFilterUrlParamChangeItem);
+ }
+
+ getValueLabel(paramName, value) {
+ switch (paramName) {
+ case 'level':
+ case 'levels': {
+ return value.toLowerCase().split(',').map(level => level[0].toUpperCase() + level.slice(1)).join(', ');
+ }
+ case 'log_message': {
+ return `"${value}"`;
+ }
+ case 'type': // query
+ case 'components': {
+ const componentLabels = this.componentsLabelsLocalCopy$.getValue();
+ return value.split(/,/g).map((component) => `${componentLabels[component] || component}`).join(', ');
+ }
+ default: {
+ return value;
+ }
+ }
+ }
+
+ private _getMultipleUrlParamDifferenceLabel(difference: UrlParamDifferences): string {
+
+ const fieldLabelTranslateKey: string = /^timeRange/.test(difference.name) ? 'timeRange' : difference.name;
+ const fieldLabel: string = this.translateService.instant(`filterHistory.paramNames.${fieldLabelTranslateKey}`);
+
+ const actionLabelTranslateKey: UrlParamsDifferenceType = difference.to ? UrlParamsDifferenceType.CHANGE : UrlParamsDifferenceType.CLEAR;
+
+ const valueLabel = difference.to ? this.getValueLabel(fieldLabelTranslateKey, difference.to) : '';
+
+ return this.translateService.instant(`filterHistory.${urlParamsActionType[difference.name]}.changeLabel.${actionLabelTranslateKey}`, {
+ fieldLabel,
+ valueLabel
+ });
+ }
+
+ private _getTimeRangeUrlParamDifferenceLabel(
+ differences: UrlParamDifferences[],
+ parameters: {[key: string]: any},
+ dateTimeFormat: string
+ ): string | undefined {
+
+ let timeRangeTypeValue: string = parameters.timeRangeType.toLowerCase();
+
+ if (!timeRangeTypeValue) {
+ return undefined;
+ }
+
+ const timeRangeUnitValue: string = parameters.timeRangeUnit;
+
+ const timeRangeIntervalValue = parameters.timeRangeInterval;
+
+ let valueLabel: string;
+ const typeLabel = this.translateService.instant(`filterHistory.timeRange.type.${timeRangeTypeValue}`);
+ const unitLabel = this.translateService.instant(`filterHistory.timeRange.unit.${timeRangeUnitValue}`);
+ const fieldLabel = this.translateService.instant(`filterHistory.paramNames.timeRange`);
+
+ if (timeRangeTypeValue.toLowerCase() === 'custom') {
+ const timeRangeStart = differences.find(diff => diff.name === 'timeRangeStart');
+ const timeRangeStartValue: string = timeRangeStart && moment(timeRangeStart.to).format(dateTimeFormat);
+ const timeRangeEnd = differences.find(diff => diff.name === 'timeRangeEnd');
+ const timeRangeEndValue: string = timeRangeEnd && moment(timeRangeEnd.to).format(dateTimeFormat);
+ valueLabel = this.translateService.instant(`filterHistory.timeRange.valueLabel.${timeRangeTypeValue}`, {
+ valueStart: timeRangeStartValue,
+ valueEnd: timeRangeEndValue
+ });
+ } else {
+ if (timeRangeTypeValue.toLowerCase() === 'current' && timeRangeUnitValue === 'd') {
+ timeRangeTypeValue = 'today';
+ }
+ if (timeRangeTypeValue.toLowerCase() === 'past' && timeRangeUnitValue === 'd') {
+ timeRangeTypeValue = 'yesterday';
+ }
+ valueLabel = this.translateService.instant(`filterHistory.timeRange.valueLabel.${timeRangeTypeValue}`, {
+ typeLabel,
+ unitLabel: parseInt(timeRangeIntervalValue, 10) > 1 ? unitLabel[1] : unitLabel[0],
+ value: timeRangeIntervalValue || ''
+ });
+ }
+
+ return this.translateService.instant(`filterHistory.timeRange.changeLabel`, { valueLabel, fieldLabel });
+ }
+
+ private _getQueryUrlParamDifferenceLabel(query): string | undefined {
+ const fromQuery: SearchBoxParameter[] | null = query.from ? JSON.parse(query.from) : null;
+ const toQuery: SearchBoxParameter[] | null = query.to ? JSON.parse(query.to) : null;
+ let addedQueries, removedQueries;
+
+ if (fromQuery && !toQuery) {
+ return this.translateService.instant(`filterHistory.query.changeLabel.clear`);
+ } else if (!fromQuery && toQuery) {
+ addedQueries = toQuery;
+ } else {
+ addedQueries = toQuery.filter((queryItem) => fromQuery.findIndex((fromQueryItem) => (
+ fromQueryItem.name === queryItem.name && fromQueryItem.value === queryItem.value
+ )));
+ removedQueries = fromQuery.filter((queryItem) => toQuery.findIndex((toQueryItem) => (
+ toQueryItem.name === queryItem.name && toQueryItem.value === queryItem.value
+ )));
+ }
+
+ const addedLabels = addedQueries ? addedQueries.reduce((labels: string[], addQuery: SearchBoxParameter): string[] => {
+ const queryLabel = this.translateService.instant('filterHistory.query.changeLabel.add', {
+ queryType: this.translateService.instant(`filterHistory.query.type.${addQuery.isExclude ? 'exclude' : 'include'}`),
+ fieldLabel: this.activeQueryFieldsLocalCopy$.getValue()[addQuery.name] || addQuery.name,
+ valueLabel: this.getValueLabel(addQuery.name, addQuery.value)
+ });
+ return queryLabel ? [...labels, queryLabel] : labels;
+ }, []) : [];
+
+ const removedLabels = removedQueries ? removedQueries.reduce((labels: string[], removedQuery: SearchBoxParameter): string[] => {
+ const queryLabel = this.translateService.instant('filterHistory.query.changeLabel.remove', {
+ queryType: this.translateService.instant(`filterHistory.query.type.${removedQuery.isExclude ? 'exclude' : 'include'}`),
+ fieldLabel: this.activeQueryFieldsLocalCopy$.getValue()[removedQuery.name] || removedQuery.name,
+ valueLabel: this.getValueLabel(removedQuery.name, removedQuery.value)
+ });
+ return queryLabel ? [...labels, queryLabel] : labels;
+ }, []) : [];
+
+ return [...addedLabels, ...removedLabels].join(this.labelSeparator);
+ }
+
+ extractParametersFromUrlSegmentGroup(group: UrlSegmentGroup): {[key: string]: string} {
+ return {
+ ...group.segments.reduce((segmentParams, segment) => ({...segmentParams, ...segment.parameters}), {}),
+ ...Object.keys(group.children).reduce(
+ (segmentsParams, key): {[key: string]: string} => {
+ return {
+ ...segmentsParams,
+ ...this.extractParametersFromUrlSegmentGroup(group.children[key])
+ };
+ },
+ {}
+ )
+ };
+ }
+
+ getParametersFromUrl(url: string): {[key: string]: string} {
+ const urlTree: UrlTree = this.router.parseUrl(url);
+ return this.extractParametersFromUrlSegmentGroup(urlTree.root);
+ }
+
+ getParameterDifferencesFromUrls(currentPath: string, previousPath: string, logsType: LogsType): UrlParamDifferences[] {
+ const currentParameters = this.getParametersFromUrl(currentPath);
+ const previousParameters = previousPath ? this.getParametersFromUrl(previousPath) : {};
+ return this.logsFilteringUtilsService.getUrlParamsDifferences(
+ {
+ ...defaultUrlParamsForFiltersByLogsType[logsType],
+ ...previousParameters
+ },
+ {
+ ...defaultUrlParamsForFiltersByLogsType[logsType],
+ ...currentParameters
+ }
+ );
+ }
+
+ getHistoryItemChangeLabels(
+ item: FilterUrlParamChange,
+ logsType: LogsType,
+ isInitial: boolean
+ ): {url: string, labels: {[key: string]: string}} {
+ if (isInitial) {
+ return {
+ url: item.currentPath,
+ labels: {
+ 'initial': this.translateService.instant(`filterHistory.initialState`)
+ }
+ };
+ }
+ const parameterDifferences = this.getParameterDifferencesFromUrls(item.currentPath, item.previousPath, logsType);
+ const differenciesLabels = parameterDifferences.reduce((labels: {[key: string]: string}, change): {[key: string]: string} => {
+ const changeKey = /^timeRange/.test(change.name) ? 'timeRange' : change.name;
+ if (labels[changeKey] !== undefined || urlParamsActionType[changeKey] === undefined) {
+ return labels;
+ }
+ let changeLabel: string;
+ if (/^timeRange/.test(change.name)) { // create time range label
+ changeLabel = this._getTimeRangeUrlParamDifferenceLabel(
+ parameterDifferences,
+ this.getParametersFromUrl(item.currentPath),
+ this.translateService.instant(`filterHistory.timeRange.dateTimeFormat`)
+ );
+ } else if (change.name === 'query') { // create query label
+ changeLabel = this._getQueryUrlParamDifferenceLabel(change);
+ } else {
+ changeLabel = this._getMultipleUrlParamDifferenceLabel(change);
+ }
+ return changeLabel ? {
+ ...labels,
+ [changeKey]: changeLabel
+ } : labels;
+ }, {});
+ return {
+ url: item.currentPath,
+ labels: differenciesLabels
+ };
+ }
+
+ mapHistoryItemsToHistoryItemLabels(
+ [items, activeLogsType, components]: [FilterUrlParamChange[], LogsType, {[key: string]: string}]
+ ): {[key: string]: any}[] {
+ return (items || []).map((item, index): {[key: string]: any} => this.getHistoryItemChangeLabels(item, activeLogsType, index === 0));
+ }
+
+ mapHistoryItemLabelsToListItems(historyLabels, labelSeparator = this.labelSeparator) {
+ return historyLabels.map((historyLabel) => {
+ return {
+ value: historyLabel.url,
+ label: Object.keys(historyLabel.labels).map((url) => historyLabel.labels[url]).join(labelSeparator)
+ };
+ });
+ }
+
+}
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 657d1eaaad..724b0a47c1 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
@@ -24,7 +24,7 @@
<time-range-picker *ngIf="isFilterConditionDisplayed('timeRange')" formControlName="timeRange"
class="filter-input"></time-range-picker>
<timezone-picker class="filter-input"></timezone-picker>
- <button class="btn btn-success search-button" type="button" (click)="updateSearchBoxValue()">
+ <button class="btn btn-success search-button" type="button" (click)="onSearchBtnClick()">
<span class="fa fa-search"></span>
</button>
</div>
diff --git a/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts b/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts
index df863a344f..90fef776f0 100644
--- a/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts
+++ b/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import {Component, OnDestroy, Input, ViewContainerRef, OnInit} from '@angular/core';
+import {Component, OnDestroy, Input, ViewContainerRef, OnInit, Output, EventEmitter} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
@@ -41,6 +41,12 @@ export class FiltersPanelComponent implements OnDestroy, OnInit {
@Input()
filtersForm: FormGroup;
+ @Output()
+ submit = new EventEmitter();
+
+ @Output()
+ clear = new EventEmitter();
+
private subscriptions: Subscription[] = [];
searchBoxItems$: Observable<ListItem[]>;
@@ -133,12 +139,18 @@ export class FiltersPanelComponent implements OnDestroy, OnInit {
});
}
- private onClearBtnClick = (): void => {
+ onClearBtnClick = (): void => {
const defaults = this.logsContainerService.isServiceLogsFileView ? {
- components: this.logsContainerService.filtersForm.controls['components'].value,
- hosts: this.logsContainerService.filtersForm.controls['hosts'].value
+ components: this.filtersForm.controls['components'].value,
+ hosts: this.filtersForm.controls['hosts'].value
} : {};
this.logsContainerService.resetFiltersForms(defaults);
+ this.clear.emit();
+ }
+
+ onSearchBtnClick(): void {
+ this.updateSearchBoxValue();
+ this.submit.emit(this.filtersForm.getRawValue());
}
}
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 73c86043f5..d026419a2a 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
@@ -192,7 +192,7 @@ export class LogIndexFilterComponent implements OnInit, OnDestroy, OnChanges, Co
writeValue(filters: HomogeneousObject<LogIndexFilterComponentConfig[]>): void {
this.configs = filters;
- this.updateValue();
+ this.setCurrentConfig();
}
registerOnChange(callback: any): void {
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 8d5070b6a7..63655142a1 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
@@ -26,7 +26,7 @@ import { AppStore } from '@app/classes/models/store';
import {
isLoginInProgressSelector,
isCheckingAuthStatusInProgressSelector,
- authMessageSelector
+ selectAuthMessage
} from '@app/store/selectors/auth.selectors';
import { LogInAction } from '@app/store/actions/auth.actions';
@@ -45,7 +45,7 @@ export class LoginFormComponent {
password: string;
- authorizationMessage$: Observable<string> = this.store.select(authMessageSelector);
+ authorizationMessage$: Observable<string> = this.store.select(selectAuthMessage);
isLoginInProgress$: Observable<boolean> = this.store.select(isLoginInProgressSelector);
isCheckingAuthStatusInProgress$: Observable<boolean> = this.store.select(isCheckingAuthStatusInProgressSelector);
diff --git a/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html b/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html
index c319ca9174..1add8a3a38 100644
--- a/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html
+++ b/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html
@@ -27,7 +27,7 @@
</div>
<div #container
[ngClass]="{'container-fluid': true, 'logs-container': true, 'fixed-filterbar': isFilterPanelFixedPostioned}">
- <filters-panel class="row" [filtersForm]="filtersForm" #filtersPanel></filters-panel>
+ <filters-panel class="row" [filtersForm]="filtersForm" #filtersPanel (clear)="onFilterPanelClear()"></filters-panel>
<div class="row events-count">
<div *ngIf="captureTimeRangeCache" class="panel panel-default panel-capture-view col-md-2 col-md-offset-5">
<i class="fa fa-play"></i>
@@ -45,23 +45,23 @@
(totalEventsFoundMessageParams.totalCount === 1 ? 'logs.oneEventFound' : 'logs.totalEventFound')) | translate: totalEventsFoundMessageParams}}</header>
<time-histogram (selectArea)="setCustomTimeRange($event[0], $event[1])" [data]="serviceLogsHistogramData"
[colors]="serviceLogsHistogramColors" [allowFractionalYTicks]="false"
- svgId="service-logs-histogram"></time-histogram>
+ svgId="service-logs-histogram" [class.loading]="logsContainerService.isGraphRequestInProgress$ | async"></time-histogram>
</collapsible-panel>
<service-logs-table [totalCount]="totalCount" [logs]="serviceLogs | async" [columns]="serviceLogsColumns | async"
- [filtersForm]="filtersForm"></service-logs-table>
+ [filtersForm]="filtersForm" [class.loading]="logsContainerService.isLogsRequestInProgress$ | async"></service-logs-table>
</ng-container>
<ng-container *ngSwitchCase="'auditLogs'">
<collapsible-panel commonTitle="logs.duration">
<time-line-graph (selectArea)="setCustomTimeRange($event[0], $event[1])" [data]="auditLogsGraphData"
- [allowFractionalYTicks]="false" [skipZeroValuesInTooltip]="false"
- svgId="audit-logs-graph"></time-line-graph>
+ [allowFractionalYTicks]="false" [skipZeroValuesInTooltip]="false" svgId="audit-logs-graph"
+ [class.loading]="logsContainerService.isGraphRequestInProgress$ | async"></time-line-graph>
</collapsible-panel>
<audit-logs-entries [totalCount]="totalCount" [logs]="auditLogs | async" [columns]="auditLogsColumns | async"
- [filtersForm]="filtersForm"></audit-logs-entries>
+ [filtersForm]="filtersForm" [class.loading]="logsContainerService.isLogsRequestInProgress$ | async"></audit-logs-entries>
</ng-container>
</ng-container>
<log-context *ngIf="isServiceLogContextView" [id]="activeLog.id" [hostName]="activeLog.host_name"
- [componentName]="activeLog.component_name"></log-context>
+ [componentName]="activeLog.component_name"></log-context>
</div>
<modal-dialog
title="{{'filter.capture' | translate}}"
diff --git a/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.less b/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.less
index ef61abe624..f0f4765292 100644
--- a/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.less
+++ b/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.less
@@ -18,6 +18,7 @@
@import '../../modules/shared/mixins';
@import '../../modules/shared/variables';
+@import '../../modules/shared/animations';
:host {
display: block;
@@ -96,4 +97,30 @@
}
}
+ /deep/ time-histogram,
+ /deep/ time-line-graph,
+ /deep/ service-logs-table {
+ display: block;
+ &.loading {
+ opacity: .8;
+ position: relative;
+ &:before {
+ .line-progress(1px);
+ content: ' ';
+ display: block;
+ opacity: .8;
+ }
+ }
+ }
+ /deep/ audit-logs-entries.loading > *:not(tabs) {
+ opacity: .8;
+ position: relative;
+ &:before {
+ .line-progress(1px);
+ content: ' ';
+ display: block;
+ opacity: .8;
+ }
+ }
+
}
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 34eb2a4c37..842218c4c2 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
@@ -16,29 +16,28 @@
* limitations under the License.
*/
-import {Component, OnInit, ElementRef, ViewChild, HostListener, Input, OnDestroy, ChangeDetectorRef} from '@angular/core';
-import {FormGroup} from '@angular/forms';
-import {Observable} from 'rxjs/Observable';
+import { Component, OnInit, ElementRef, ViewChild, Input, OnDestroy } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
-import {LogsContainerService} from '@app/services/logs-container.service';
-import {ServiceLogsHistogramDataService} from '@app/services/storage/service-logs-histogram-data.service';
-import {AuditLogsGraphDataService} from '@app/services/storage/audit-logs-graph-data.service';
-import {AppStateService} from '@app/services/storage/app-state.service';
-import {TabsService} from '@app/services/storage/tabs.service';
-import {AuditLog} from '@app/classes/models/audit-log';
-import {ServiceLog} from '@app/classes/models/service-log';
-import {LogTypeTab} from '@app/classes/models/log-type-tab';
-import {BarGraph} from '@app/classes/models/bar-graph';
-import {ActiveServiceLogEntry} from '@app/classes/active-service-log-entry';
-import {ListItem} from '@app/classes/list-item';
-import {HomogeneousObject, LogLevelObject} from '@app/classes/object';
-import {LogsType, LogLevel} from '@app/classes/string';
-import {FiltersPanelComponent} from '@app/components/filters-panel/filters-panel.component';
-import {Subscription} from 'rxjs/Subscription';
-import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.service';
-import {ActivatedRoute, Router} from '@angular/router';
-import {BehaviorSubject} from 'rxjs/BehaviorSubject';
-import {LogsStateService} from '@app/services/storage/logs-state.service';
+import { LogsContainerService } from '@app/services/logs-container.service';
+import { ServiceLogsHistogramDataService } from '@app/services/storage/service-logs-histogram-data.service';
+import { AuditLogsGraphDataService } from '@app/services/storage/audit-logs-graph-data.service';
+import { AppStateService } from '@app/services/storage/app-state.service';
+import { TabsService } from '@app/services/storage/tabs.service';
+import { AuditLog } from '@app/classes/models/audit-log';
+import { ServiceLog } from '@app/classes/models/service-log';
+import { LogTypeTab } from '@app/classes/models/log-type-tab';
+import { BarGraph } from '@app/classes/models/bar-graph';
+import { ActiveServiceLogEntry } from '@app/classes/active-service-log-entry';
+import { ListItem } from '@app/classes/list-item';
+import { HomogeneousObject, LogLevelObject } from '@app/classes/object';
+import { LogsType, LogLevel } from '@app/classes/string';
+import { FiltersPanelComponent } from '@app/components/filters-panel/filters-panel.component';
+import { Subscription } from 'rxjs/Subscription';
+import { LogsFilteringUtilsService } from '@app/services/logs-filtering-utils.service';
+import { ActivatedRoute, Router } from '@angular/router';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Component({
selector: 'logs-container',
@@ -92,13 +91,12 @@ export class LogsContainerComponent implements OnInit, OnDestroy {
constructor(
private appState: AppStateService,
private tabsStorage: TabsService,
- private logsContainerService: LogsContainerService,
+ public logsContainerService: LogsContainerService,
private logsFilteringUtilsService: LogsFilteringUtilsService,
private serviceLogsHistogramStorage: ServiceLogsHistogramDataService,
private auditLogsGraphStorage: AuditLogsGraphDataService,
private router: Router,
- private activatedRoute: ActivatedRoute,
- private logsStateService: LogsStateService
+ private activatedRoute: ActivatedRoute
) {}
ngOnInit() {
@@ -143,7 +141,7 @@ export class LogsContainerComponent implements OnInit, OnDestroy {
// Sync from form to params on form values change
this.subscriptions.push(
this.filtersForm.valueChanges
- .filter(() => !this.logsContainerService.filtersFormSyncInProgress.getValue())
+ .filter(() => !this.logsContainerService.filtersFormSyncInProgress$.getValue())
.subscribe(this.onFiltersFormChange)
);
//// SYNC BETWEEN PARAMS AND FORM END
@@ -262,13 +260,15 @@ export class LogsContainerComponent implements OnInit, OnDestroy {
* @param filters
*/
private syncFiltersToParams(filters): void {
- const params = this.logsFilteringUtilsService.getParamsFromActiveFilter(
- filters, this.logsContainerService.activeLogsType
- );
- this.paramsSyncStart(); // turn on the 'sync in progress' flag
- this.router.navigate([params], { relativeTo: this.activatedRoute })
- .then(this.paramsSyncStop, this.paramsSyncStop) // turn off the 'sync in progress' flag
- .catch(this.paramsSyncStop); // turn off the 'sync in progress' flag
+ this.activatedRoute.params.first().subscribe((routeParams) => {
+ const params = this.logsFilteringUtilsService.getParamsFromActiveFilter(
+ filters, this.logsContainerService.activeLogsType
+ );
+ this.paramsSyncStart(); // turn on the 'sync in progress' flag
+ this.router.navigate([params], { relativeTo: this.activatedRoute })
+ .then(this.paramsSyncStop, this.paramsSyncStop) // turn off the 'sync in progress' flag
+ .catch(this.paramsSyncStop); // turn off the 'sync in progress' flag
+ });
}
/**
@@ -277,12 +277,9 @@ export class LogsContainerComponent implements OnInit, OnDestroy {
* @param values {[key: string]: any} The new values for the filter form
*/
private resetFiltersForm(values: {[key: string]: any}): void {
- if (Object.keys(values).length) {
- this.logsContainerService.resetFiltersForms({
- ...this.logsFilteringUtilsService.defaultFilterSelections,
- ...values
- });
- }
+ this.logsContainerService.resetFiltersForms({
+ ...values
+ });
}
/**
@@ -305,25 +302,49 @@ export class LogsContainerComponent implements OnInit, OnDestroy {
private onParamsChange = (params: {[key: string]: any}) => {
const {activeTab, ...filtersParams} = params;
- this.tabsStorage.findInCollection((tab: LogTypeTab) => tab.id === params.activeTab)
+
+ if (activeTab !== this.activeTabId$.getValue()) { // tab change
+ this.tabsStorage.findInCollection((tab: LogTypeTab) => tab.id === params.activeTab)
.first()
.subscribe((tab) => {
if (tab) {
- const filtersFromParams: {[key: string]: any} = this.logsFilteringUtilsService.getFilterFromParams(
- filtersParams,
- tab.appState.activeLogsType
- );
- // 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
- if (this.activeTabId$.getValue() === activeTab) {
- this.resetFiltersForm(filtersFromParams);
- }
- this.syncFilterToTabStore(filtersFromParams, activeTab);
this.activeTabId$.next(activeTab);
}
});
+ } else { // filter change
+ const filtersFromParams: {[key: string]: any} = this.logsFilteringUtilsService.getFilterFromParams(
+ filtersParams,
+ this.logsContainerService.activeLogsType
+ );
+ const currentFormParams = this.logsFilteringUtilsService.getParamsFromActiveFilter(
+ this.filtersForm.value, this.logsContainerService.activeLogsType
+ );
+ const filtersFormControlNames = Object.keys(this.filtersForm.controls);
+ const hasChange = filtersFormControlNames.reduce(
+ (changed, key) => {
+ if (currentFormParams[key] === undefined && filtersParams[key] === undefined) {
+ return changed;
+ }
+ return (
+ changed
+ || (currentFormParams[key] === undefined && filtersParams[key] !== undefined)
+ || (currentFormParams[key] !== undefined && filtersParams[key] === undefined)
+ || currentFormParams[key].toString() !== filtersParams[key].toString()
+ );
+ },
+ false
+ );
+ if (hasChange) {
+ // 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
+ if (this.activeTabId$.getValue() === activeTab) {
+ this.resetFiltersForm(filtersFromParams);
+ }
+ this.syncFilterToTabStore(filtersFromParams, activeTab);
+ }
+ }
}
//
@@ -381,4 +402,8 @@ export class LogsContainerComponent implements OnInit, OnDestroy {
}
}
+ onFilterPanelClear() {
+ this.syncFiltersToParams(this.filtersForm.getRawValue());
+ }
+
}
diff --git a/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.spec.ts b/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.spec.ts
index 4e77db597a..26912733bb 100644
--- a/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.spec.ts
+++ b/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.spec.ts
@@ -40,6 +40,7 @@ import {LogsContainerService} from '@app/services/logs-container.service';
import {AuthService} from '@app/services/auth.service';
import {MenuButtonComponent} from './menu-button.component';
+import { UtilsService } from '@app/services/utils.service';
describe('MenuButtonComponent', () => {
let component: MenuButtonComponent;
@@ -93,7 +94,8 @@ describe('MenuButtonComponent', () => {
useValue: httpClient
},
LogsContainerService,
- AuthService
+ AuthService,
+ UtilsService
],
schemas: [NO_ERRORS_SCHEMA]
})
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 788494cf98..faf2165c7a 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
@@ -19,6 +19,7 @@
import {Component, Input, Output, ViewChild, ElementRef, EventEmitter} from '@angular/core';
import {ListItem} from '@app/classes/list-item';
import {DropdownListComponent} from '@modules/shared/components/dropdown-list/dropdown-list.component';
+import {UtilsService} from '@app/services/utils.service';
@Component({
selector: 'menu-button',
@@ -46,13 +47,13 @@ export class MenuButtonComponent {
subItems?: ListItem[];
@Input()
- isMultipleChoice: boolean = false;
+ isMultipleChoice = false;
@Input()
- hideCaret: boolean = false;
+ hideCaret = false;
@Input()
- isRightAlign: boolean = false;
+ isRightAlign = false;
@Input()
additionalLabelComponentSetter?: string;
@@ -61,10 +62,10 @@ export class MenuButtonComponent {
badge: string;
@Input()
- caretClass: string = 'fa-caret-down';
+ caretClass = 'fa-caret-down';
@Input()
- useDropDownLocalFilter: boolean = false;
+ useDropDownLocalFilter = false;
/**
* The minimum time to handle a mousedown as a longclick. Default is 500 ms (0.5sec)
@@ -72,7 +73,7 @@ export class MenuButtonComponent {
* @type {number}
*/
@Input()
- minLongClickDelay: number = 500;
+ minLongClickDelay = 500;
/**
* The maximum milliseconds to wait for longclick ends. The default is 0 which means no upper limit.
@@ -80,13 +81,13 @@ export class MenuButtonComponent {
* @type {number}
*/
@Input()
- maxLongClickDelay: number = 0;
+ maxLongClickDelay = 0;
@Input()
- isDisabled: boolean = false;
+ isDisabled = false;
@Input()
- listClass: string = '';
+ listClass = '';
@Output()
buttonClick: EventEmitter<void> = new EventEmitter();
@@ -104,7 +105,7 @@ export class MenuButtonComponent {
* Indicates if the dropdown list is open or not. So that we use internal state to display or hide the dropdown.
* @type {boolean}
*/
- private dropdownIsOpen: boolean = false;
+ private dropdownIsOpen = false;
get hasSubItems(): boolean {
return Boolean(this.subItems && this.subItems.length);
@@ -114,6 +115,26 @@ export class MenuButtonComponent {
return this.hasSubItems && !this.hideCaret;
}
+ set selection(items: ListItem[] | null) {
+ const selectedItems = items ? (Array.isArray(items) ? items : [items]) : [];
+ this.subItems.forEach((subItem: ListItem) => {
+ const indexInSelection = this.findItemIndexInList(subItem, selectedItems);
+ subItem.isChecked = indexInSelection > -1;
+ });
+ this.refreshDropdownList();
+ }
+ get selection(): ListItem[] {
+ return this.subItems && this.subItems.filter((option: ListItem): boolean => option.isChecked);
+ }
+
+ constructor(private utils: UtilsService) {}
+
+ findItemIndexInList(item: ListItem, itemList: ListItem[] = this.subItems): number {
+ return itemList.findIndex((subItem) => (
+ item === subItem || this.utils.isEqual(item.value, subItem.value)
+ ));
+ }
+
/**
* Handling the click event on the component element.
* Two goal:
@@ -214,7 +235,7 @@ export class MenuButtonComponent {
/**
* The main goal if this function is tho handle the item change event on the child dropdown list.
- * Should update the value and close the dropdown if it is not multiple choice type.
+ * Should update the value and close the dropdown.
* @param {ListItem} item The selected item(s) from the dropdown list.
*/
onDropdownItemChange(item: ListItem | ListItem[]) {
@@ -224,11 +245,22 @@ export class MenuButtonComponent {
}
}
- updateSelection(item: ListItem | ListItem[]) {
- this.selectItem.emit(item);
+ refreshDropdownList() {
if (this.dropdownList) {
this.dropdownList.doItemsCheck();
}
}
+ updateSelection(item: ListItem | ListItem[]) {
+ const changes = Array.isArray(item) ? item : [item];
+ changes.forEach((change: ListItem): void => {
+ const subItemIndex = this.findItemIndexInList(change);
+ if (subItemIndex > -1) {
+ this.subItems[subItemIndex].isChecked = change.isChecked;
+ }
+ });
+ this.selectItem.emit(item);
+ this.refreshDropdownList();
+ }
+
}
diff --git a/ambari-logsearch-web/src/app/components/pagination-controls/pagination-controls.component.ts b/ambari-logsearch-web/src/app/components/pagination-controls/pagination-controls.component.ts
index 5f85da7da5..b476bf32a3 100644
--- a/ambari-logsearch-web/src/app/components/pagination-controls/pagination-controls.component.ts
+++ b/ambari-logsearch-web/src/app/components/pagination-controls/pagination-controls.component.ts
@@ -54,9 +54,6 @@ export class PaginationControlsComponent implements ControlValueAccessor {
if (this.isValidValue(newValue)) { // this is the last validation check
this.currentPage = newValue;
this.currentPageChange.emit(newValue);
- if (this.onChange) {
- this.onChange(newValue);
- }
} else {
throw new Error(`Invalid value ${newValue}. The currentPage should be between 0 and ${this.pagesCount}.`);
}
@@ -75,14 +72,14 @@ export class PaginationControlsComponent implements ControlValueAccessor {
* The goal is to set the value to the first page... obviously to zero. It is just to have a centralized api for that.
*/
setFirstPage(): void {
- this.value = 0;
+ this._setValueByUserInput(0);
}
/**
* The goal is to set the value to the last page which is the pagesCount property anyway.
*/
setLastPage(): void {
- this.value = this.pagesCount - 1;
+ this._setValueByUserInput(this.pagesCount - 1);
}
/**
@@ -91,7 +88,7 @@ export class PaginationControlsComponent implements ControlValueAccessor {
*/
setPreviousPage(): number {
if (this.hasPreviousPage()) {
- this.value -= 1;
+ this._setValueByUserInput(this.value - 1);
}
return this.value;
}
@@ -101,8 +98,8 @@ export class PaginationControlsComponent implements ControlValueAccessor {
* @returns {number} The new value of the currentPage
*/
setNextPage(): number {
- if (this.hasNextPage()){
- this.value += 1;
+ if (this.hasNextPage()) {
+ this._setValueByUserInput(this.value + 1);
}
return this.value;
}
@@ -123,6 +120,17 @@ export class PaginationControlsComponent implements ControlValueAccessor {
return this.pagesCount > 0 && this.value > 0;
}
+ private _setValueByUserInput(value) {
+ this.value = value;
+ this._onChange(this.value);
+ }
+
+ private _onChange(value) {
+ if (this.onChange) {
+ this.onChange(value);
+ }
+ }
+
writeValue(value: number) {
this.value = value;
}
diff --git a/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts b/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts
index 62835bbc74..da33f607dd 100644
--- a/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts
+++ b/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts
@@ -97,7 +97,7 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
* @type {boolean}
*/
@Input()
- updateValueImmediately: boolean = true;
+ updateValueImmediately = true;
@ViewChild('parameterInput')
parameterInputRef: ElementRef;
@@ -121,26 +121,22 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
*/
parameters: SearchBoxParameterProcessed[] = [];
- private subscriptions: Subscription[] = [];
+ private onChange;
+
+ private destroyed$ = new Subject();
constructor(private utils: UtilsService) {}
ngOnInit(): void {
this.parameterInput = this.parameterInputRef.nativeElement;
this.valueInput = this.valueInputRef.nativeElement;
- this.subscriptions.push(
- this.parameterNameChangeSubject.subscribe(this.onParameterNameChange)
- );
- this.subscriptions.push(
- this.parameterAddSubject.subscribe(this.onParameterAdd)
- );
- this.subscriptions.push(
- this.updateValueSubject.subscribe(this.updateValue)
- );
+ this.parameterNameChangeSubject.takeUntil(this.destroyed$).subscribe(this.onParameterNameChange);
+ this.parameterAddSubject.takeUntil(this.destroyed$).subscribe(this.onParameterAdd);
+ this.updateValueSubject.takeUntil(this.destroyed$).subscribe(this.updateValue);
}
ngOnDestroy(): void {
- this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
+ this.destroyed$.next(true);
}
/**
@@ -152,8 +148,6 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
this.itemsOptions[this.activeItem.value] : [];
}
- private onChange: (fn: any) => void;
-
@HostListener('click')
private onRootClick(): void {
if (!this.isActive) {
@@ -310,7 +304,7 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
updateValue = (): void => {
this.currentValue = '';
if (this.onChange) {
- this.onChange(this.parameters.slice());
+ this.onChange([...this.parameters]);
}
}
@@ -331,8 +325,9 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
}
writeValue(parameters: SearchBoxParameterProcessed[] = []): void {
- this.parameters = parameters.slice();
- this.updateValueSubject.next();
+ this.currentValue = '';
+ this.parameters = [...parameters];
+ // this.updateValueSubject.next();
}
registerOnChange(callback: any): void {
diff --git a/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts b/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts
index 757b4a0199..bfb6068006 100644
--- a/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts
+++ b/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts
@@ -52,6 +52,12 @@ export enum ListLayout {
})
export class ServiceLogsTableComponent extends LogsTableComponent implements AfterViewChecked, OnInit, OnDestroy {
+ /**
+ * Extra css class which can be applied to the container element
+ */
+ @Input()
+ cssClass: string;
+
/**
* The element reference is used to check if the table is broken or not.
*/
diff --git a/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.ts b/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.ts
index e4e146fd57..3d031b2d14 100644
--- a/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.ts
+++ b/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.ts
@@ -61,9 +61,6 @@ export class TimeRangePickerComponent implements ControlValueAccessor {
set selection(newValue: TimeUnitListItem) {
this.timeRange = newValue;
- if (this.onChange) {
- this.onChange(newValue);
- }
this.setEndTime(this.logsFilteringUtilsService.getEndTimeMomentFromTimeUnitListItem(newValue, this.logsContainer.timeZone));
this.setStartTime(this.logsFilteringUtilsService.getStartTimeMomentFromTimeUnitListItem(
newValue, this.endTime, this.logsContainer.timeZone
@@ -80,6 +77,7 @@ export class TimeRangePickerComponent implements ControlValueAccessor {
setTimeRange(value: any, label: string): void {
this.selection = {label, value};
+ this._onChange(this.selection);
}
setCustomTimeRange(): void {
@@ -91,6 +89,13 @@ export class TimeRangePickerComponent implements ControlValueAccessor {
end: this.endTime
}
};
+ this._onChange(this.selection);
+ }
+
+ private _onChange(value: TimeUnitListItem): void {
+ if (this.onChange) {
+ this.onChange(value);
+ }
}
writeValue(selection: TimeUnitListItem): 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 2f93cb3350..c717b324e3 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
@@ -25,7 +25,7 @@ import { DataAvailabilityStatesStore } from '@app/modules/app-load/stores/data-a
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 { selectAuthStatus } from '@app/store/selectors/auth.selectors';
import { AuthorizationStatuses } from '@app/store/reducers/auth.reducers';
export function set_translation_service(appLoadService: AppLoadService) {
@@ -34,7 +34,7 @@ export function set_translation_service(appLoadService: AppLoadService) {
export function check_auth_status(store: Store<AppStore>) {
return () => new Promise((resolve) => {
- store.select(authStatusSelector)
+ store.select(selectAuthStatus)
.filter(
(status: AuthorizationStatuses): boolean => (status !== null && AuthorizationStatuses.CHEKCING_AUTHORIZATION_STATUS !== status)
).first().subscribe(resolve);
diff --git a/ambari-logsearch-web/src/app/modules/app-load/models/data-availability-state.model.ts b/ambari-logsearch-web/src/app/modules/app-load/models/data-availability-state.model.ts
index d819dec57b..9a512df272 100644
--- a/ambari-logsearch-web/src/app/modules/app-load/models/data-availability-state.model.ts
+++ b/ambari-logsearch-web/src/app/modules/app-load/models/data-availability-state.model.ts
@@ -22,6 +22,7 @@ export interface DataAvaibilityStatesModel {
hostsDataState: DataAvailabilityValues;
componentsDataState: DataAvailabilityValues;
logFieldsDataState: DataAvailabilityValues;
+ [key: string]: DataAvailabilityValues;
}
export const initialDataAvaibilityStates: DataAvaibilityStatesModel = {
diff --git a/ambari-logsearch-web/src/app/modules/shared/animations.less b/ambari-logsearch-web/src/app/modules/shared/animations.less
index 5b8a04ce93..5f33c46fd9 100644
--- a/ambari-logsearch-web/src/app/modules/shared/animations.less
+++ b/ambari-logsearch-web/src/app/modules/shared/animations.less
@@ -25,9 +25,40 @@
}
}
+@keyframes rotatecircle {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes running-progress {
+ 0% { margin-left: 0px; margin-right: 100%; }
+ 50% { margin-left: 25%; margin-right: 0%; }
+ 100% { margin-left: 100%; margin-right: 0; }
+}
+
.square-spinner(@size: 40px, @background: #3FAE2A, @speed: 1.2s) {
width: @size;
height: @size;
background: @background;
animation: rotateplane @speed infinite ease-in-out;
}
+
+.circle-spinner(@size: 40px, @thickness: 3px, @color: #3FAE2A, @speed: 1.2s) {
+ animation: rotatecircle @speed infinite ease-in-out;
+ border: @thickness solid @color;
+ border-right-color: transparent;
+ border-radius: 50%;
+ display: inline-block;
+ height: @size;
+ width: @size;
+}
+
+.line-progress(@height: 3px, @color: #3FAE2A, @speed: 1.2s) {
+ height: @height;
+ background-color: @color;
+ animation: running-progress @speed cubic-bezier(0.4, 0, 0.2, 1) infinite;
+}
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 ab519d069d..74341aeff4 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
@@ -91,7 +91,19 @@ export class DropdownButtonComponent {
protected utils: UtilsService
) {}
- updateSelection(updates: ListItem | ListItem[]): void {
+ clearSelection(silent: boolean = false) {
+ let hasChange = false;
+ this.options.forEach((item: ListItem) => {
+ hasChange = hasChange || item.isChecked;
+ item.isChecked = false;
+ });
+ if (!silent && hasChange) {
+ this.selectItem.emit(this.isMultipleChoice ? [] : undefined);
+ }
+ }
+
+ updateSelection(updates: ListItem | ListItem[], callOnChange: boolean = true): boolean {
+ let hasChange = false;
if (updates && (!Array.isArray(updates) || updates.length)) {
const items: ListItem[] = Array.isArray(updates) ? updates : [updates];
if (this.isMultipleChoice) {
@@ -99,6 +111,7 @@ export class DropdownButtonComponent {
if (this.options && this.options.length) {
const itemToUpdate: ListItem = this.options.find((option: ListItem) => this.utils.isEqual(option.value, item.value));
if (itemToUpdate) {
+ hasChange = hasChange || itemToUpdate.isChecked !== item.isChecked;
itemToUpdate.isChecked = item.isChecked;
}
}
@@ -106,7 +119,9 @@ export class DropdownButtonComponent {
} else {
const selectedItem: ListItem = Array.isArray(updates) ? updates[0] : updates;
this.options.forEach((item: ListItem) => {
+ const checkedStateBefore = item.isChecked;
item.isChecked = this.utils.isEqual(item.value, selectedItem.value);
+ hasChange = hasChange || checkedStateBefore !== item.isChecked;
});
}
} else {
@@ -114,8 +129,11 @@ export class DropdownButtonComponent {
}
const checkedItems = this.options.filter((option: ListItem): boolean => option.isChecked);
this.selection = checkedItems;
- const selectedValues = checkedItems.map((option: ListItem): any => option.value);
- this.selectItem.emit(this.isMultipleChoice ? selectedValues : selectedValues.shift());
+ if (hasChange) {
+ const selectedValues = checkedItems.map((option: ListItem): any => option.value);
+ this.selectItem.emit(this.isMultipleChoice ? selectedValues : selectedValues.shift());
+ }
+ return hasChange;
}
}
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 fac626fd4e..89a794be47 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,7 @@
-->
<ng-template #listItem let-item let-isMultipleChoice="isMultipleChoice">
<li [class.divider]="item.isDivider" [class.filtered]="isFiltered(item)"
- [attr.role]="item.isDivider ? 'separator' : null" [class]="(item.cssClass || '')">
+ [attr.role]="item.isDivider ? 'separator' : null" [ngClass]="(item.cssClass || '')">
<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"
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 5ce1061ae4..7461da3b8f 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
@@ -20,11 +20,14 @@
:host {
max-height: @dropdown-max-height;
- overflow-y: auto;
+ max-width: @dropdown-max-width;
+ overflow-y: hidden;
+ z-index: 1100;
> li {
.dropdown-item-default;
transition: opacity 300ms ease-in, height 100ms 400ms ease-in;
+ text-overflow: ellipsis;
&.filtered {
overflow: hidden;
opacity: 0;
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 18096370c2..9967c80468 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
@@ -23,6 +23,7 @@ import {
import {Subscription} from 'rxjs/Subscription';
import {ListItem} from '@app/classes/list-item';
import {ComponentGeneratorService} from '@app/services/component-generator.service';
+import { Subject } from 'rxjs/Subject';
@Component({
selector: 'ul[data-component="dropdown-list"]',
@@ -77,7 +78,7 @@ export class DropdownListComponent implements OnInit, OnChanges, AfterViewChecke
private filterRegExp: RegExp;
- private subscriptions: Subscription[] = [];
+ private destroyed$ = new Subject();
instanceId: string;
@@ -95,13 +96,11 @@ export class DropdownListComponent implements OnInit, OnChanges, AfterViewChecke
if (this.items.some((item: ListItem) => item.isChecked)) {
this.selectedItemChange.emit(this.items);
}
- this.subscriptions.push(
- this.selectedItemChange.subscribe(this.separateSelections)
- );
+ this.selectedItemChange.takeUntil(this.destroyed$).subscribe(this.separateSelections)
}
ngOnDestroy() {
- this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
+ this.destroyed$.next(true);
}
ngOnChanges(changes: SimpleChanges): void {
@@ -186,9 +185,6 @@ export class DropdownListComponent implements OnInit, OnChanges, AfterViewChecke
unSelectAll() {
this.items.forEach((item: ListItem) => {
item.isChecked = false;
- if (item.onSelect) {
- item.onSelect(...this.actionArguments);
- }
});
this.selectedItemChange.emit(this.items);
}
diff --git a/ambari-logsearch-web/src/app/modules/shared/components/filter-dropdown/filter-dropdown.component.ts b/ambari-logsearch-web/src/app/modules/shared/components/filter-dropdown/filter-dropdown.component.ts
index 6140e7d117..669fcc9d3d 100644
--- a/ambari-logsearch-web/src/app/modules/shared/components/filter-dropdown/filter-dropdown.component.ts
+++ b/ambari-logsearch-web/src/app/modules/shared/components/filter-dropdown/filter-dropdown.component.ts
@@ -34,7 +34,7 @@ import {ListItem} from '@app/classes/list-item';
})
export class FilterDropdownComponent extends DropdownButtonComponent implements ControlValueAccessor {
- private onChange: (fn: any) => void;
+ private onChange;
get selection(): ListItem[] {
return this.selectedItems;
@@ -48,9 +48,20 @@ export class FilterDropdownComponent extends DropdownButtonComponent implements
option.isChecked = Boolean(selectionItem);
});
}
+ }
+
+ private _onChange(value) {
if (this.onChange) {
- this.onChange(items);
+ this.onChange(value);
+ }
+ }
+
+ updateSelection(updates: ListItem | ListItem[], callOnChange: boolean = true): boolean {
+ const hasChange = super.updateSelection(updates);
+ if (hasChange && callOnChange) {
+ this._onChange(this.selection);
}
+ return hasChange;
}
writeValue(items: ListItem[]) {
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 f13872c0a0..787937787c 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
@@ -32,6 +32,7 @@ import { AuthEffects } from '@app/store/effects/auth.effects';
import { NotificationEffects } from '@app/store/effects/notification.effects';
import { ModalDialogComponent } from './modal-dialog.component';
+import { RouterTestingModule } from '@angular/router/testing';
describe('ModalDialogComponent', () => {
let component: ModalDialogComponent;
@@ -41,6 +42,7 @@ describe('ModalDialogComponent', () => {
TestBed.configureTestingModule(getCommonTestingBedConfiguration({
imports: [
...TranslationModules,
+ RouterTestingModule,
StoreModule.provideStore({
appState,
auth: auth.reducer
diff --git a/ambari-logsearch-web/src/app/modules/shared/shared.module.ts b/ambari-logsearch-web/src/app/modules/shared/shared.module.ts
index 8269852155..c520c38d43 100644
--- a/ambari-logsearch-web/src/app/modules/shared/shared.module.ts
+++ b/ambari-logsearch-web/src/app/modules/shared/shared.module.ts
@@ -16,27 +16,27 @@
* limitations under the License.
*/
-import {NgModule} from '@angular/core';
-import {CommonModule} from '@angular/common';
-import {BrowserModule, Title} from '@angular/platform-browser';
-import {FormsModule} from '@angular/forms';
-import {Http} from '@angular/http';
-import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
-import {NotificationsService as Angular2NotificationsService} from 'angular2-notifications/src/notifications.service';
-import {TranslateModule, TranslateLoader} from '@ngx-translate/core';
-import {NgObjectPipesModule} from 'angular-pipes';
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { BrowserModule, Title } from '@angular/platform-browser';
+import { FormsModule } from '@angular/forms';
+import { Http } from '@angular/http';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { NotificationsService as Angular2NotificationsService } from 'angular2-notifications/src/notifications.service';
+import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
+import { NgObjectPipesModule } from 'angular-pipes';
-import {TranslateService as AppTranslateService} from '@app/services/translate.service';
+import { TranslateService as AppTranslateService } from '@app/services/translate.service';
-import {NotificationService} from './services/notification.service';
+import { NotificationService } from './services/notification.service';
-import {CanDeactivateGuardService} from './services/can-deactivate-guard.service';
-import {DisableControlDirective} from './directives/disable-control.directive';
+import { CanDeactivateGuardService } from './services/can-deactivate-guard.service';
+import { DisableControlDirective } from './directives/disable-control.directive';
-import {DropdownButtonComponent} from './components/dropdown-button/dropdown-button.component';
-import {DropdownListComponent} from './components/dropdown-list/dropdown-list.component';
-import {FilterDropdownComponent} from './components/filter-dropdown/filter-dropdown.component';
-import {ModalComponent} from './components/modal/modal.component';
+import { DropdownButtonComponent } from './components/dropdown-button/dropdown-button.component';
+import { DropdownListComponent } from './components/dropdown-list/dropdown-list.component';
+import { FilterDropdownComponent } from './components/filter-dropdown/filter-dropdown.component';
+import { ModalComponent } from './components/modal/modal.component';
import { DataLoadingIndicatorComponent } from '@app/modules/shared/components/data-loading-indicator/data-loading-indicator.component';
import { ModalDialogComponent } from './components/modal-dialog/modal-dialog.component';
import { LoadingIndicatorComponent } from './components/loading-indicator/loading-indicator.component';
diff --git a/ambari-logsearch-web/src/app/modules/shared/variables.less b/ambari-logsearch-web/src/app/modules/shared/variables.less
index b9175274d1..437c5561ee 100644
--- a/ambari-logsearch-web/src/app/modules/shared/variables.less
+++ b/ambari-logsearch-web/src/app/modules/shared/variables.less
@@ -46,6 +46,7 @@
@checkbox-top: 4px;
@dropdown-min-width: 160px;
@dropdown-max-height: 60vh; // TODO get rid of magic number, base on actual design
+@dropdown-max-width: 50vw; // TODO get rid of magic number, base on actual design
@dropdown-border-radius: 2px;
@input-height: 34px;
@input-padding: 10px;
diff --git a/ambari-logsearch-web/src/app/services/filter-history.guard.ts b/ambari-logsearch-web/src/app/services/filter-history.guard.ts
new file mode 100644
index 0000000000..95997cd003
--- /dev/null
+++ b/ambari-logsearch-web/src/app/services/filter-history.guard.ts
@@ -0,0 +1,128 @@
+/**
+ * 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, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
+import { Observable } from 'rxjs/Observable';
+
+import { Store } from '@ngrx/store';
+import { AppStore } from '@app/classes/models/store';
+
+import * as dataAvailabilitySelectors from '@app/store/selectors/data-availability.selectors';
+
+import * as fromFilterHistoryReducers from '@app/store/reducers/filter-history.reducers';
+import * as filterHistorySelectors from '@app/store/selectors/filter-history.selectors';
+import { AddFilterHistoryAction, SetCurrentFilterHistoryByIndexAction } from '@app/store/actions/filter-history.actions';
+
+@Injectable()
+export class FilterHistoryIndexGuard implements CanActivate {
+
+ private currentUrl: string;
+
+ currentFilterHistory$: Observable<fromFilterHistoryReducers.FilterHistoryState> = this.store.select(
+ filterHistorySelectors.selectFilterHistoryState
+ );
+
+ filterHistoryIndexUrlParamName = '_fhi';
+ logsTypeUrlParamName = 'activeTab';
+
+ constructor (
+ private router: Router,
+ private store: Store<AppStore>
+ ) {}
+
+ removeFilterHistoryIndexFromUrl(url) {
+ const regexp = new RegExp(`;${this.filterHistoryIndexUrlParamName}=\\d{1,}`, 'g');
+ return url.replace(regexp, '');
+ }
+
+ addFilterHistoryIndexToUrl(url, index) {
+ return `${url};${this.filterHistoryIndexUrlParamName}=${index}`;
+ }
+
+ canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
+ let filterHistoryIndex = next.params[this.filterHistoryIndexUrlParamName];
+ if (typeof filterHistoryIndex === 'string') {
+ filterHistoryIndex = parseInt(filterHistoryIndex, 0);
+ }
+ const logsType = next.params[this.logsTypeUrlParamName];
+ return Observable.combineLatest(
+ this.currentFilterHistory$,
+ this.store.select(dataAvailabilitySelectors.isBaseDataAvailable)
+ ).first().map(([historyState, isBaseDataAvailable]: [fromFilterHistoryReducers.FilterHistoryState, boolean]): boolean => {
+ const history = historyState[logsType];
+ let canActivate = true;
+ const requestedUrlWithoutFilterHistoryIndex = this.removeFilterHistoryIndexFromUrl(state.url);
+ const lastChangeUrlWithoutFilterHistoryIndex = history && history.changes.length && (
+ this.removeFilterHistoryIndexFromUrl(history.changes[ history.changes.length - 1].currentPath)
+ );
+ const isUrlChanged = lastChangeUrlWithoutFilterHistoryIndex !== requestedUrlWithoutFilterHistoryIndex;
+ // check if the filter history index is correct
+ if (isUrlChanged && filterHistoryIndex !== undefined) {
+ const currentUrlAtIndex = (
+ history
+ && history.changes[filterHistoryIndex]
+ && this.removeFilterHistoryIndexFromUrl(history.changes[filterHistoryIndex].currentPath)
+ );
+ if (requestedUrlWithoutFilterHistoryIndex !== currentUrlAtIndex) {
+ filterHistoryIndex = undefined;
+ // correct the filter history index if we already have history and the URL exists in the list
+ if (history && history.changes.length) {
+ const existingIndex = history.changes.findIndex(
+ (change) => this.removeFilterHistoryIndexFromUrl(change.currentPath) === requestedUrlWithoutFilterHistoryIndex
+ );
+ if (existingIndex > -1) {
+ filterHistoryIndex = existingIndex;
+ }
+ }
+ }
+ }
+ if (!history || filterHistoryIndex === undefined) { // new History URL
+ const nextFilterHistoryIndex: number = history ? history.currentChangeIndex + 1 : 0;
+ const indexedUrl = this.addFilterHistoryIndexToUrl(requestedUrlWithoutFilterHistoryIndex, nextFilterHistoryIndex);
+ if (isUrlChanged) {
+ this.store.dispatch( new AddFilterHistoryAction({
+ logType: logsType,
+ change: {
+ previousPath: this.currentUrl,
+ currentPath: indexedUrl,
+ time: new Date()
+ }
+ }));
+ this.currentUrl = indexedUrl;
+ }
+ this.router.navigateByUrl(indexedUrl);
+ canActivate = false;
+ } else if (history.currentChangeIndex !== filterHistoryIndex) {
+ // set the current index in the store
+ this.store.dispatch(
+ new SetCurrentFilterHistoryByIndexAction({
+ logType: logsType,
+ index: filterHistoryIndex
+ })
+ );
+ }
+ // if we found the requested URL in the history but the recorded index is not the same as it is in the URL
+ // we add it and navigate to the indexed URL
+ if (filterHistoryIndex !== undefined && next.params[this.filterHistoryIndexUrlParamName] !== filterHistoryIndex.toString()) {
+ this.router.navigateByUrl( this.addFilterHistoryIndexToUrl(state.url, filterHistoryIndex) );
+ canActivate = false;
+ }
+ return canActivate;
+ });
+ }
+}
diff --git a/ambari-logsearch-web/src/app/services/history-manager.service.ts b/ambari-logsearch-web/src/app/services/history-manager.service.ts
index 2a3f533484..b484cf14e1 100644
--- a/ambari-logsearch-web/src/app/services/history-manager.service.ts
+++ b/ambari-logsearch-web/src/app/services/history-manager.service.ts
@@ -43,22 +43,14 @@ export class HistoryManagerService {
* Maximal number of displayed history items
* @type {number}
*/
- private readonly maxHistoryItemsCount: number = 25;
-
- /**
- * Indicates whether there is no changes being applied to filters that are triggered by undo or redo action.
- * Since user can undo or redo several filters changes at once, and they are applied to form controls step-by-step,
- * this flag is needed to avoid recording intermediate items to history.
- * @type {boolean}
- */
- private hasNoPendingUndoOrRedo: boolean = true;
+ private readonly maxHistoryItemsCount = 25;
/**
* Id of currently active history item.
* Generally speaking, it isn't id of the latest one because it can be shifted by undo or redo action.
* @type {number}
*/
- private currentHistoryItemId: number = -1;
+ private currentHistoryItemId = -1;
/**
* Contains i18n labels for filtering form control names
@@ -125,7 +117,7 @@ export class HistoryManagerService {
});
this.logsContainerService.filtersForm.valueChanges
- .filter(() => !this.logsContainerService.filtersFormSyncInProgress.getValue())
+ .filter(() => !this.logsContainerService.filtersFormSyncInProgress$.getValue())
.distinctUntilChanged(this.isHistoryUnchanged)
.subscribe(this.onFormValueChanges);
}
@@ -146,41 +138,33 @@ export class HistoryManagerService {
}
onFormValueChanges = (value): void => {
- if (this.hasNoPendingUndoOrRedo) {
- const defaultState = this.logsContainerService.getFiltersData(this.logsContainerService.activeLogsType);
- const currentHistory = this.activeHistory;
- const previousValue = this.activeHistory.length ? this.activeHistory[0].value.currentValue : defaultState;
- const isUndoOrRedo = value.isUndoOrRedo;
- const previousChangeId = this.currentHistoryItemId;
- if (isUndoOrRedo) {
- this.hasNoPendingUndoOrRedo = false;
- this.logsContainerService.filtersForm.patchValue({
- isUndoOrRedo: false
- });
- this.hasNoPendingUndoOrRedo = true;
- } else {
- this.currentHistoryItemId = currentHistory.length;
- }
- const newItem = {
- value: {
- currentValue: Object.assign({}, value),
- previousValue: Object.assign({}, previousValue),
- changeId: this.currentHistoryItemId,
- previousChangeId,
- isUndoOrRedo
- },
- label: this.getHistoryItemLabel(previousValue, value)
- };
- if (newItem.label) {
- this.activeHistory = [
- newItem,
- ...currentHistory
- ].slice(0, this.maxHistoryItemsCount);
- this.appState.setParameter('history', {
- items: this.activeHistory.slice(),
- currentId: this.currentHistoryItemId
- });
- }
+ const defaultState = this.logsContainerService.getFiltersData(this.logsContainerService.activeLogsType);
+ const currentHistory = this.activeHistory;
+ const previousValue = this.activeHistory.length ? this.activeHistory[0].value.currentValue : defaultState;
+ const previousChangeId = this.currentHistoryItemId;
+ this.currentHistoryItemId = currentHistory.length;
+ const newItem = {
+ value: {
+ currentValue: Object.assign({}, value),
+ previousValue: Object.assign({}, previousValue),
+ changeId: this.currentHistoryItemId,
+ previousChangeId
+ },
+ label: this.getHistoryItemLabel(previousValue, value)
+ };
+ if (newItem.label) {
+ this.activeHistory = [
+ newItem,
+ ...currentHistory
+ ].slice(0, this.maxHistoryItemsCount);
+ this.activeHistory = this.activeHistory.map((item) => {
+ item.cssClass = item.value.changeId === this.currentHistoryItemId ? 'current-history-item' : '';
+ return item;
+ });
+ this.appState.setParameter('history', {
+ items: [...this.activeHistory],
+ currentId: this.currentHistoryItemId
+ });
}
}
@@ -191,15 +175,11 @@ export class HistoryManagerService {
get undoItems(): ListItem[] {
const allItems = this.activeHistory;
const startIndex = allItems.findIndex((item: ListItem): boolean => {
- return item.value.changeId === this.currentHistoryItemId && !item.value.isUndoOrRedo;
- });
- let endIndex = allItems.slice(startIndex + 1).findIndex((item: ListItem): boolean => item.value.isUndoOrRedo);
+ return item.value.changeId === this.currentHistoryItemId;
+ });
let items = [];
if (startIndex > -1) {
- if (endIndex === -1) {
- endIndex = allItems.length;
- }
- items = allItems.slice(startIndex, startIndex + endIndex + 1);
+ items = allItems.slice(startIndex);
}
return items;
}
@@ -209,18 +189,14 @@ export class HistoryManagerService {
* @returns {ListItem[]}
*/
get redoItems(): ListItem[] {
- const allItems = this.activeHistory.slice().reverse();
+ const allItems = [...this.activeHistory].reverse();
let startIndex = allItems.findIndex((item: ListItem): boolean => {
- return item.value.previousChangeId === this.currentHistoryItemId && !item.value.isUndoOrRedo;
- }),
- endIndex = allItems.slice(startIndex + 1).findIndex((item: ListItem): boolean => item.value.isUndoOrRedo);
+ return item.value.previousChangeId === this.currentHistoryItemId;
+ });
if (startIndex === -1) {
startIndex = allItems.length;
}
- if (endIndex === -1) {
- endIndex = allItems.length;
- }
- return allItems.slice(startIndex, endIndex + startIndex + 1);
+ return allItems.slice(startIndex);
}
/**
@@ -292,24 +268,11 @@ export class HistoryManagerService {
* @param {object} value
*/
private handleUndoOrRedo(value: object): void {
- const filtersForm = this.logsContainerService.filtersForm;
- this.hasNoPendingUndoOrRedo = false;
- this.logsContainerService.filtersFormSyncInProgress.next(true);
- this.filterParameters.filter(controlName => this.ignoredParameters.indexOf(controlName) === -1)
- .forEach((controlName: string): void => {
- filtersForm.controls[controlName].setValue(value[controlName], {
- emitEvent: false,
- onlySelf: true
- });
- });
- this.logsContainerService.filtersFormSyncInProgress.next(false);
- this.hasNoPendingUndoOrRedo = true;
- filtersForm.controls.isUndoOrRedo.setValue(true);
+ this.logsContainerService.resetFiltersForms(value);
}
undo(item: ListItem): void {
if (item) {
- this.hasNoPendingUndoOrRedo = false;
this.currentHistoryItemId = item.value.previousChangeId;
this.handleUndoOrRedo(item.value.previousValue);
}
@@ -317,7 +280,6 @@ export class HistoryManagerService {
redo(item: ListItem): void {
if (item) {
- this.hasNoPendingUndoOrRedo = false;
this.currentHistoryItemId = item.value.changeId;
this.handleUndoOrRedo(item.value.currentValue);
}
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 2d68977e46..4b2152c4ff 100644
--- a/ambari-logsearch-web/src/app/services/http-client.service.ts
+++ b/ambari-logsearch-web/src/app/services/http-client.service.ts
@@ -37,6 +37,8 @@ import { AppStore } from '@app/classes/models/store';
import { HttpAuthorizationErrorResponseAction } from '@app/store/actions/auth.actions';
import { isAuthorizedSelector } from '@app/store/selectors/auth.selectors';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
@Injectable()
export class HttpClientService extends Http {
@@ -105,6 +107,9 @@ export class HttpClientService extends Http {
private readonly unauthorizedStatuses = [401, 403, 419];
+ requestsPending: BehaviorSubject<number> = new BehaviorSubject(0);
+ requestInProgress: Observable<boolean> = this.requestsPending.map((totalRequest: number) => totalRequest > 0);
+
constructor(
backend: XHRBackend,
defaultOptions: RequestOptions,
@@ -185,11 +190,14 @@ export class HttpClientService extends Http {
}
return handled;
};
- return super.request(this.generateUrl(url), options).first().share()
+ const req = super.request(this.generateUrl(url), options).first().share()
.map(response => response)
.catch((error: any) => {
return handleResponseError(error) ? Observable.of(error) : Observable.throw(error);
});
+ req.subscribe(() => this.requestsPending.next(this.requestsPending.getValue() - 1));
+ this.requestsPending.next(this.requestsPending.getValue() + 1);
+ return req;
}
get(url: string, params?: HomogeneousObject<string>, urlVariables?: HomogeneousObject<string>): Observable<Response> {
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 eb4bf66057..287b1dc8fa 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
@@ -28,12 +28,14 @@ import {NotificationService} from '@modules/shared/services/notification.service
import {NotificationsService} from 'angular2-notifications/src/notifications.service';
import { LogIndexFilterService } from './log-index-filter.service';
+import { RouterTestingModule } from '@angular/router/testing';
describe('LogIndexFilterService', () => {
beforeEach(() => {
TestBed.configureTestingModule(getCommonTestingBedConfiguration({
imports: [
- ...TranslationModules
+ ...TranslationModules,
+ RouterTestingModule,
],
providers: [
AppStateService,
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 d550fbb971..0612744345 100644
--- a/ambari-logsearch-web/src/app/services/logs-container.service.ts
+++ b/ambari-logsearch-web/src/app/services/logs-container.service.ts
@@ -59,7 +59,7 @@ 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 { LogsFilteringUtilsService, defaultFilterSelections } 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';
@@ -69,6 +69,8 @@ import { Store } from '@ngrx/store';
import { AppStore } from '@app/classes/models/store';
import { isAuthorizedSelector } from '@app/store/selectors/auth.selectors';
+import { Subscription } from 'rxjs/Subscription';
+
@Injectable()
export class LogsContainerService {
@@ -118,19 +120,19 @@ export class LogsContainerService {
clusters: {
label: 'filter.clusters',
options: [],
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.clusters,
+ defaultSelection: defaultFilterSelections.clusters,
fieldName: 'cluster'
},
timeRange: { // @ToDo remove duplication, this options are in the LogsFilteringUtilsService too
label: 'logs.duration',
options: this.logsFilteringUtilsService.getTimeRandeOptionsByGroup(),
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.timeRange
+ defaultSelection: defaultFilterSelections.timeRange
},
components: {
label: 'filter.components',
iconClass: 'fa fa-cubes',
options: [],
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.components,
+ defaultSelection: defaultFilterSelections.components,
fieldName: 'type'
},
levels: {
@@ -145,14 +147,14 @@ export class LogsContainerService {
iconClass: `fa ${LogLevelComponent.classMap[cssClass]}`
};
}),
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.levels,
+ defaultSelection: defaultFilterSelections.levels,
fieldName: 'level'
},
hosts: {
label: 'filter.hosts',
iconClass: 'fa fa-server',
options: [],
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.hosts,
+ defaultSelection: defaultFilterSelections.hosts,
fieldName: 'host'
},
auditLogsSorting: {
@@ -173,7 +175,7 @@ export class LogsContainerService {
}
}
],
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.auditLogsSorting
+ defaultSelection: defaultFilterSelections.auditLogsSorting
},
serviceLogsSorting: {
label: 'sorting.title',
@@ -193,7 +195,7 @@ export class LogsContainerService {
}
}
],
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.serviceLogsSorting
+ defaultSelection: defaultFilterSelections.serviceLogsSorting
},
pageSize: {
label: 'pagination.title',
@@ -203,23 +205,20 @@ export class LogsContainerService {
value: option
};
}),
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.pageSize
+ defaultSelection: defaultFilterSelections.pageSize
},
page: {
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.page
+ defaultSelection: defaultFilterSelections.page
},
query: {
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.query
+ defaultSelection: defaultFilterSelections.query
},
users: {
label: 'filter.users',
iconClass: 'fa fa-server',
options: [],
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.users,
+ defaultSelection: defaultFilterSelections.users,
fieldName: 'reqUser'
- },
- isUndoOrRedo: {
- defaultSelection: this.logsFilteringUtilsService.defaultFilterSelections.isUndoOrRedo
}
};
@@ -369,7 +368,11 @@ export class LogsContainerService {
excludeQuery: this.logsFilteringUtilsService.getQuery(true)
};
- filtersFormSyncInProgress: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
+ filtersFormSyncInProgress$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
+
+ private pendingLogRequests: {[key: string]: Subscription[]} = {};
+ isLogsRequestInProgress$: BehaviorSubject<boolean> = new BehaviorSubject(false);
+ isGraphRequestInProgress$: BehaviorSubject<boolean> = new BehaviorSubject(false);
constructor(
private httpClient: HttpClientService, private utils: UtilsService,
@@ -392,7 +395,7 @@ export class LogsContainerService {
const item = {
[key]: formControl
};
- formControl.setValue(this.logsFilteringUtilsService.defaultFilterSelections[key]);
+ formControl.setValue(defaultFilterSelections[key]);
return Object.assign(currentObject, item);
}, {});
this.filtersForm = new FormGroup(formItems);
@@ -401,9 +404,9 @@ export class LogsContainerService {
this.clustersStorage.getAll().subscribe(this.setClustersFilters);
this.hostsStorage.getAll().subscribe(this.setHostsFilters);
- appState.getParameter('activeLog').subscribe((value: ActiveServiceLogEntry | null) => this.activeLog = value);
- appState.getParameter('isServiceLogsFileView').subscribe((value: boolean) => this.isServiceLogsFileView = value);
- appState.getParameter('activeLogsType').subscribe((value: LogsType) => {
+ appState.getParameter('activeLog').distinctUntilChanged().subscribe((value: ActiveServiceLogEntry | null) => this.activeLog = value);
+ appState.getParameter('isServiceLogsFileView').distinctUntilChanged().subscribe((value: boolean) => this.isServiceLogsFileView = value);
+ appState.getParameter('activeLogsType').distinctUntilChanged().subscribe((value: LogsType) => {
if (this.isLogsTypeSupported(value)) {
this.activeLogsType = value;
}
@@ -425,7 +428,7 @@ export class LogsContainerService {
});
});
- this.filtersForm.valueChanges.filter(() => !this.filtersFormSyncInProgress.getValue()).subscribe(this.onFiltersFormValueChange);
+ this.filtersForm.valueChanges.filter(() => !this.filtersFormSyncInProgress$.getValue()).subscribe(this.onFiltersFormValueChange);
this.auditLogsSource.subscribe((logs: AuditLog[]): void => {
const userNames = logs.map((log: AuditLog): string => log.reqUser);
@@ -460,12 +463,12 @@ export class LogsContainerService {
.filter((dataSetState: DataAvailabilityValues) => dataSetState === DataAvailabilityValues.AVAILABLE)
.first()
.subscribe(() => {
- this.filtersFormSyncInProgress.next(true);
+ this.filtersFormSyncInProgress$.next(true);
this.filtersForm.reset(
- {...this.logsFilteringUtilsService.defaultFilterSelections, ...filters},
+ {...defaultFilterSelections, ...filters},
{emitEvent: false}
);
- this.filtersFormSyncInProgress.next(false);
+ this.filtersFormSyncInProgress$.next(false);
this.onFiltersFormValueChange();
});
}
@@ -589,59 +592,76 @@ export class LogsContainerService {
loadLogs = (logsType: LogsType = this.activeLogsType): void => {
if (this.isLogsTypeSupported(logsType)) {
- this.httpClient.get(logsType, this.getParams('listFilters', {}, logsType)).subscribe((response: Response): void => {
- const jsonResponse = response.json(),
- model = this.logsTypeMap[logsType].logsModel;
- model.clear();
- if (jsonResponse) {
- const logs = jsonResponse.logList,
- count = jsonResponse.totalCount || 0;
- if (logs) {
- model.addInstances(logs);
- }
- this.totalCount = count;
- }
- });
- this.httpClient.get(this.logsTypeMap[logsType].graphRequestName, this.getParams('graphFilters', {}, logsType))
- .subscribe((response: Response): void => {
+ let pendingLogRequests = this.pendingLogRequests[logsType] || [];
+ pendingLogRequests.forEach((subscription: Subscription) => subscription.unsubscribe());
+ pendingLogRequests = [];
+
+ this.isLogsRequestInProgress$.next(true);
+ pendingLogRequests.push(
+ this.httpClient.get(logsType, this.getParams('listFilters', {}, logsType)).subscribe((response: Response): void => {
const jsonResponse = response.json(),
- model = this.logsTypeMap[logsType].graphModel;
+ model = this.logsTypeMap[logsType].logsModel;
model.clear();
if (jsonResponse) {
- const graphData = jsonResponse.graphData;
- if (graphData) {
- model.addInstances(graphData);
+ const logs = jsonResponse.logList,
+ count = jsonResponse.totalCount || 0;
+ if (logs) {
+ model.addInstances(logs);
}
+ this.totalCount = count;
}
- });
+ this.isLogsRequestInProgress$.next(false);
+ })
+ );
+ this.isGraphRequestInProgress$.next(true);
+ pendingLogRequests.push(
+ this.httpClient.get(this.logsTypeMap[logsType].graphRequestName, this.getParams('graphFilters', {}, logsType))
+ .subscribe((response: Response): void => {
+ const jsonResponse = response.json(),
+ model = this.logsTypeMap[logsType].graphModel;
+ model.clear();
+ if (jsonResponse) {
+ const graphData = jsonResponse.graphData;
+ if (graphData) {
+ model.addInstances(graphData);
+ }
+ }
+ this.isGraphRequestInProgress$.next(false);
+ })
+ );
if (logsType === 'auditLogs') {
- this.httpClient.get('topAuditLogsResources', this.getParams('topResourcesFilters', {
- field: 'resource'
- }, logsType), {
- number: this.topResourcesCount
- }).subscribe((response: Response): void => {
- const jsonResponse = response.json();
- if (jsonResponse) {
- const data = jsonResponse.graphData;
- if (data) {
- this.topResourcesGraphData = this.parseAuditLogsTopData(data);
+ pendingLogRequests.push(
+ this.httpClient.get('topAuditLogsResources', this.getParams('topResourcesFilters', {
+ field: 'resource'
+ }, logsType), {
+ number: this.topResourcesCount
+ }).subscribe((response: Response): void => {
+ const jsonResponse = response.json();
+ if (jsonResponse) {
+ const data = jsonResponse.graphData;
+ if (data) {
+ this.topResourcesGraphData = this.parseAuditLogsTopData(data);
+ }
}
- }
- });
- this.httpClient.get('topAuditLogsResources', this.getParams('topResourcesFilters', {
- field: 'reqUser'
- }, logsType), {
- number: this.topUsersCount
- }).subscribe((response: Response): void => {
- const jsonResponse = response.json();
- if (jsonResponse) {
- const data = jsonResponse.graphData;
- if (data) {
- this.topUsersGraphData = this.parseAuditLogsTopData(data);
+ })
+ );
+ pendingLogRequests.push(
+ this.httpClient.get('topAuditLogsResources', this.getParams('topResourcesFilters', {
+ field: 'reqUser'
+ }, logsType), {
+ number: this.topUsersCount
+ }).subscribe((response: Response): void => {
+ const jsonResponse = response.json();
+ if (jsonResponse) {
+ const data = jsonResponse.graphData;
+ if (data) {
+ this.topUsersGraphData = this.parseAuditLogsTopData(data);
+ }
}
- }
- });
+ })
+ );
}
+ this.pendingLogRequests[logsType] = pendingLogRequests;
} else {
console.error(`Logs Type does not supported: ${logsType}`);
}
@@ -883,7 +903,7 @@ export class LogsContainerService {
const keys = Object.keys(this.filters).filter((key: string): boolean => itemsList.indexOf(key) > -1);
return keys.reduce((currentObject: object, key: string): object => {
return Object.assign(currentObject, {
- [key]: this.logsFilteringUtilsService.defaultFilterSelections[key]
+ [key]: defaultFilterSelections[key]
});
}, {});
}
@@ -909,6 +929,7 @@ export class LogsContainerService {
.subscribe((componentName) => {
const tab = {
id: log.id || `${log.host}-${log.type}`,
+ logsType: <LogsType>'serviceLogs',
isCloseable: true,
isActive: false,
label: `${log.host} >> ${componentName || log.type}`,
diff --git a/ambari-logsearch-web/src/app/services/logs-filtering-utils.service.ts b/ambari-logsearch-web/src/app/services/logs-filtering-utils.service.ts
index 9fddade6db..d6bf06d16f 100644
--- a/ambari-logsearch-web/src/app/services/logs-filtering-utils.service.ts
+++ b/ambari-logsearch-web/src/app/services/logs-filtering-utils.service.ts
@@ -4,27 +4,97 @@
* 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
+ * '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,
+ * 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 {ListItem} from '@app/classes/list-item';
-import {CustomTimeRange, SearchBoxParameter, SortingListItem, TimeUnit, TimeUnitListItem} from '@app/classes/filtering';
+import { Injectable } from '@angular/core';
+import { ListItem } from '@app/classes/list-item';
+import { CustomTimeRange, SearchBoxParameter, SortingListItem, TimeUnit, TimeUnitListItem } from '@app/classes/filtering';
import * as moment from 'moment-timezone';
-import {HomogeneousObject} from '@app/classes/object';
-import {LogsType, SortingType} from '@app/classes/string';
-import {UtilsService} from '@app/services/utils.service';
+import { HomogeneousObject } from '@app/classes/object';
+import { LogsType, SortingType } from '@app/classes/string';
+import { UtilsService } from '@app/services/utils.service';
import { LogTypeTab } from '@app/classes/models/log-type-tab';
-// @ToDo remove duplication, this options are in the LogsContainerService
+export enum UrlParamsDifferenceType {
+ ADD = 'add',
+ REMOVE = 'remove',
+ CHANGE = 'change',
+ CLEAR = 'clear'
+};
+
+export interface UrlParamDifferences {
+ name: string;
+ type: UrlParamsDifferenceType;
+ from: any;
+ to: any;
+};
+
+export const defaultFilterSelections = {
+ clusters: [],
+ timeRange: {
+ value: {
+ type: 'LAST',
+ unit: 'h',
+ interval: 1
+ },
+ label: 'filter.timeRange.1hr'
+ },
+ components: [],
+ levels: [],
+ hosts: [],
+ auditLogsSorting: {
+ label: 'sorting.time.desc',
+ value: {
+ key: 'evtTime',
+ type: 'desc'
+ }
+ },
+ serviceLogsSorting: {
+ label: 'sorting.time.desc',
+ value: {
+ key: 'logtime',
+ type: 'desc'
+ }
+ },
+ pageSize: [{
+ label: '100',
+ value: '100'
+ }],
+ page: 0,
+ query: [],
+ users: []
+};
+
+export const defaultUrlParamsForFiltersByLogsType = {
+ serviceLogs: {
+ timeRangeType: 'LAST',
+ timeRangeUnit: 'h',
+ timeRangeInterval: 1,
+ sortingKey: 'logtime',
+ sortingType: 'desc',
+ pageSize: '100',
+ page: '0'
+ },
+ auditLogs: {
+ timeRangeType: 'LAST',
+ timeRangeUnit: 'h',
+ timeRangeInterval: 1,
+ sortingKey: 'evtTime',
+ sortingType: 'desc',
+ pageSize: '100',
+ page: '0'
+ }
+};
+
export const timeRangeFilterOptions = [{
label: 'filter.timeRange.7d',
value: {
@@ -237,46 +307,9 @@ export const timeRangeFilterOptions = [{
@Injectable()
export class LogsFilteringUtilsService {
- readonly defaultFilterSelections = {
- clusters: [],
- timeRange: {
- value: {
- type: 'LAST',
- unit: 'h',
- interval: 1
- },
- label: 'filter.timeRange.1hr'
- },
- components: [],
- levels: [],
- hosts: [],
- auditLogsSorting: {
- label: 'sorting.time.desc',
- value: {
- key: 'evtTime',
- type: 'desc'
- }
- },
- serviceLogsSorting: {
- label: 'sorting.time.desc',
- value: {
- key: 'logtime',
- type: 'desc'
- }
- },
- pageSize: [{
- label: '100',
- value: '100'
- }],
- page: 0,
- query: [],
- users: [],
- isUndoOrRedo: false
- };
-
constructor(
private utilsService: UtilsService
- ) { }
+ ) {}
getTimeRandeOptionsByGroup() {
return timeRangeFilterOptions.reduce((groups: any, item: any) => {
@@ -395,7 +428,6 @@ export class LogsFilteringUtilsService {
getParamsFromActiveFilter(activeFilter: any, activeLogsType: LogsType): {[key: string]: string} {
const {...filters} = activeFilter;
- delete filters.isUndoOrRedo;
return Object.keys(filters).reduce((currentParams, key) => {
const newParams = {
...currentParams
@@ -554,4 +586,34 @@ export class LogsFilteringUtilsService {
return [tab.id, this.getParamsFromActiveFilter(tab.activeFilters || {}, logsType)];
}
+ getUrlParamsDifferences(previousParams, currentParams): UrlParamDifferences[] {
+ const allKeys = [
+ ...Object.keys(previousParams),
+ ...Object.keys(currentParams)
+ ].reduce((uniques: string[], key: string) => {
+ return uniques.indexOf(key) === -1 ? [...uniques, key] : uniques;
+ }, []);
+ return allKeys.reduce((changes: UrlParamDifferences[], key: string) => {
+ const change: {[key: string]: any} = {};
+ if (previousParams[key] === undefined && currentParams[key] !== undefined) {
+ change.type = 'add';
+ } else if (previousParams[key] !== undefined && currentParams[key] === undefined) {
+ change.type = 'remove';
+ } else if (previousParams[key].toString() !== currentParams[key].toString()) {
+ change.type = 'change';
+ }
+ if (change.type) {
+ change.name = key;
+ change.from = previousParams[key];
+ change.to = currentParams[key];
+ return [...changes, <UrlParamDifferences>change];
+ }
+ return [...changes];
+ }, []);
+ }
+
+ private _getUrlParamValue(name: string, value: string): any[] | undefined {
+ return value !== undefined ? (name === 'query' ? JSON.parse(value) : value.split(',')) : undefined;
+ }
+
}
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 1d18406131..4f724875dd 100644
--- a/ambari-logsearch-web/src/app/services/storage/reducers.service.ts
+++ b/ambari-logsearch-web/src/app/services/storage/reducers.service.ts
@@ -37,6 +37,7 @@ 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';
+import * as filterHistory from '@app/store/reducers/filter-history.reducers';
export const reducers = {
appSettings,
@@ -57,7 +58,8 @@ export const reducers = {
clusterSelections,
logsState,
dataAvailabilityStates,
- auth: auth.reducer
+ auth: auth.reducer,
+ filterHistory: filterHistory.reducer
};
export function reducer(state: any, action: any) {
diff --git a/ambari-logsearch-web/src/app/store/actions/filter-history.actions.ts b/ambari-logsearch-web/src/app/store/actions/filter-history.actions.ts
new file mode 100644
index 0000000000..5fa278bb1b
--- /dev/null
+++ b/ambari-logsearch-web/src/app/store/actions/filter-history.actions.ts
@@ -0,0 +1,56 @@
+/**
+ * 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 { FilterUrlParamChange } from '@app/classes/models/filter-url-param-change.interface';
+import { LogsType } from '@app/classes/string';
+
+export enum FilterHistoryActionTypes {
+ ADD_FILTER_HISTORY = '[Filter History] Add',
+ SET_CURRENT_FILTER_HISTORY_BY_INDEX = '[Filter History] Set Current By Index',
+ SET_CURRENT_FILTER_HISTORY_BY_URL_PARAM = '[Filter History] Set Current By Url Param'
+}
+
+export class AddFilterHistoryAction implements Action {
+ readonly type = FilterHistoryActionTypes.ADD_FILTER_HISTORY;
+ constructor(public payload: {
+ logType: LogsType,
+ change: FilterUrlParamChange
+ }) {}
+}
+
+export class SetCurrentFilterHistoryByIndexAction implements Action {
+ readonly type = FilterHistoryActionTypes.SET_CURRENT_FILTER_HISTORY_BY_INDEX;
+ constructor(public payload: {
+ logType: LogsType,
+ index: number
+ }) {}
+}
+
+export class SetCurrentFilterHistoryByUrlParamAction implements Action {
+ readonly type = FilterHistoryActionTypes.SET_CURRENT_FILTER_HISTORY_BY_URL_PARAM;
+ constructor(public payload: {
+ logType: LogsType,
+ url: string
+ }) {}
+}
+
+export type FilterHistoryActions =
+ | AddFilterHistoryAction
+ | SetCurrentFilterHistoryByIndexAction
+ | SetCurrentFilterHistoryByUrlParamAction;
diff --git a/ambari-logsearch-web/src/app/store/reducers/filter-history.reducers.ts b/ambari-logsearch-web/src/app/store/reducers/filter-history.reducers.ts
new file mode 100644
index 0000000000..41418bbed4
--- /dev/null
+++ b/ambari-logsearch-web/src/app/store/reducers/filter-history.reducers.ts
@@ -0,0 +1,111 @@
+/**
+ * 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 { FilterUrlParamChange } from '@app/classes/models/filter-url-param-change.interface';
+
+import { FilterHistoryActions, FilterHistoryActionTypes } from '../actions/filter-history.actions';
+
+export interface LogTypeFilterHistory {
+ changes: FilterUrlParamChange[];
+ currentChangeIndex: number;
+};
+
+export interface FilterHistoryState {
+ [key: string]: LogTypeFilterHistory;
+}
+
+export const initialState: FilterHistoryState = {};
+
+export function reducer(state = initialState, action: FilterHistoryActions): FilterHistoryState {
+ switch (action.type) {
+ case FilterHistoryActionTypes.ADD_FILTER_HISTORY: {
+ const payload = action.payload;
+ const history: LogTypeFilterHistory = state[payload.logType] ? {...state[payload.logType]} : {
+ changes: [],
+ currentChangeIndex: -1
+ };
+ // we throw away the 'rest' of the history changes when the user create a new filter
+ // while he/she did undo/redo so when the current index is pointing other than the last element
+ if (history.currentChangeIndex > -1 && history.currentChangeIndex < history.changes.length - 1) {
+ history.changes = [...history.changes.slice(0, history.currentChangeIndex + 1)];
+ }
+ history.changes = [...history.changes, payload.change];
+ history.currentChangeIndex = history.changes.length - 1;
+ return {
+ ...state,
+ [payload.logType]: history
+ };
+ }
+ case FilterHistoryActionTypes.SET_CURRENT_FILTER_HISTORY_BY_INDEX: {
+ const payload = action.payload;
+ const history: LogTypeFilterHistory = state[payload.logType];
+ const subState = {};
+ if (history && history.changes.length > payload.index) {
+ subState[payload.logType] = {
+ ...history,
+ currentChangeIndex: payload.index
+ };
+ return {
+ ...state,
+ ...subState
+ };
+ }
+ return state;
+ }
+ case FilterHistoryActionTypes.SET_CURRENT_FILTER_HISTORY_BY_URL_PARAM: {
+ const payload = action.payload;
+ const {logType, url} = payload;
+ const history: LogTypeFilterHistory = state[logType];
+ const subState = {};
+ if (history && history.changes.length) {
+ const index = history.changes.findIndex((change) => change.currentPath === url);
+ subState[logType] = {
+ ...history,
+ currentChangeIndex: index > -1 ? index : history.currentChangeIndex
+ };
+ return {
+ ...state,
+ ...subState
+ };
+ }
+ return state;
+ }
+ default: {
+ return state;
+ }
+ };
+}
+
+export const createFilterHistoryGetterById = (id: string): (state: FilterHistoryState) => LogTypeFilterHistory => {
+ return (state: FilterHistoryState): LogTypeFilterHistory => state[id];
+};
+export const getServiceLogsFilterHistory = createFilterHistoryGetterById('serviceLogs');
+export const getAuditLogsFilterHistory = createFilterHistoryGetterById('auditLogs');
+
+export const getFilterHistoryList = (filterHistory: LogTypeFilterHistory): FilterUrlParamChange[] => filterHistory.changes;
+export const getFilterHistoryCurrentChangeIndex = (filterHistory: LogTypeFilterHistory): number => filterHistory.currentChangeIndex;
+
+export const getUndoHistoryList = (filterHistory: LogTypeFilterHistory): FilterUrlParamChange[] => (
+ getFilterHistoryList(filterHistory).slice(0, getFilterHistoryCurrentChangeIndex(filterHistory))
+);
+export const getRedoHistoryList = (filterHistory: LogTypeFilterHistory): FilterUrlParamChange[] => (
+ getFilterHistoryList(filterHistory).slice(getFilterHistoryCurrentChangeIndex(filterHistory) + 1)
+);
+export const getCurrentFilterUrlParamChange = (filterHistory: LogTypeFilterHistory): FilterUrlParamChange => (
+ getFilterHistoryList(filterHistory)[getFilterHistoryCurrentChangeIndex(filterHistory)]
+);
diff --git a/ambari-logsearch-web/src/app/store/selectors/app-state.selectors.ts b/ambari-logsearch-web/src/app/store/selectors/app-state.selectors.ts
new file mode 100644
index 0000000000..4262489a89
--- /dev/null
+++ b/ambari-logsearch-web/src/app/store/selectors/app-state.selectors.ts
@@ -0,0 +1,41 @@
+/**
+ * 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, Selector } from 'reselect';
+
+import { AppStore } from '@app/classes/models/store';
+import { AppState } from '@app/classes/models/app-state';
+import { LogsType } from '@app/classes/string';
+import { ActiveServiceLogEntry } from '@app/classes/active-service-log-entry';
+
+export const selectAppState = (state: AppStore): AppState => state.appState;
+
+export const selectActiveLogsType: Selector<AppStore, LogsType> = createSelector(
+ selectAppState,
+ (appState: AppState): LogsType => appState.activeLogsType
+);
+
+export const selectActiveLog: Selector<AppStore, ActiveServiceLogEntry> = createSelector(
+ selectAppState,
+ (appState: AppState): ActiveServiceLogEntry => appState.activeLog
+);
+
+export const selectActiveLogId: Selector<AppStore, string> = createSelector(
+ selectActiveLog,
+ (activeLog: ActiveServiceLogEntry): string => activeLog.id
+);
diff --git a/ambari-logsearch-web/src/app/store/selectors/audit-logs-fields.selectors.ts b/ambari-logsearch-web/src/app/store/selectors/audit-logs-fields.selectors.ts
new file mode 100644
index 0000000000..60eddcc5b5
--- /dev/null
+++ b/ambari-logsearch-web/src/app/store/selectors/audit-logs-fields.selectors.ts
@@ -0,0 +1,51 @@
+/**
+ * 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, Selector } from 'reselect';
+import { AppStore } from '@app/classes/models/store';
+import { LogField } from '@app/classes/object';
+import { ResponseRootProperties } from '@app/services/storage/audit-logs-fields.service';
+
+export const selectAuditLogsFieldState = (state: AppStore): LogField[] => state.auditLogsFields;
+
+export const selectDefaultAuditLogsFields = createSelector(
+ selectAuditLogsFieldState,
+ (root) => root && root[ResponseRootProperties.DEFAULTS]
+);
+
+export const createAuditLogsFieldComponentFieldsSelectorByComponentName = (componentName: string): Selector<AppStore, LogField[]> => (
+ createSelector(
+ selectAuditLogsFieldState,
+ (root): LogField[] => {
+ const overrides = root[ResponseRootProperties.OVERRIDES];
+ return (overrides && overrides[componentName]) || root[ResponseRootProperties.DEFAULTS];
+ }
+ )
+);
+
+export const createAuditLogsFieldLabelSelectorByComponentNameAndFieldName = (
+ componentName: string,
+ fieldName: string
+): Selector<AppStore, string> => createSelector(
+ selectAuditLogsFieldState,
+ createAuditLogsFieldComponentFieldsSelectorByComponentName(componentName),
+ (fields: LogField[]): string => {
+ const field: LogField = fields.find((nextField: LogField) => nextField.name === fieldName);
+ return field ? field.label : fieldName;
+ }
+);
diff --git a/ambari-logsearch-web/src/app/store/selectors/auth.selectors.ts b/ambari-logsearch-web/src/app/store/selectors/auth.selectors.ts
index 23decd96a9..1899bee7af 100644
--- a/ambari-logsearch-web/src/app/store/selectors/auth.selectors.ts
+++ b/ambari-logsearch-web/src/app/store/selectors/auth.selectors.ts
@@ -21,27 +21,27 @@ 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 selectAuthState = (state: AppStore): fromAuth.State => state.auth;
-export const authStatusSelector = createSelector( getAuthState, fromAuth.getStatus );
-export const authMessageSelector = createSelector( getAuthState, fromAuth.getMessage );
+export const selectAuthStatus = createSelector( selectAuthState, fromAuth.getStatus );
+export const selectAuthMessage = createSelector( selectAuthState, fromAuth.getMessage );
export const isAuthorizedSelector = createSelector(
- authStatusSelector,
+ selectAuthStatus,
fromAuth.isAuthorized
);
export const isLoginInProgressSelector = createSelector(
- authStatusSelector,
+ selectAuthStatus,
fromAuth.isLoginInProgress
);
export const isLoggedOutSelector = createSelector(
- authStatusSelector,
+ selectAuthStatus,
fromAuth.isLoggedOut
);
export const isCheckingAuthStatusInProgressSelector = createSelector(
- authStatusSelector,
+ selectAuthStatus,
fromAuth.isCheckingAuthInProgress
);
diff --git a/ambari-logsearch-web/src/app/store/selectors/components.selectors.ts b/ambari-logsearch-web/src/app/store/selectors/components.selectors.ts
new file mode 100644
index 0000000000..6788493c11
--- /dev/null
+++ b/ambari-logsearch-web/src/app/store/selectors/components.selectors.ts
@@ -0,0 +1,34 @@
+/**
+ * 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, Selector } from 'reselect';
+
+import { AppStore } from '@app/classes/models/store';
+import { NodeItem } from '@app/classes/models/node-item';
+
+export const selectComponentsList = (state: AppStore): NodeItem[] => state.components;
+
+export const selectComponentsLabels = createSelector(
+ selectComponentsList,
+ (components: NodeItem[]) => (components || []).reduce((labels: {[key: string]: string}, component: NodeItem) => (
+ {
+ ...labels,
+ [component.name]: component.label || component.name
+ }
+ ), {})
+);
diff --git a/ambari-logsearch-web/src/app/store/selectors/data-availability.selectors.ts b/ambari-logsearch-web/src/app/store/selectors/data-availability.selectors.ts
new file mode 100644
index 0000000000..0b0fde5ecc
--- /dev/null
+++ b/ambari-logsearch-web/src/app/store/selectors/data-availability.selectors.ts
@@ -0,0 +1,49 @@
+/**
+ * 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 { AppStore } from '@app/classes/models/store';
+import { DataAvaibilityStatesModel } from '@modules/app-load/models/data-availability-state.model';
+import { DataAvailabilityValues } from '@app/classes/string';
+import { DataStateStoreKeys, baseDataKeys } from '@app/modules/app-load/services/app-load.service';
+
+export const selectDataAvailabilityState = (state: AppStore): DataAvaibilityStatesModel => state.dataAvailabilityStates;
+
+export const selectBaseDataAvailability = createSelector(
+ selectDataAvailabilityState,
+ (dataAvailabilityState: DataAvaibilityStatesModel): DataAvailabilityValues => {
+ const values: DataAvailabilityValues[] = Object.keys(dataAvailabilityState)
+ .filter((key: DataStateStoreKeys): boolean => baseDataKeys.indexOf(key) > -1)
+ .map((key): DataAvailabilityValues => dataAvailabilityState[key]);
+ let nextDataState: DataAvailabilityValues = DataAvailabilityValues.NOT_AVAILABLE;
+ if (values.indexOf(DataAvailabilityValues.ERROR) > -1) {
+ nextDataState = DataAvailabilityValues.ERROR;
+ } else if (values.indexOf(DataAvailabilityValues.LOADING) > -1) {
+ nextDataState = DataAvailabilityValues.LOADING;
+ } else if ( values.filter((value: DataAvailabilityValues) => value !== DataAvailabilityValues.AVAILABLE).length === 0 ) {
+ nextDataState = DataAvailabilityValues.AVAILABLE;
+ }
+ return nextDataState;
+ }
+);
+
+export const isBaseDataAvailable = createSelector(
+ selectBaseDataAvailability,
+ (baseDataAvailabilityState: DataAvailabilityValues) => baseDataAvailabilityState === DataAvailabilityValues.AVAILABLE
+);
diff --git a/ambari-logsearch-web/src/app/store/selectors/filter-history.selectors.ts b/ambari-logsearch-web/src/app/store/selectors/filter-history.selectors.ts
new file mode 100644
index 0000000000..fd88c4a1bc
--- /dev/null
+++ b/ambari-logsearch-web/src/app/store/selectors/filter-history.selectors.ts
@@ -0,0 +1,94 @@
+/**
+ * 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, Selector } from 'reselect';
+
+import { AppStore } from '@app/classes/models/store';
+import * as fromFilterHistoryReducers from '@app/store/reducers/filter-history.reducers';
+import { FilterUrlParamChange } from '@app/classes/models/filter-url-param-change.interface';
+import * as fromAppStateSelector from '@app/store/selectors/app-state.selectors';
+
+
+export const selectFilterHistoryState = (state: AppStore): fromFilterHistoryReducers.FilterHistoryState => state.filterHistory;
+
+export const selectActiveFilterHistory = createSelector(
+ selectFilterHistoryState,
+ fromAppStateSelector.selectActiveLogsType,
+ (filterHistoryState, activeLogsType): fromFilterHistoryReducers.LogTypeFilterHistory => (
+ filterHistoryState && filterHistoryState[activeLogsType]
+ )
+);
+
+export const selectActiveFilterHistoryChangeIndex = createSelector(
+ selectActiveFilterHistory,
+ (logTypeFilterHistory: fromFilterHistoryReducers.LogTypeFilterHistory): number => (
+ logTypeFilterHistory && logTypeFilterHistory.currentChangeIndex
+ )
+);
+
+export const selectActiveFilterHistoryChanges = createSelector(
+ selectActiveFilterHistory,
+ (logTypeFilterHistory: fromFilterHistoryReducers.LogTypeFilterHistory): FilterUrlParamChange[] => (
+ logTypeFilterHistory && logTypeFilterHistory.changes
+ )
+);
+
+export const selectActiveFilterHistoryChangesUndoItems = createSelector(
+ selectActiveFilterHistoryChanges,
+ selectActiveFilterHistoryChangeIndex,
+ (items: FilterUrlParamChange[], changeIndex: number): FilterUrlParamChange[] => items && items.slice(0, changeIndex)
+);
+
+export const selectActiveFilterHistoryChangesRedoItems = createSelector(
+ selectActiveFilterHistoryChanges,
+ selectActiveFilterHistoryChangeIndex,
+ (items: FilterUrlParamChange[], changeIndex: number): FilterUrlParamChange[] => items && items.slice(changeIndex + 1)
+);
+
+export const createFilterHistorySelectorById = (id: string): Selector<AppStore, fromFilterHistoryReducers.LogTypeFilterHistory> => (
+ createSelector(
+ selectFilterHistoryState,
+ (filterHistoryState): fromFilterHistoryReducers.LogTypeFilterHistory => filterHistoryState[id]
+ )
+);
+
+export const createFilterHistoryChangeIndexSelectorById = (id: string): Selector<AppStore, number> => createSelector(
+ createFilterHistorySelectorById(id),
+ (logTypeFilterHistory: fromFilterHistoryReducers.LogTypeFilterHistory): number => (
+ logTypeFilterHistory && logTypeFilterHistory.currentChangeIndex
+ )
+);
+
+export const createFilterHistoryChangesSelectorById = (id: string): Selector<AppStore, FilterUrlParamChange[]> => createSelector(
+ createFilterHistorySelectorById(id),
+ (logTypeFilterHistory: fromFilterHistoryReducers.LogTypeFilterHistory): FilterUrlParamChange[] => (
+ logTypeFilterHistory && logTypeFilterHistory.changes
+ )
+);
+
+export const createFilterHistoryChangesUndoItemsSelectorById = (id: string): Selector<AppStore, FilterUrlParamChange[]> => createSelector(
+ createFilterHistoryChangesSelectorById(id),
+ createFilterHistoryChangeIndexSelectorById(id),
+ (items: FilterUrlParamChange[], changeIndex: number): FilterUrlParamChange[] => items && items.slice(0, changeIndex)
+);
+
+export const createFilterHistoryChangesRedoItemsSelectorById = (id: string): Selector<AppStore, FilterUrlParamChange[]> => createSelector(
+ createFilterHistoryChangesSelectorById(id),
+ createFilterHistoryChangeIndexSelectorById(id),
+ (items: FilterUrlParamChange[], changeIndex: number): FilterUrlParamChange[] => items && items.slice(changeIndex + 1)
+);
diff --git a/ambari-logsearch-web/src/app/store/selectors/service-logs-fields.selectors.ts b/ambari-logsearch-web/src/app/store/selectors/service-logs-fields.selectors.ts
new file mode 100644
index 0000000000..6fa28af741
--- /dev/null
+++ b/ambari-logsearch-web/src/app/store/selectors/service-logs-fields.selectors.ts
@@ -0,0 +1,31 @@
+/**
+ * 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, Selector } from 'reselect';
+import { AppStore } from '@app/classes/models/store';
+import { LogField } from '@app/classes/object';
+
+export const selectServiceLogsFieldState = (state: AppStore): LogField[] => state.serviceLogsFields;
+
+export const createServiceLogsFieldLabelSelectorByFieldName = (fieldName: string): Selector<AppStore, string> => createSelector(
+ selectServiceLogsFieldState,
+ (fields: LogField[]): string => {
+ const field: LogField = fields.find((nextField: LogField) => nextField.name === fieldName);
+ return field ? field.label : fieldName;
+ }
+);
diff --git a/ambari-logsearch-web/src/app/test-config.spec.ts b/ambari-logsearch-web/src/app/test-config.spec.ts
index 0bad5f809c..bc770449db 100644
--- a/ambari-logsearch-web/src/app/test-config.spec.ts
+++ b/ambari-logsearch-web/src/app/test-config.spec.ts
@@ -67,7 +67,7 @@ export const getCommonTestingBedConfiguration = (
) => ({
imports: [
...TranslationModules,
- RouterTestingModule,
+ // RouterTestingModule,
StoreModule.provideStore({
clusters,
auth: auth.reducer
diff --git a/ambari-logsearch-web/src/assets/i18n/en.json b/ambari-logsearch-web/src/assets/i18n/en.json
index d8b38018a6..770f5e9dfe 100644
--- a/ambari-logsearch-web/src/assets/i18n/en.json
+++ b/ambari-logsearch-web/src/assets/i18n/en.json
@@ -7,6 +7,7 @@
"common.name": "Name",
"common.value": "Value",
"common.settings": "Settings",
+ "common.loading": "Loading...",
"common.form.errors.required": "This field is required",
@@ -101,6 +102,85 @@
"filter.timeRange.error.tooShort": "The selected time range is too short.",
+ "filterHistory": {
+ "initialState": "Initial filter values",
+ "paramNames": {
+ "clusters": "Cluster",
+ "timeRange": "Time Range",
+ "components": "Component",
+ "levels": "Level",
+ "hosts": "Host",
+ "auditLogsSorting": "Sort by",
+ "serviceLogsSorting": "Sort by",
+ "pageSize": "Result Per Page",
+ "page": "Page Number",
+ "query": "Query",
+ "users": "User",
+ "sortingType": "Sorting Direction",
+ "sortingKey": "Sorting Field"
+ },
+ "single": {
+ "actionLabel": {
+ "change": "changed"
+ },
+ "changeLabel": {
+ "change": "{{fieldLabel}}: {{valueLabel}}"
+ }
+ },
+ "multiple": {
+ "actionLabel": {
+ "add": "selected",
+ "change": "changed",
+ "remove": "unselected",
+ "clear": "selection cleared"
+ },
+ "changeLabel": {
+ "add": "{{fieldLabel}}: {{valueLabel}}",
+ "change": "{{fieldLabel}}: {{valueLabel}}",
+ "remove": "{{fieldLabel}}: {{valueLabel}}",
+ "clear": "{{fieldLabel}}: {{valueLabel}}"
+ }
+ },
+ "query": {
+ "type": {
+ "include": "include",
+ "exclude": "exclude"
+ },
+ "changeLabel": {
+ "add": "{{queryType}} query added: {{fieldLabel}} - {{valueLabel}}",
+ "remove": "{{queryType}} query removed: {{fieldLabel}} - {{valueLabel}}",
+ "clear": "Query cleared"
+ }
+ },
+ "timeRange": {
+ "type": {
+ "last": "last",
+ "past": "previous",
+ "current": "this",
+ "today": "today",
+ "custom": ""
+ },
+ "unit": {
+ "d": ["day", "days"],
+ "M": ["month", "months"],
+ "y": ["year", "years"],
+ "w": ["week", "weeks"],
+ "m": ["minute", "minutes"],
+ "h": ["hour", "hours"]
+ },
+ "valueLabel": {
+ "last": "{{typeLabel}} {{value}} {{unitLabel}}",
+ "past": "{{typeLabel}} {{value}} {{unitLabel}}",
+ "yesterday": "yesterday",
+ "current": "{{typeLabel}} {{value}} {{unitLabel}}",
+ "today": "today",
+ "custom": "`{{valueStart}} - {{valueEnd}}`"
+ },
+ "changeLabel": "{{fieldLabel}}: {{valueLabel}}",
+ "dateTimeFormat": "YYYY-MM-DD hh:mm A zz"
+ }
+ },
+
"levels.fatal": "Fatal",
"levels.error": "Error",
"levels.warn": "Warn",
@@ -279,6 +359,20 @@
"dataAvaibilityState.clustersDataState.label": "Loading clusters",
"dataAvaibilityState.hostsDataState.label": "Loading hosts",
"dataAvaibilityState.componentsDataState.label": "Loading components",
- "dataAvaibilityState.hasError.message": "We were not able to load the data. Please check your internet connection and reload the page!"
+ "dataAvaibilityState.hasError.message": "We were not able to load the data. Please check your internet connection and reload the page!",
+
+ "urlParamsLabels": {
+ "clusters": "Clusters",
+ "timeRange": "Time range",
+ "components": "Components",
+ "levels": "Levels",
+ "hosts": "Hosts",
+ "auditLogsSorting": "Sort by",
+ "serviceLogsSorting": "Sort by",
+ "pageSize": "# of logs",
+ "page": "Page",
+ "query": "Query",
+ "users": "Users"
+ }
}
----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
With regards,
Apache Git Services