You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by to...@apache.org on 2018/11/13 15:26:07 UTC
[ambari-logsearch] branch ui-branch-fix updated: [AMBARI-24551]
[Log Search UI] get rid of redundant requests after undoing or redoing
several history steps (#22)
This is an automated email from the ASF dual-hosted git repository.
tobiasistvan pushed a commit to branch ui-branch-fix
in repository https://gitbox.apache.org/repos/asf/ambari-logsearch.git
The following commit(s) were added to refs/heads/ui-branch-fix by this push:
new 5670040 [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps (#22)
5670040 is described below
commit 567004042bc974f6c13b45ec596d4558c80d026f
Author: Istvan Tobias <to...@gmail.com>
AuthorDate: Tue Nov 13 16:26:01 2018 +0100
[AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps (#22)
* [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps
* [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps - PR fixes
* In progress
* [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps - fixing form control implementations
* [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps - change history manager with actions/reducers/effects focused solution.
* [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps - working history manager
* [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps - request in progress indicators
* [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps - fixing dropdown with icons, optimize code readibility, changing labels
* [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps - cleaning up the branch, writing tests fixing issues revealed by unit tests.
* [AMBARI-24551] [Log Search UI] get rid of redundant requests after undoing or redoing several history steps - PR change requests
---
ambari-logsearch-web/src/app/app-routing.module.ts | 19 +-
ambari-logsearch-web/src/app/app.module.ts | 8 +-
.../models/filter-url-param-change.interface.ts} | 18 +-
.../src/app/classes/models/store.ts | 2 +
.../action-menu/action-menu.component.html | 14 +-
.../action-menu/action-menu.component.less | 72 +---
.../action-menu/action-menu.component.ts | 46 +--
.../src/app/components/app.component.html | 4 +
.../src/app/components/app.component.less | 25 ++
.../src/app/components/app.component.ts | 7 +-
.../cluster-filter/cluster-filter.component.ts | 4 +-
.../filter-button/filter-button.component.ts | 59 +---
.../filter-history-manager.component.html | 29 ++
.../filter-history-manager.component.less | 114 ++++++
.../filter-history-manager.component.spec.ts | 296 ++++++++++++++++
.../filter-history-manager.component.ts | 381 +++++++++++++++++++++
.../filters-panel/filters-panel.component.html | 2 +-
.../filters-panel/filters-panel.component.ts | 20 +-
.../log-index-filter/log-index-filter.component.ts | 2 +-
.../components/login-form/login-form.component.ts | 4 +-
.../logs-container/logs-container.component.html | 14 +-
.../logs-container/logs-container.component.less | 27 ++
.../logs-container/logs-container.component.ts | 129 ++++---
.../menu-button/menu-button.component.spec.ts | 4 +-
.../menu-button/menu-button.component.ts | 58 +++-
.../pagination-controls.component.ts | 24 +-
.../components/search-box/search-box.component.ts | 29 +-
.../service-logs-table.component.ts | 6 +
.../time-range-picker.component.ts | 11 +-
.../src/app/modules/app-load/app-load.module.ts | 4 +-
.../models/data-availability-state.model.ts | 1 +
.../src/app/modules/shared/animations.less | 31 ++
.../dropdown-button/dropdown-button.component.ts | 24 +-
.../dropdown-list/dropdown-list.component.html | 2 +-
.../dropdown-list/dropdown-list.component.less | 5 +-
.../dropdown-list/dropdown-list.component.ts | 12 +-
.../filter-dropdown/filter-dropdown.component.ts | 15 +-
.../modal-dialog/modal-dialog.component.spec.ts | 2 +
.../src/app/modules/shared/shared.module.ts | 34 +-
.../src/app/modules/shared/variables.less | 1 +
.../src/app/services/filter-history.guard.ts | 128 +++++++
.../src/app/services/history-manager.service.ts | 114 ++----
.../src/app/services/http-client.service.ts | 10 +-
.../app/services/log-index-filter.service.spec.ts | 4 +-
.../src/app/services/logs-container.service.ts | 159 +++++----
.../app/services/logs-filtering-utils.service.ts | 158 ++++++---
.../src/app/services/storage/reducers.service.ts | 4 +-
.../app/store/actions/filter-history.actions.ts | 56 +++
.../app/store/reducers/filter-history.reducers.ts | 111 ++++++
.../{auth.selectors.ts => app-state.selectors.ts} | 34 +-
.../store/selectors/audit-logs-fields.selectors.ts | 51 +++
.../src/app/store/selectors/auth.selectors.ts | 14 +-
.../selectors/components.selectors.ts} | 31 +-
.../store/selectors/data-availability.selectors.ts | 49 +++
.../store/selectors/filter-history.selectors.ts | 94 +++++
.../selectors/service-logs-fields.selectors.ts} | 28 +-
ambari-logsearch-web/src/app/test-config.spec.ts | 2 +-
ambari-logsearch-web/src/assets/i18n/en.json | 96 +++++-
58 files changed, 2098 insertions(+), 604 deletions(-)
diff --git a/ambari-logsearch-web/src/app/app-routing.module.ts b/ambari-logsearch-web/src/app/app-routing.module.ts
index a55e51a..5c8c4a1 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 097bf04..9026f67 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/modules/shared/animations.less b/ambari-logsearch-web/src/app/classes/models/filter-url-param-change.interface.ts
similarity index 64%
copy from ambari-logsearch-web/src/app/modules/shared/animations.less
copy to ambari-logsearch-web/src/app/classes/models/filter-url-param-change.interface.ts
index 5b8a04c..2428fc0 100644
--- a/ambari-logsearch-web/src/app/modules/shared/animations.less
+++ b/ambari-logsearch-web/src/app/classes/models/filter-url-param-change.interface.ts
@@ -15,19 +15,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@keyframes rotateplane {
- 0% {
- transform: perspective(120px) rotateX(0deg) rotateY(0deg);
- } 50% {
- transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
- } 100% {
- transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
- }
-}
-.square-spinner(@size: 40px, @background: #3FAE2A, @speed: 1.2s) {
- width: @size;
- height: @size;
- background: @background;
- animation: rotateplane @speed infinite ease-in-out;
+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 f106b17..3811de1 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 e64a89c..f8c65de 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 a8c6e05..db9a86b 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 46a0a76..721ae93 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 47d461b..f788ab7 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 @@
<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 3e56671..5d51fa8 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 68d220e..1d6e29e 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 086160b..391d117 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 af14925..d6f24e5 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 0000000..c34a46e
--- /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 0000000..1a0cd46
--- /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 0000000..2f2e738
--- /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 0000000..ddf0df5
--- /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 657d1ea..724b0a4 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 df863a3..90fef77 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 73c8604..d026419 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 8d5070b..6365514 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 c319ca9..1add8a3 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 ef61abe..f0f4765 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 34eb2a4..842218c 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 4e77db5..2691273 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 788494c..faf2165 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 5f85da7..b476bf3 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 62835bb..da33f60 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 757b4a0..bfb6068 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
@@ -53,6 +53,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.
*/
@ViewChild('tableListEl', {
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 e4e146f..3d031b2 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 2f93cb3..c717b32 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 d819dec..9a512df 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 5b8a04c..5f33c46 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 ab519d0..74341ae 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 fac626f..89a794b 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 5ce1061..7461da3 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 1809637..9967c80 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 6140e7d..669fcc9 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 f13872c..7879377 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 8269852..c520c38 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 b917527..437c556 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 0000000..95997cd
--- /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 2a3f533..b484cf1 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 2d68977..4b2152c 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 eb4bf66..287b1dc 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 d550fbb..0612744 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 9fddade..d6bf06d 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 1d18406..4f72487 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 0000000..5fa278b
--- /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 0000000..41418bb
--- /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/auth.selectors.ts b/ambari-logsearch-web/src/app/store/selectors/app-state.selectors.ts
similarity index 52%
copy from ambari-logsearch-web/src/app/store/selectors/auth.selectors.ts
copy to ambari-logsearch-web/src/app/store/selectors/app-state.selectors.ts
index 23decd9..4262489 100644
--- a/ambari-logsearch-web/src/app/store/selectors/auth.selectors.ts
+++ b/ambari-logsearch-web/src/app/store/selectors/app-state.selectors.ts
@@ -16,32 +16,26 @@
* limitations under the License.
*/
-import { createSelector } from 'reselect';
+import { createSelector, Selector } from 'reselect';
-import * as fromAuth from '@app/store/reducers/auth.reducers';
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 getAuthState = (state: AppStore): fromAuth.State => state.auth;
+export const selectAppState = (state: AppStore): AppState => state.appState;
-export const authStatusSelector = createSelector( getAuthState, fromAuth.getStatus );
-export const authMessageSelector = createSelector( getAuthState, fromAuth.getMessage );
-
-export const isAuthorizedSelector = createSelector(
- authStatusSelector,
- fromAuth.isAuthorized
-);
-
-export const isLoginInProgressSelector = createSelector(
- authStatusSelector,
- fromAuth.isLoginInProgress
+export const selectActiveLogsType: Selector<AppStore, LogsType> = createSelector(
+ selectAppState,
+ (appState: AppState): LogsType => appState.activeLogsType
);
-export const isLoggedOutSelector = createSelector(
- authStatusSelector,
- fromAuth.isLoggedOut
+export const selectActiveLog: Selector<AppStore, ActiveServiceLogEntry> = createSelector(
+ selectAppState,
+ (appState: AppState): ActiveServiceLogEntry => appState.activeLog
);
-export const isCheckingAuthStatusInProgressSelector = createSelector(
- authStatusSelector,
- fromAuth.isCheckingAuthInProgress
+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 0000000..60eddcc
--- /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 23decd9..1899bee 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/modules/shared/animations.less b/ambari-logsearch-web/src/app/store/selectors/components.selectors.ts
similarity index 59%
copy from ambari-logsearch-web/src/app/modules/shared/animations.less
copy to ambari-logsearch-web/src/app/store/selectors/components.selectors.ts
index 5b8a04c..6788493 100644
--- a/ambari-logsearch-web/src/app/modules/shared/animations.less
+++ b/ambari-logsearch-web/src/app/store/selectors/components.selectors.ts
@@ -15,19 +15,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@keyframes rotateplane {
- 0% {
- transform: perspective(120px) rotateX(0deg) rotateY(0deg);
- } 50% {
- transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
- } 100% {
- transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
- }
-}
-.square-spinner(@size: 40px, @background: #3FAE2A, @speed: 1.2s) {
- width: @size;
- height: @size;
- background: @background;
- animation: rotateplane @speed infinite ease-in-out;
-}
+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 0000000..0b0fde5
--- /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 0000000..fd88c4a
--- /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/modules/shared/animations.less b/ambari-logsearch-web/src/app/store/selectors/service-logs-fields.selectors.ts
similarity index 57%
copy from ambari-logsearch-web/src/app/modules/shared/animations.less
copy to ambari-logsearch-web/src/app/store/selectors/service-logs-fields.selectors.ts
index 5b8a04c..6fa28af 100644
--- a/ambari-logsearch-web/src/app/modules/shared/animations.less
+++ b/ambari-logsearch-web/src/app/store/selectors/service-logs-fields.selectors.ts
@@ -15,19 +15,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@keyframes rotateplane {
- 0% {
- transform: perspective(120px) rotateX(0deg) rotateY(0deg);
- } 50% {
- transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
- } 100% {
- transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
- }
-}
-.square-spinner(@size: 40px, @background: #3FAE2A, @speed: 1.2s) {
- width: @size;
- height: @size;
- background: @background;
- animation: rotateplane @speed infinite ease-in-out;
-}
+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 0bad5f8..bc77044 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 d8b3801..770f5e9 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"
+ }
}