This is an automated email from the ASF dual-hosted git repository. ababiichuk pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/ambari.git
The following commit(s) were added to refs/heads/trunk by this push: new 3869055 AMBARI-22900 Log Search UI: implement 'History' functionality 3869055 is described below commit 3869055ed2cf643271b103fe85c161940c7196a0 Author: aBabiichuk <ababiic...@hortonworks.com> AuthorDate: Fri Feb 2 14:21:16 2018 +0200 AMBARI-22900 Log Search UI: implement 'History' functionality --- .../ambari-logsearch-web/src/app/app.module.ts | 13 +- .../classes/components/graph/graph.component.ts | 18 +- .../src/app/classes/filtering.ts | 2 +- .../src/app/classes/list-item.ts | 5 +- .../src/app/classes/models/app-state.ts | 13 +- .../action-menu/action-menu.component.html | 15 +- .../action-menu/action-menu.component.spec.ts | 69 ++++- .../action-menu/action-menu.component.ts | 109 +++---- .../audit-logs-entries.component.html | 4 +- .../audit-logs-entries.component.spec.ts | 2 + .../audit-logs-table.component.html | 7 +- .../audit-logs-table/audit-logs-table.component.ts | 8 +- .../context-menu/context-menu.component.spec.ts | 6 +- .../dropdown-button/dropdown-button.component.html | 20 +- .../dropdown-button/dropdown-button.component.less | 8 +- .../dropdown-button.component.spec.ts | 12 +- .../dropdown-button/dropdown-button.component.ts | 40 +-- .../dropdown-list/dropdown-list.component.html | 28 +- .../dropdown-list/dropdown-list.component.spec.ts | 6 +- .../dropdown-list/dropdown-list.component.ts | 39 ++- .../filter-button/filter-button.component.spec.ts | 12 +- .../filter-dropdown.component.spec.ts | 12 +- .../filter-dropdown/filter-dropdown.component.ts | 5 - .../filters-panel/filters-panel.component.html | 10 +- .../filters-panel/filters-panel.component.ts | 17 ++ .../history-item-controls.component.html} | 8 +- .../history-item-controls.component.less} | 9 +- .../history-item-controls.component.spec.ts} | 16 +- .../history-item-controls.component.ts} | 16 +- .../log-context/log-context.component.spec.ts | 4 +- .../menu-button/menu-button.component.html | 8 +- .../menu-button/menu-button.component.spec.ts | 12 +- .../menu-button/menu-button.component.ts | 28 +- .../pagination/pagination.component.html | 3 +- .../components/search-box/search-box.component.ts | 4 +- .../service-logs-table.component.html | 13 +- .../service-logs-table.component.spec.ts | 2 - .../service-logs-table.component.ts | 99 +++++-- .../time-range-picker.component.spec.ts | 2 + .../time-range-picker.component.ts | 2 +- .../timezone-picker.component.spec.ts | 2 - .../timezone-picker/timezone-picker.component.ts | 17 +- .../components/top-menu/top-menu.component.html | 8 +- .../components/top-menu/top-menu.component.spec.ts | 4 + .../app/components/top-menu/top-menu.component.ts | 23 +- .../app/services/component-actions.service.spec.ts | 101 ------- .../src/app/services/component-actions.service.ts | 155 ---------- .../services/component-generator.service.spec.ts | 2 + .../app/services/component-generator.service.ts | 6 + ...ice.spec.ts => history-manager.service.spec.ts} | 94 +++++- .../src/app/services/history-manager.service.ts | 330 +++++++++++++++++++++ .../app/services/logs-container.service.spec.ts | 4 +- .../src/app/services/logs-container.service.ts | 88 +++++- .../ambari-logsearch-web/src/assets/i18n/en.json | 5 + 54 files changed, 944 insertions(+), 601 deletions(-) diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/app.module.ts b/ambari-logsearch/ambari-logsearch-web/src/app/app.module.ts index c6c6922..0a42994 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/app.module.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/app.module.ts @@ -35,7 +35,6 @@ import {ServiceInjector} from '@app/classes/service-injector'; import {mockApiDataService} from '@app/services/mock-api-data.service' import {HttpClientService} from '@app/services/http-client.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; import {UtilsService} from '@app/services/utils.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {ComponentGeneratorService} from '@app/services/component-generator.service'; @@ -57,6 +56,7 @@ 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'; @@ -96,6 +96,7 @@ import {GraphTooltipComponent} from '@app/components/graph-tooltip/graph-tooltip import {GraphLegendItemComponent} from '@app/components/graph-legend-item/graph-legend-item.component'; import {TimeLineGraphComponent} from '@app/components/time-line-graph/time-line-graph.component'; import {ContextMenuComponent} from '@app/components/context-menu/context-menu.component'; +import {HistoryItemControlsComponent} from '@app/components/history-item-controls/history-item-controls.component'; import {TimeZoneAbbrPipe} from '@app/pipes/timezone-abbr.pipe'; import {TimerSecondsPipe} from '@app/pipes/timer-seconds.pipe'; @@ -159,6 +160,7 @@ export function getXHRBackend(injector: Injector, browser: BrowserXhr, xsrf: XSR GraphLegendItemComponent, TimeLineGraphComponent, ContextMenuComponent, + HistoryItemControlsComponent, TimeZoneAbbrPipe, TimerSecondsPipe ], @@ -183,7 +185,6 @@ export function getXHRBackend(injector: Injector, browser: BrowserXhr, xsrf: XSR ], providers: [ HttpClientService, - ComponentActionsService, UtilsService, LogsContainerService, ComponentGeneratorService, @@ -208,10 +209,14 @@ export function getXHRBackend(injector: Injector, browser: BrowserXhr, xsrf: XSR useFactory: getXHRBackend, deps: [Injector, BrowserXhr, XSRFStrategy, ResponseOptions] }, - AuthService + AuthService, + HistoryManagerService ], bootstrap: [AppComponent], - entryComponents: [NodeBarComponent], + entryComponents: [ + NodeBarComponent, + HistoryItemControlsComponent + ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class AppModule { diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.ts index 0cfe69a..b9140cd 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.ts @@ -122,20 +122,6 @@ export class GraphComponent implements AfterViewInit, OnChanges { skipZeroValuesInTooltip: boolean = true; /** - * Indicates whether context menu for X axis ticks is available - * @type {boolean} - */ - @Input() - hasXTickContextMenu: boolean = false; - - /** - * Indicates whether context menu for Y axis ticks is available - * @type {boolean} - */ - @Input() - hasYTickContextMenu: boolean = false; - - /** * Indicates whether X axis event should be emitted with formatted string values that are displayed * (instead of raw values) * @type {boolean} @@ -320,7 +306,7 @@ export class GraphComponent implements AfterViewInit, OnChanges { this.xAxis = axis; this.svg.append('g').attr('class', `axis ${this.xAxisClassName}`).attr('transform', `translate(0,${this.height})`) .call(this.xAxis); - if (this.hasXTickContextMenu) { + if (this.xTickContextMenu.observers.length) { this.svg.selectAll(`.${this.xAxisClassName} .tick`).on('contextmenu', (tickValue: any, index: number): void => { const tick = this.emitFormattedXTick ? this.xAxisTickFormatter(tickValue, index) : tickValue, nativeEvent = d3.event; @@ -342,7 +328,7 @@ export class GraphComponent implements AfterViewInit, OnChanges { } this.yAxis = axis; this.svg.append('g').attr('class', `axis ${this.yAxisClassName}`).call(this.yAxis); - if (this.hasYTickContextMenu) { + if (this.yTickContextMenu.observers.length) { this.svg.selectAll(`.${this.yAxisClassName} .tick`).on('contextmenu', (tickValue: any, index: number): void => { const tick = this.emitFormattedYTick ? this.yAxisTickFormatter(tickValue, index): tickValue, nativeEvent = d3.event; diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/filtering.ts b/ambari-logsearch/ambari-logsearch-web/src/app/classes/filtering.ts index 3348969..bb75786 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/classes/filtering.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/classes/filtering.ts @@ -48,7 +48,7 @@ export interface SortingListItem extends ListItem { export interface FilterCondition { label?: string; options?: (ListItem | TimeUnitListItem[])[]; - defaultSelection?: ListItem | ListItem[] | number; + defaultSelection?: ListItem | ListItem[] | number | boolean; iconClass?: string; fieldName?: string; } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/list-item.ts b/ambari-logsearch/ambari-logsearch-web/src/app/classes/list-item.ts index 1aaaecc..8505373 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/classes/list-item.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/classes/list-item.ts @@ -17,10 +17,11 @@ */ export interface ListItem { - id?: string; + id?: string | number; label?: string; value: any; iconClass?: string; isChecked?: boolean; - action?: string; + onSelect?: Function; + isDivider?: boolean; } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/models/app-state.ts b/ambari-logsearch/ambari-logsearch-web/src/app/classes/models/app-state.ts index c3279ce..b187ca6 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/classes/models/app-state.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/classes/models/app-state.ts @@ -17,8 +17,14 @@ */ import {ActiveServiceLogEntry} from '@app/classes/active-service-log-entry'; +import {ListItem} from '@app/classes/list-item'; import {LogsType} from '@app/classes/string'; +export interface History { + items: ListItem[]; + currentId: number; +} + export interface AppState { isAuthorized: boolean; isInitialLoading: boolean; @@ -28,6 +34,7 @@ export interface AppState { isServiceLogContextView: boolean; activeLog: ActiveServiceLogEntry | null; activeFilters: object; + history: History; } export const initialState: AppState = { @@ -38,5 +45,9 @@ export const initialState: AppState = { isServiceLogsFileView: false, isServiceLogContextView: false, activeLog: null, - activeFilters: null + activeFilters: null, + history: { + items: [], + currentId: -1 + } }; diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html index ab6326a..2e332d4 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html @@ -14,7 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. --> -<menu-button *ngFor="let item of items" label="{{item.label | translate}}" [action]="item.action" - [iconClass]="item.iconClass" [labelClass]="item.labelClass" [subItems]="item.subItems" - [hideCaret]="item.hideCaret" [badge]="item.badge" [isRightAlign]="item.isRightAlign"> -</menu-button> + +<!-- TODO use listClass="history-dropdown" for custom styling --> +<menu-button label="{{'topMenu.undo' | translate}}" [subItems]="undoItems" iconClass="fa fa-arrow-left" + listClass="history-dropdown" (buttonClick)="undoLatest()" (selectItem)="undo($event)"></menu-button> +<menu-button label="{{'topMenu.redo' | translate}}" [subItems]="redoItems" iconClass="fa fa-arrow-right" + listClass="history-dropdown" (buttonClick)="redoLatest()" (selectItem)="redo($event)">></menu-button> +<menu-button label="{{'topMenu.history' | translate}}" [subItems]="historyItems" iconClass="fa fa-history" + listClass="history-dropdown" [isRightAlign]="true" + additionalLabelComponentSetter="getHistoryItemIcons"></menu-button> +<menu-button label="{{'topMenu.refresh' | translate}}" iconClass="fa fa-refresh" + (buttonClick)="refresh()"></menu-button> diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts index 081304e..ba53ee1 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts @@ -19,6 +19,26 @@ import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {TranslationModules} from '@app/test-config.spec'; +import {StoreModule} from '@ngrx/store'; +import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service'; +import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service'; +import {AuditLogsFieldsService, auditLogsFields} from '@app/services/storage/audit-logs-fields.service'; +import {AuditLogsGraphDataService, auditLogsGraphData} from '@app/services/storage/audit-logs-graph-data.service'; +import {ServiceLogsFieldsService, serviceLogsFields} from '@app/services/storage/service-logs-fields.service'; +import { + ServiceLogsHistogramDataService, serviceLogsHistogramData +} from '@app/services/storage/service-logs-histogram-data.service'; +import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; +import {AppStateService, appState} from '@app/services/storage/app-state.service'; +import {ClustersService, clusters} from '@app/services/storage/clusters.service'; +import {ComponentsService, components} from '@app/services/storage/components.service'; +import {HostsService, hosts} from '@app/services/storage/hosts.service'; +import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service'; +import {TabsService, tabs} from '@app/services/storage/tabs.service'; +import {HistoryManagerService} from '@app/services/history-manager.service'; +import {HttpClientService} from '@app/services/http-client.service'; +import {LogsContainerService} from '@app/services/logs-container.service'; +import {UtilsService} from '@app/services/utils.service'; import {ActionMenuComponent} from './action-menu.component'; @@ -27,9 +47,56 @@ describe('ActionMenuComponent', () => { let fixture: ComponentFixture<ActionMenuComponent>; beforeEach(async(() => { + const httpClient = { + get: () => { + return { + subscribe: () => { + } + } + } + }; TestBed.configureTestingModule({ - imports: TranslationModules, + imports: [ + ...TranslationModules, + StoreModule.provideStore({ + auditLogs, + serviceLogs, + auditLogsFields, + auditLogsGraphData, + serviceLogsFields, + serviceLogsHistogramData, + appSettings, + appState, + clusters, + components, + hosts, + serviceLogsTruncated, + tabs + }) + ], declarations: [ActionMenuComponent], + providers: [ + { + provide: HttpClientService, + useValue: httpClient + }, + HistoryManagerService, + LogsContainerService, + UtilsService, + AuditLogsService, + ServiceLogsService, + AuditLogsFieldsService, + AuditLogsGraphDataService, + ServiceLogsFieldsService, + ServiceLogsHistogramDataService, + AppSettingsService, + AppStateService, + ClustersService, + ComponentsService, + HostsService, + ServiceLogsTruncatedService, + TabsService + ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) .compileComponents(); diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts index 72037f8..351268c 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts @@ -17,6 +17,9 @@ */ import {Component} from '@angular/core'; +import {LogsContainerService} from '@app/services/logs-container.service'; +import {HistoryManagerService} from '@app/services/history-manager.service'; +import {ListItem} from '@app/classes/list-item'; @Component({ selector: 'action-menu', @@ -25,77 +28,39 @@ import {Component} from '@angular/core'; }) export class ActionMenuComponent { - //TODO implement loading of real data into subItems - readonly items = [ - { - iconClass: 'fa fa-arrow-left', - label: 'topMenu.undo', - action: 'undo', - subItems: [ - { - label: 'Apply \'Last week\' filter' - }, - { - label: 'Clear all filters' - }, - { - label: 'Apply \'HDFS\' filter' - }, - { - label: 'Apply \'Errors\' filter' - } - ] - }, - { - iconClass: 'fa fa-arrow-right', - label: 'topMenu.redo', - action: 'redo', - subItems: [ - { - label: 'Apply \'Warnings\' filter' - }, - { - label: 'Switch to graph mode' - }, - { - label: 'Apply \'Custom Date\' filter' - } - ] - }, - { - iconClass: 'fa fa-refresh', - label: 'topMenu.refresh', - action: 'refresh' - }, - { - iconClass: 'fa fa-history', - label: 'topMenu.history', - action: 'openHistory', - isRightAlign: true, - subItems: [ - { - label: 'Apply \'Custom Date\' filter' - }, - { - label: 'Switch to graph mode' - }, - { - label: 'Apply \'Warnings\' filter' - }, - { - label: 'Apply \'Last week\' filter' - }, - { - label: 'Clear all filters' - }, - { - label: 'Apply \'HDFS\' filter' - }, - { - label: 'Apply \'Errors\' filter' - } - ] - } - ]; + constructor(private logsContainer: LogsContainerService, private historyManager: HistoryManagerService) { + } + + get undoItems(): ListItem[] { + return this.historyManager.undoItems; + } + + get redoItems(): ListItem[] { + return this.historyManager.redoItems; + } + + get historyItems(): ListItem[] { + return this.historyManager.activeHistory; + } + + undoLatest(): void { + this.historyManager.undo(this.undoItems[0]); + } + + redoLatest(): void { + 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.logsContainer.loadLogs(); + } } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.html index 22deef1..6ebb92e 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.html @@ -25,8 +25,8 @@ svgId="top-users-graph"></horizontal-histogram> </collapsible-panel> <collapsible-panel commonTitle="{{'logs.topResources' | translate: resourcesGraphTitleParams}}" class="col-md-6"> - <horizontal-histogram [data]="topResourcesGraphData" [allowFractionalXTicks]="false" [hasYTickContextMenu]="true" - [emitFormattedYTick]="true" (yTickContextMenu)="showContextMenu($event)" + <horizontal-histogram [data]="topResourcesGraphData" [allowFractionalXTicks]="false" [emitFormattedYTick]="true" + (yTickContextMenu)="showContextMenu($event)" svgId="top-resources-graph"></horizontal-histogram> <context-menu [isDisplayed]="isContextMenuDisplayed" [contextMenuItems]="contextMenuItems" [leftPosition]="contextMenuLeft" [topPosition]="contextMenuTop" (itemSelect)="updateQuery($event)" diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts index f7d0cdc..51aaaa1 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts @@ -37,6 +37,7 @@ import {TabsService, tabs} from '@app/services/storage/tabs.service'; import {TabsComponent} from '@app/components/tabs/tabs.component'; import {LogsContainerService} from '@app/services/logs-container.service'; import {HttpClientService} from '@app/services/http-client.service'; +import {UtilsService} from '@app/services/utils.service'; import {AuditLogsEntriesComponent} from './audit-logs-entries.component'; @@ -82,6 +83,7 @@ describe('AuditLogsEntriesComponent', () => { provide: HttpClientService, useValue: httpClient }, + UtilsService, AuditLogsService, ServiceLogsService, AuditLogsFieldsService, diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.html index cad09bc..f970726 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.html @@ -15,13 +15,12 @@ limitations under the License. --> -<dropdown-button class="pull-right" label="{{'logs.columns' | translate}}" [options]="columns" [isRightAlign]="true" - [isMultipleChoice]="true" action="updateSelectedColumns" - [additionalArgs]="logsTypeMapObject.fieldsModel"></dropdown-button> +<dropdown-button class="pull-right" label="{{'logs.columns' | translate}}" (selectItem)="updateSelectedColumns($event)" + [options]="columns" [isRightAlign]="true" [isMultipleChoice]="true"></dropdown-button> <form *ngIf="logs && logs.length" [formGroup]="filtersForm" class="row pull-right"> <filter-dropdown class="col-md-12" label="{{filters.auditLogsSorting.label | translate}}" formControlName="auditLogsSorting" [options]="filters.auditLogsSorting.options" - [isRightAlign]="true"></filter-dropdown> + [isRightAlign]="true" [showCommonLabelWithSelection]="true"></filter-dropdown> </form> <div class="panel panel-default"> <div class="panel-body"> diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.ts index deca936..fa5b1c5 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.ts @@ -36,9 +36,7 @@ export class AuditLogsTableComponent extends LogsTableComponent { readonly timeFormat: string = 'YYYY-MM-DD HH:mm:ss,SSS'; - get logsTypeMapObject(): object { - return this.logsContainer.logsTypeMap.auditLogs; - } + private readonly logsType: string = 'auditLogs'; get filters(): any { return this.logsContainer.filters; @@ -52,4 +50,8 @@ export class AuditLogsTableComponent extends LogsTableComponent { return this.columns.find((column: ListItem): boolean => column.value === name); } + updateSelectedColumns(columns: string[]): void { + this.logsContainer.updateSelectedColumns(columns, this.logsType); + } + } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/context-menu/context-menu.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/context-menu/context-menu.component.spec.ts index 1c5154b..1881f57 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/context-menu/context-menu.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/context-menu/context-menu.component.spec.ts @@ -39,8 +39,8 @@ import {TabsService, tabs} from '@app/services/storage/tabs.service'; import {ComponentGeneratorService} from '@app/services/component-generator.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {HttpClientService} from '@app/services/http-client.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; import {AuthService} from '@app/services/auth.service'; +import {UtilsService} from '@app/services/utils.service'; import {DropdownListComponent} from '@app/components/dropdown-list/dropdown-list.component'; import {ContextMenuComponent} from './context-menu.component'; @@ -90,7 +90,6 @@ describe('ContextMenuComponent', () => { provide: HttpClientService, useValue: httpClient }, - ComponentActionsService, HostsService, AuditLogsService, ServiceLogsService, @@ -104,7 +103,8 @@ describe('ContextMenuComponent', () => { ComponentsService, ServiceLogsTruncatedService, TabsService, - AuthService + AuthService, + UtilsService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.html index d047d7a..396a277 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.html @@ -16,16 +16,20 @@ --> <div [ngClass]="{'dropup': isDropup}"> - <button [ngClass]="['btn', 'btn-link', 'dropdown-toggle', buttonClass]" data-toggle="dropdown"> - <span *ngIf="iconClass || label" - [ngClass]="{'filter-label': true, 'plain': !isMultipleChoice && !hideCaret && showSelectedValue}"> - <span *ngIf="iconClass" [ngClass]="iconClass"></span> - <span *ngIf="label">{{label}}</span> + <button [ngClass]="['btn', 'dropdown-toggle', buttonClass]" data-toggle="dropdown"> + <span class="filter-label"> + <span *ngIf="iconClass || label" [class.plain]="!isMultipleChoice && !hideCaret && showSelectedValue"> + <span *ngIf="iconClass" [ngClass]="iconClass"></span> + <span *ngIf="label && (!selection.length || isMultipleChoice || showCommonLabelWithSelection)" + [class.label-before-selection]="isSelectionDisplayable"> + {{label}} + </span> + </span> + <span *ngIf="isSelectionDisplayable">{{selection[0].label | translate}}</span> + <span *ngIf="!hideCaret" class="caret"></span> </span> - <span *ngIf="showSelectedValue && !isMultipleChoice && selection.length">{{selection[0].label | translate}}</span> - <span *ngIf="!hideCaret" class="caret"></span> </button> <ul data-component="dropdown-list" [ngClass]="{'dropdown-menu': true, 'dropdown-menu-right': isRightAlign}" [items]="options" [isMultipleChoice]="isMultipleChoice" (selectedItemChange)="updateSelection($event)" - [actionArguments]="additionalArgs"></ul> + [actionArguments]="listItemArguments"></ul> </div> diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.less b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.less index 7b560c1..06861722 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.less +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.less @@ -25,11 +25,13 @@ text-transform: none; .filter-label { - padding: @input-group-addon-padding; - - &.plain { + .plain { color: initial; } + + .label-before-selection { + padding: @input-group-addon-padding; + } } } } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.spec.ts index b9f9540..8a72c38 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.spec.ts @@ -16,11 +16,10 @@ * limitations under the License. */ -import {NO_ERRORS_SCHEMA, Injector} from '@angular/core'; -import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {TranslationModules} from '@app/test-config.spec'; import {StoreModule} from '@ngrx/store'; -import {ServiceInjector} from '@app/classes/service-injector'; import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; import {ClustersService, clusters} from '@app/services/storage/clusters.service'; import {ComponentsService, components} from '@app/services/storage/components.service'; @@ -37,7 +36,6 @@ import { import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service'; import {TabsService, tabs} from '@app/services/storage/tabs.service'; import {UtilsService} from '@app/services/utils.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; import {HttpClientService} from '@app/services/http-client.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {AuthService} from '@app/services/auth.service'; @@ -92,7 +90,6 @@ describe('DropdownButtonComponent', () => { ServiceLogsTruncatedService, TabsService, UtilsService, - ComponentActionsService, { provide: HttpClientService, useValue: httpClient @@ -105,12 +102,11 @@ describe('DropdownButtonComponent', () => { .compileComponents(); })); - beforeEach(inject([Injector], (injector: Injector) => { - ServiceInjector.injector = injector; + beforeEach(() => { fixture = TestBed.createComponent(DropdownButtonComponent); component = fixture.componentInstance; fixture.detectChanges(); - })); + }); it('should create component', () => { expect(component).toBeTruthy(); diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.ts index ead9e1a..b642dd5 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.ts @@ -16,10 +16,8 @@ * limitations under the License. */ -import {Component, Input} from '@angular/core'; +import {Component, Input, Output, EventEmitter} from '@angular/core'; import {ListItem} from '@app/classes/list-item'; -import {ServiceInjector} from '@app/classes/service-injector'; -import {ComponentActionsService} from '@app/services/component-actions.service'; import {UtilsService} from '@app/services/utils.service'; @Component({ @@ -30,14 +28,13 @@ import {UtilsService} from '@app/services/utils.service'; export class DropdownButtonComponent { constructor(protected utils: UtilsService) { - this.actions = ServiceInjector.injector.get(ComponentActionsService); } - + @Input() label?: string; @Input() - buttonClass: string = ''; + buttonClass: string = 'btn-link'; @Input() iconClass?: string; @@ -52,10 +49,7 @@ export class DropdownButtonComponent { options: ListItem[] = []; @Input() - action?: string; - - @Input() - additionalArgs: any[] = []; + listItemArguments: any[] = []; @Input() isMultipleChoice: boolean = false; @@ -66,7 +60,11 @@ export class DropdownButtonComponent { @Input() isDropup: boolean = false; - private actions: ComponentActionsService; + @Input() + showCommonLabelWithSelection: boolean = false; + + @Output() + selectItem: EventEmitter<any> = new EventEmitter(); protected selectedItems?: ListItem[] = []; @@ -78,22 +76,28 @@ export class DropdownButtonComponent { this.selectedItems = items; } + // TODO handle case of selections with multiple items + /** + * Indicates whether selection can be displayed at the moment, i.e. it's not empty, not multiple + * and set to be displayed by showSelectedValue flag + * @returns {boolean} + */ + get isSelectionDisplayable():boolean { + return this.showSelectedValue && !this.isMultipleChoice && this.selection.length > 0; + } + updateSelection(item: ListItem): void { - const action = this.action && this.actions[this.action]; + const hasAction = this.selectItem.observers.length; if (this.isMultipleChoice) { this.options.find((option: ListItem): boolean => { return this.utils.isEqual(option.value, item.value); }).isChecked = item.isChecked; const checkedItems = this.options.filter((option: ListItem): boolean => option.isChecked); this.selection = checkedItems; - if (action) { - action(checkedItems.map((option: ListItem): any => option.value), ...this.additionalArgs); - } + this.selectItem.emit(checkedItems.map((option: ListItem): any => option.value)); } else if (!this.utils.isEqual(this.selection[0], item)) { this.selection = [item]; - if (action) { - action(item.value, ...this.additionalArgs); - } + this.selectItem.emit(item.value); } } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.html index 64e4b8e..9861c24 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.html @@ -15,19 +15,21 @@ limitations under the License. --> -<li *ngFor="let item of items"> - <label class="list-item-label" *ngIf="isMultipleChoice"> - <input type="checkbox" [attr.id]="item.id || item.value" [(ngModel)]="item.isChecked" - (change)="changeSelectedItem({value: item.value, isChecked: $event.currentTarget.checked})"> - <label [attr.for]="item.id || item.value" class="label-container"> +<li *ngFor="let item of items" [class.divider]="item.isDivider" [attr.role]="item.isDivider ? 'separator' : null"> + <ng-container *ngIf="!item.isDivider"> + <label class="list-item-label" *ngIf="isMultipleChoice"> + <input type="checkbox" [attr.id]="item.id || item.value" [(ngModel)]="item.isChecked" + (change)="changeSelectedItem({value: item.value, isChecked: $event.currentTarget.checked})"> + <label [attr.for]="item.id || item.value" class="label-container"> + <span *ngIf="item.iconClass" [ngClass]="item.iconClass"></span> + {{item.label | translate}} + <span #additionalComponent></span> + </label> + </label> + <span class="list-item-label label-container" *ngIf="!isMultipleChoice" (click)="changeSelectedItem(item)"> <span *ngIf="item.iconClass" [ngClass]="item.iconClass"></span> {{item.label | translate}} - <div #additionalComponent></div> - </label> - </label> - <span class="list-item-label label-container" *ngIf="!isMultipleChoice" (click)="changeSelectedItem(item)"> - <span *ngIf="item.iconClass" [ngClass]="item.iconClass"></span> - {{item.label | translate}} - <div #additionalComponent></div> - </span> + <span #additionalComponent></span> + </span> + </ng-container> </li> diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.spec.ts index dd602d7..6d88010 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.spec.ts @@ -38,8 +38,8 @@ import {TabsService, tabs} from '@app/services/storage/tabs.service'; import {ComponentGeneratorService} from '@app/services/component-generator.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {HttpClientService} from '@app/services/http-client.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; import {AuthService} from '@app/services/auth.service'; +import {UtilsService} from '@app/services/utils.service'; import {DropdownListComponent} from './dropdown-list.component'; @@ -84,7 +84,6 @@ describe('DropdownListComponent', () => { provide: HttpClientService, useValue: httpClient }, - ComponentActionsService, HostsService, AuditLogsService, ServiceLogsService, @@ -98,7 +97,8 @@ describe('DropdownListComponent', () => { ComponentsService, ServiceLogsTruncatedService, TabsService, - AuthService + AuthService, + UtilsService ] }) .compileComponents(); diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.ts index ef185d0..5d0ad4a 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-list/dropdown-list.component.ts @@ -16,33 +16,37 @@ * limitations under the License. */ -import {Component, AfterViewInit, Input, Output, EventEmitter, ViewChildren, ViewContainerRef, QueryList} from '@angular/core'; +import { + Component, OnChanges, AfterViewChecked, SimpleChanges, Input, Output, EventEmitter, ViewChildren, ViewContainerRef, + QueryList +} from '@angular/core'; import {ListItem} from '@app/classes/list-item'; import {ComponentGeneratorService} from '@app/services/component-generator.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; @Component({ selector: 'ul[data-component="dropdown-list"]', templateUrl: './dropdown-list.component.html', styleUrls: ['./dropdown-list.component.less'] }) -export class DropdownListComponent implements AfterViewInit { +export class DropdownListComponent implements OnChanges, AfterViewChecked { - constructor(private componentGenerator: ComponentGeneratorService, private actions: ComponentActionsService) { + constructor(private componentGenerator: ComponentGeneratorService) { } - ngAfterViewInit() { - const setter = this.additionalLabelComponentSetter; - if (setter) { - this.containers.forEach((container, index) => this.componentGenerator[setter](this.items[index].value, container)); + private shouldRenderAdditionalComponents: boolean = false; + + ngOnChanges(changes: SimpleChanges): void { + if (changes.hasOwnProperty('items')) { + this.shouldRenderAdditionalComponents = true; } } - @Input() - items: ListItem[]; + ngAfterViewChecked() { + this.renderAdditionalComponents(); + } @Input() - defaultAction: Function; + items: ListItem[]; @Input() isMultipleChoice?: boolean = false; @@ -61,9 +65,18 @@ export class DropdownListComponent implements AfterViewInit { }) containers: QueryList<ViewContainerRef>; + private renderAdditionalComponents(): void { + const setter = this.additionalLabelComponentSetter, + containers = this.containers; + if (this.shouldRenderAdditionalComponents && setter && containers) { + containers.forEach((container, index) => this.componentGenerator[setter](this.items[index].value, container)); + this.shouldRenderAdditionalComponents = false; + } + } + changeSelectedItem(options: ListItem): void { - if (options.action) { - this.actions[options.action](...this.actionArguments); + if (options.onSelect) { + options.onSelect(...this.actionArguments); } this.selectedItemChange.emit(options); } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-button/filter-button.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-button/filter-button.component.spec.ts index 4c93cbe..87c490c 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-button/filter-button.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-button/filter-button.component.spec.ts @@ -16,11 +16,10 @@ * limitations under the License. */ -import {NO_ERRORS_SCHEMA, Injector} from '@angular/core'; -import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {TranslationModules} from '@app/test-config.spec'; import {StoreModule} from '@ngrx/store'; -import {ServiceInjector} from '@app/classes/service-injector'; import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; import {ClustersService, clusters} from '@app/services/storage/clusters.service'; import {ComponentsService, components} from '@app/services/storage/components.service'; @@ -36,7 +35,6 @@ import { } from '@app/services/storage/service-logs-histogram-data.service'; import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service'; import {TabsService, tabs} from '@app/services/storage/tabs.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; import {UtilsService} from '@app/services/utils.service'; import {HttpClientService} from '@app/services/http-client.service'; import {LogsContainerService} from '@app/services/logs-container.service'; @@ -91,7 +89,6 @@ describe('FilterButtonComponent', () => { ServiceLogsHistogramDataService, ServiceLogsTruncatedService, TabsService, - ComponentActionsService, UtilsService, { provide: HttpClientService, @@ -105,12 +102,11 @@ describe('FilterButtonComponent', () => { .compileComponents(); })); - beforeEach(inject([Injector], (injector: Injector) => { - ServiceInjector.injector = injector; + beforeEach(() => { fixture = TestBed.createComponent(FilterButtonComponent); component = fixture.componentInstance; fixture.detectChanges(); - })); + }); it('should create component', () => { expect(component).toBeTruthy(); diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-dropdown/filter-dropdown.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-dropdown/filter-dropdown.component.spec.ts index 61d30d1..e3105c1 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-dropdown/filter-dropdown.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-dropdown/filter-dropdown.component.spec.ts @@ -15,11 +15,10 @@ * limitations under the License. */ -import {NO_ERRORS_SCHEMA, Injector} from '@angular/core'; -import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {TranslationModules} from '@app/test-config.spec'; import {StoreModule} from '@ngrx/store'; -import {ServiceInjector} from '@app/classes/service-injector'; import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; import {AppStateService, appState} from '@app/services/storage/app-state.service'; import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service'; @@ -36,7 +35,6 @@ import {ClustersService, clusters} from '@app/services/storage/clusters.service' import {ComponentsService, components} from '@app/services/storage/components.service'; import {HostsService, hosts} from '@app/services/storage/hosts.service'; import {UtilsService} from '@app/services/utils.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {HttpClientService} from '@app/services/http-client.service'; import {AuthService} from '@app/services/auth.service'; @@ -111,7 +109,6 @@ describe('FilterDropdownComponent', () => { useValue: filtering }, UtilsService, - ComponentActionsService, LogsContainerService, { provide: HttpClientService, @@ -124,12 +121,11 @@ describe('FilterDropdownComponent', () => { .compileComponents(); })); - beforeEach(inject([Injector], (injector: Injector) => { - ServiceInjector.injector = injector; + beforeEach(() => { fixture = TestBed.createComponent(FilterDropdownComponent); component = fixture.componentInstance; fixture.detectChanges(); - })); + }); it('should create component', () => { expect(component).toBeTruthy(); diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-dropdown/filter-dropdown.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-dropdown/filter-dropdown.component.ts index 665386b..b85c572 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-dropdown/filter-dropdown.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/filter-dropdown/filter-dropdown.component.ts @@ -17,7 +17,6 @@ import {Component, forwardRef} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {UtilsService} from '@app/services/utils.service'; import {DropdownButtonComponent} from '@app/components/dropdown-button/dropdown-button.component'; import {ListItem} from '@app/classes/list-item'; @@ -35,10 +34,6 @@ import {ListItem} from '@app/classes/list-item'; }) export class FilterDropdownComponent extends DropdownButtonComponent implements ControlValueAccessor { - constructor(protected utils: UtilsService) { - super(utils); - } - private onChange: (fn: any) => void; get selection(): ListItem[] { diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.html index f0cf3f4..51233ff 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.html @@ -29,9 +29,9 @@ </button> </div> <div class="filter-buttons col-md-4"> - <dropdown-button [options]="searchBoxItems | async" iconClass="fa fa-search-minus" action="proceedWithExclude" - label="{{'filter.exclude' | translate}}" [hideCaret]="true" - [showSelectedValue]="false"></dropdown-button> + <dropdown-button iconClass="fa fa-search-minus" label="{{'filter.exclude' | translate}}" [hideCaret]="true" + [showSelectedValue]="false" [showCommonLabelWithSelection]="true" + [options]="searchBoxItems | async" (selectItem)="proceedWithExclude($event)"></dropdown-button> <filter-button *ngIf="isFilterConditionDisplayed('hosts')" formControlName="hosts" label="{{filters.hosts.label | translate}}" [iconClass]="filters.hosts.iconClass" [subItems]="filters.hosts.options" [isMultipleChoice]="true" [isRightAlign]="true" @@ -44,8 +44,8 @@ label="{{filters.levels.label | translate}}" [iconClass]="filters.levels.iconClass" [subItems]="filters.levels.options" [isMultipleChoice]="true" [isRightAlign]="true"></filter-button> <menu-button *ngIf="!captureSeconds" label="{{'filter.capture' | translate}}" iconClass="fa fa-caret-right" - action="startCapture"></menu-button> + (buttonClick)="startCapture()"></menu-button> <menu-button *ngIf="captureSeconds" label="{{captureSeconds | timerSeconds}}" iconClass="fa fa-stop stop-icon" - action="stopCapture"></menu-button> + (buttonClick)="stopCapture()"></menu-button> </div> </form> diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts index cd372ec..fe60671 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts @@ -109,4 +109,21 @@ export class FiltersPanelComponent implements OnChanges { this.searchBoxValueUpdate.next(); } + startCapture(): void { + this.logsContainer.startCaptureTimer(); + } + + stopCapture(): void { + this.logsContainer.stopCaptureTimer(); + } + + proceedWithExclude(item: string): void { + this.queryParameterNameChange.next({ + item: { + value: item + }, + isExclude: true + }); + } + } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.html similarity index 71% copy from ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html copy to ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.html index ab6326a..e6978f1 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.html @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> -<menu-button *ngFor="let item of items" label="{{item.label | translate}}" [action]="item.action" - [iconClass]="item.iconClass" [labelClass]="item.labelClass" [subItems]="item.subItems" - [hideCaret]="item.hideCaret" [badge]="item.badge" [isRightAlign]="item.isRightAlign"> -</menu-button> + +<!-- TODO implement View details and Save filter actions --> +<span class="fa fa-info-circle"></span> +<span class="fa fa-floppy-o"></span> diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/list-item.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.less similarity index 85% copy from ambari-logsearch/ambari-logsearch-web/src/app/classes/list-item.ts copy to ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.less index 1aaaecc..dfb9997 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/classes/list-item.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.less @@ -16,11 +16,6 @@ * limitations under the License. */ -export interface ListItem { - id?: string; - label?: string; - value: any; - iconClass?: string; - isChecked?: boolean; - action?: string; +:host { + float: right; } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.spec.ts similarity index 70% copy from ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts copy to ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.spec.ts index 081304e..4dbaa2d 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.spec.ts @@ -16,27 +16,23 @@ * limitations under the License. */ -import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {TranslationModules} from '@app/test-config.spec'; -import {ActionMenuComponent} from './action-menu.component'; +import {HistoryItemControlsComponent} from './history-item-controls.component'; -describe('ActionMenuComponent', () => { - let component: ActionMenuComponent; - let fixture: ComponentFixture<ActionMenuComponent>; +describe('HistoryItemControlsComponent', () => { + let component: HistoryItemControlsComponent; + let fixture: ComponentFixture<HistoryItemControlsComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: TranslationModules, - declarations: [ActionMenuComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + declarations: [HistoryItemControlsComponent] }) .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ActionMenuComponent); + fixture = TestBed.createComponent(HistoryItemControlsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/list-item.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.ts similarity index 72% copy from ambari-logsearch/ambari-logsearch-web/src/app/classes/list-item.ts copy to ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.ts index 1aaaecc..1975d9a 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/classes/list-item.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/history-item-controls/history-item-controls.component.ts @@ -16,11 +16,13 @@ * limitations under the License. */ -export interface ListItem { - id?: string; - label?: string; - value: any; - iconClass?: string; - isChecked?: boolean; - action?: string; +import {Component} from '@angular/core'; + +@Component({ + selector: 'history-item-controls', + templateUrl: './history-item-controls.component.html', + styleUrls: ['./history-item-controls.component.less'] +}) +export class HistoryItemControlsComponent { + // TODO implement View details and Save filter actions } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/log-context/log-context.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/log-context/log-context.component.spec.ts index c346c9a..6e6a70e 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/log-context/log-context.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/log-context/log-context.component.spec.ts @@ -37,6 +37,7 @@ import {TranslationModules} from '@app/test-config.spec'; import {ModalComponent} from '@app/components/modal/modal.component'; import {LogsContainerService} from '@app/services/logs-container.service'; import {HttpClientService} from '@app/services/http-client.service'; +import {UtilsService} from '@app/services/utils.service'; import {LogContextComponent} from './log-context.component'; @@ -94,7 +95,8 @@ describe('LogContextComponent', () => { { provide: HttpClientService, useValue: httpClient - } + }, + UtilsService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html index 5e2b15f..8e67b5d 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html @@ -23,7 +23,9 @@ <i *ngIf="hasCaret" [ngClass]="['fa ', caretClass ]"></i> <span *ngIf="label" class="menu-button-label">{{label}}</span> </a> - <ul data-component="dropdown-list" *ngIf="hasSubItems" [items]="subItems" (selectedItemChange)="onDropdownItemChange($event)" - [isMultipleChoice]="isMultipleChoice" [additionalLabelComponentSetter]="additionalLabelComponentSetter" - [ngClass]="{'dropdown-menu': true, 'dropdown-menu-right': isRightAlign}"></ul> + <ul data-component="dropdown-list" *ngIf="hasSubItems" [items]="subItems" + (selectedItemChange)="onDropdownItemChange($event)" [isMultipleChoice]="isMultipleChoice" + [additionalLabelComponentSetter]="additionalLabelComponentSetter" + [ngClass]="'dropdown-menu' + (isRightAlign ? ' dropdown-menu-right' : '') + (listClass ? ' ' + listClass : '')" + ></ul> </div> diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.spec.ts index 3b210a3..4e77db5 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.spec.ts @@ -16,10 +16,9 @@ * limitations under the License. */ -import {NO_ERRORS_SCHEMA, Injector} from '@angular/core'; -import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {TranslationModules} from '@app/test-config.spec'; -import {ServiceInjector} from '@app/classes/service-injector'; import {StoreModule} from '@ngrx/store'; import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; import {AppStateService, appState} from '@app/services/storage/app-state.service'; @@ -36,7 +35,6 @@ import { } from '@app/services/storage/service-logs-histogram-data.service'; import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service'; import {TabsService, tabs} from '@app/services/storage/tabs.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; import {HttpClientService} from '@app/services/http-client.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {AuthService} from '@app/services/auth.service'; @@ -90,7 +88,6 @@ describe('MenuButtonComponent', () => { ServiceLogsHistogramDataService, ServiceLogsTruncatedService, TabsService, - ComponentActionsService, { provide: HttpClientService, useValue: httpClient @@ -103,12 +100,11 @@ describe('MenuButtonComponent', () => { .compileComponents(); })); - beforeEach(inject([Injector], (injector: Injector) => { - ServiceInjector.injector = injector; + beforeEach(() => { fixture = TestBed.createComponent(MenuButtonComponent); component = fixture.componentInstance; fixture.detectChanges(); - })); + }); it('should create component', () => { expect(component).toBeTruthy(); diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts index 12da4ac..432b561 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts @@ -16,10 +16,8 @@ * limitations under the License. */ -import {Component, Input, ViewChild, ElementRef} from '@angular/core'; +import {Component, Input, Output, ViewChild, ElementRef, EventEmitter} from '@angular/core'; import {ListItem} from '@app/classes/list-item'; -import {ServiceInjector} from '@app/classes/service-injector'; -import {ComponentActionsService} from '@app/services/component-actions.service'; @Component({ selector: 'menu-button', @@ -28,10 +26,6 @@ import {ComponentActionsService} from '@app/services/component-actions.service'; }) export class MenuButtonComponent { - constructor() { - this.actions = ServiceInjector.injector.get(ComponentActionsService); - } - @ViewChild('dropdown') dropdown: ElementRef; @@ -39,9 +33,6 @@ export class MenuButtonComponent { label?: string; @Input() - action: string; - - @Input() iconClass: string; @Input() @@ -84,7 +75,14 @@ export class MenuButtonComponent { @Input() maxLongClickDelay: number = 0; - private actions: ComponentActionsService; + @Input() + listClass: string = ''; + + @Output() + buttonClick: EventEmitter<void> = new EventEmitter(); + + @Output() + selectItem: EventEmitter<ListItem> = new EventEmitter(); /** * This is a private property to indicate the mousedown timestamp, so that we can check it when teh click event @@ -122,14 +120,14 @@ export class MenuButtonComponent { !this.maxLongClickDelay || mdt + this.maxLongClickDelay >= now ); let openDropdown = this.hasSubItems && ( - el.classList.contains(this.caretClass) || isLongClick || !this.actions[this.action] + el.classList.contains(this.caretClass) || isLongClick || !this.buttonClick.observers.length ); if (openDropdown && this.dropdown) { if (this.toggleDropdown()) { this.listenToClickOut(); } - } else if (this.action) { - this.actions[this.action](); + } else if (this.buttonClick.observers.length) { + this.buttonClick.emit(); } this.mouseDownTimestamp = 0; event.preventDefault(); @@ -211,7 +209,7 @@ export class MenuButtonComponent { } updateSelection(options: ListItem) { - // TODO implement value change behaviour + this.selectItem.emit(options); } } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/pagination/pagination.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/pagination/pagination.component.html index 02ed84b..ecfa75d 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/pagination/pagination.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/pagination/pagination.component.html @@ -17,7 +17,8 @@ <form class="pagination-form" [formGroup]="filtersForm"> <filter-dropdown label="{{filterInstance.label | translate}}" formControlName="pageSize" - [options]="filterInstance.options" [isRightAlign]="true" [isDropup]="true"></filter-dropdown> + [options]="filterInstance.options" [isRightAlign]="true" [isDropup]="true" + [showCommonLabelWithSelection]="true"></filter-dropdown> <span>{{'pagination.numbers' | translate: numbersTranslateParams}}</span> <pagination-controls formControlName="page" [totalCount]="totalCount" [pagesCount]="pagesCount" (currentPageChange)="setCurrentPage($event)"></pagination-controls> diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts index 9dd8e26..ade57e5 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts @@ -278,7 +278,7 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess updateValue = (): void => { this.currentValue = ''; if (this.onChange) { - this.onChange(this.parameters); + this.onChange(this.parameters.slice()); } }; @@ -299,7 +299,7 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess } writeValue(parameters: SearchBoxParameterProcessed[] = []): void { - this.parameters = parameters; + this.parameters = parameters.slice(); this.updateValueSubject.next(); } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html index 3cd829e..3110d9a 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html @@ -23,12 +23,13 @@ {{'logs.brokenListLayoutMessage' | translate}} </div> <form *ngIf="logs && logs.length" [formGroup]="filtersForm"> - <filter-dropdown label="{{filters.serviceLogsSorting.label | translate}}" - formControlName="serviceLogsSorting" [options]="filters.serviceLogsSorting.options" [isRightAlign]="true"> - </filter-dropdown> + <filter-dropdown formControlName="serviceLogsSorting" label="{{filters.serviceLogsSorting.label | translate}}" + [showCommonLabelWithSelection]="true" [options]="filters.serviceLogsSorting.options" + [isRightAlign]="true"></filter-dropdown> </form> <dropdown-button label="{{'logs.columns' | translate}}" [options]="columns" [isRightAlign]="true" - [isMultipleChoice]="true" action="updateSelectedColumns" [additionalArgs]="logsTypeMapObject.fieldsModel"> + [isMultipleChoice]="true" (selectItem)="updateSelectedColumns($event)" + [listItemArguments]="logsTypeMapObject.fieldsModel"> </dropdown-button> <div class="layout-btn-group"> <a *ngIf="layout==='FLEX'" class="btn" (click)="toggleShowLabels()" tooltip="{{'logs.toggleLabels' | translate}}"> @@ -67,7 +68,7 @@ <tr class="log-item-row"> <td class="log-action"> <dropdown-button iconClass="fa fa-ellipsis-v action" [hideCaret]="true" [options]="logActions" - [additionalArgs]="[log]" [showSelectedValue]="false"></dropdown-button> + [listItemArguments]="[log]" [showSelectedValue]="false"></dropdown-button> </td> <td *ngIf="isColumnDisplayed('logtime')" class="log-time"> <time> @@ -115,7 +116,7 @@ <div class="log-header"> <div class="log-action"> <dropdown-button iconClass="fa fa-ellipsis-v action" [hideCaret]="true" [options]="logActions" - [additionalArgs]="[log]" [showSelectedValue]="false"></dropdown-button> + [listItemArguments]="[log]" [showSelectedValue]="false"></dropdown-button> </div> <div *ngIf="isColumnDisplayed('level')" [ngClass]="'log-level ' + log.level.toLowerCase()"> <log-level [logEntry]="log"></log-level> diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts index e883c99..e2afbc8 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts @@ -43,7 +43,6 @@ import {LogsContainerService} from '@app/services/logs-container.service'; import {UtilsService} from '@app/services/utils.service'; import {HttpClientService} from '@app/services/http-client.service'; import {ComponentGeneratorService} from '@app/services/component-generator.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; import {AuthService} from '@app/services/auth.service'; import {PaginationComponent} from '@app/components/pagination/pagination.component'; import {DropdownListComponent} from '@app/components/dropdown-list/dropdown-list.component'; @@ -113,7 +112,6 @@ describe('ServiceLogsTableComponent', () => { ComponentsService, HostsService, ComponentGeneratorService, - ComponentActionsService, AuthService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts index 681149d..ee19e1c 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts @@ -16,10 +16,11 @@ * limitations under the License. */ -import {Component, AfterViewInit, AfterViewChecked, ViewChild, ElementRef, Input, ChangeDetectorRef} from '@angular/core'; +import {Component, AfterViewChecked, ViewChild, ElementRef, Input, ChangeDetectorRef} from '@angular/core'; import {ListItem} from '@app/classes/list-item'; import {LogsTableComponent} from '@app/classes/components/logs-table/logs-table-component'; +import {ServiceLog} from '@app/classes/models/service-log'; import {LogsContainerService} from '@app/services/logs-container.service'; import {UtilsService} from '@app/services/utils.service'; @@ -100,53 +101,71 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft readonly timeFormat: string = 'h:mm:ss A'; + private copyLog = (log: ServiceLog): void => { + if (document.queryCommandSupported('copy')) { + const text = log.log_message, + node = document.createElement('textarea'); + node.value = text; + Object.assign(node.style, { + position: 'fixed', + top: '0', + left: '0', + width: '1px', + height: '1px', + border: 'none', + outline: 'none', + boxShadow: 'none', + backgroundColor: 'transparent', + padding: '0' + }); + document.body.appendChild(node); + node.select(); + if (document.queryCommandEnabled('copy')) { + document.execCommand('copy'); + } else { + // TODO open failed alert + } + // TODO success alert + document.body.removeChild(node); + } else { + // TODO failed alert + } + }; + + private openLog = (log: ServiceLog): void => { + this.logsContainer.openServiceLog(log); + }; + + private openContext = (log: ServiceLog): void => { + this.logsContainer.loadLogContext(log.id, log.host, log.type); + }; + readonly logActions = [ { label: 'logs.copy', iconClass: 'fa fa-files-o', - action: 'copyLog' + onSelect: this.copyLog }, { label: 'logs.open', iconClass: 'fa fa-external-link', - action: 'openLog' + onSelect: this.openLog }, { label: 'logs.context', iconClass: 'fa fa-crosshairs', - action: 'openContext' + onSelect: this.openContext } ]; readonly customStyledColumns: string[] = ['level', 'type', 'logtime', 'log_message', 'path']; - get contextMenuItems(): ListItem[] { - return this.logsContainer.queryContextMenuItems; - } - private readonly messageFilterParameterName: string = 'log_message'; - /** - * The goal is to show or hide the context menu on right click. - * @type {boolean} - */ - private isContextMenuDisplayed: boolean = false; - - /** - * 'left' CSS property value for context menu dropdown - * @type {number} - */ - private contextMenuLeft: number = 0; - - /** - * 'top' CSS property value for context menu dropdown - * @type {number} - */ - private contextMenuTop:number = 0; + private readonly logsType: string = 'serviceLogs'; private selectedText: string = ''; - /** * This is a private flag to store the table layout check result. It is used to show user notifications about * non-visible information. @@ -154,6 +173,10 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft */ private tooManyColumnsSelected: boolean = false; + get contextMenuItems(): ListItem[] { + return this.logsContainer.queryContextMenuItems; + } + get timeZone(): string { return this.logsContainer.timeZone; } @@ -166,6 +189,22 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft return this.logsContainer.logsTypeMap.serviceLogs; } + get isContextMenuDisplayed(): boolean { + return Boolean(this.selectedText); + }; + + /** + * 'left' CSS property value for context menu dropdown + * @type {number} + */ + contextMenuLeft: number = 0; + + /** + * 'top' CSS property value for context menu dropdown + * @type {number} + */ + contextMenuTop: number = 0; + isDifferentDates(dateA, dateB): boolean { return this.utils.isDifferentDates(dateA, dateB, this.timeZone); } @@ -173,7 +212,6 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft openMessageContextMenu(event: MouseEvent): void { const selectedText = getSelection().toString(); if (selectedText) { - this.isContextMenuDisplayed = true; this.contextMenuLeft = event.clientX; this.contextMenuTop = event.clientY; this.selectedText = selectedText; @@ -192,8 +230,7 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft /** * Handle the event when the contextual menu component hide itself. */ - private onContextMenuDismiss = (): void => { - this.isContextMenuDisplayed = false; + onContextMenuDismiss(): void { this.selectedText = ''; }; @@ -264,4 +301,8 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft this.showLabels = !this.showLabels; } + updateSelectedColumns(columns: string[]): void { + this.logsContainer.updateSelectedColumns(columns, this.logsType); + } + } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.spec.ts index e3034b0..b41760f 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.spec.ts @@ -37,6 +37,7 @@ import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/s import {TabsService, tabs} from '@app/services/storage/tabs.service'; import {HttpClientService} from '@app/services/http-client.service'; import {LogsContainerService} from '@app/services/logs-container.service'; +import {UtilsService} from '@app/services/utils.service'; import {TimeRangePickerComponent} from './time-range-picker.component'; @@ -79,6 +80,7 @@ describe('TimeRangePickerComponent', () => { useValue: httpClient }, LogsContainerService, + UtilsService, AppSettingsService, AppStateService, ClustersService, diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.ts index 74a2b2d..6c71a26 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.ts @@ -79,7 +79,7 @@ export class TimeRangePickerComponent implements ControlValueAccessor { setCustomTimeRange(): void { this.selection = { - label: 'filter.timeRange.custom', + label: this.logsContainer.customTimeRangeKey, value: { type: 'CUSTOM', start: this.startTime, diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.spec.ts index 1772ec0..3d79b46 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.spec.ts @@ -34,7 +34,6 @@ import { } from '@app/services/storage/service-logs-histogram-data.service'; import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service'; import {TabsService, tabs} from '@app/services/storage/tabs.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; import {HttpClientService} from '@app/services/http-client.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {AuthService} from '@app/services/auth.service'; @@ -94,7 +93,6 @@ describe('TimeZonePickerComponent', () => { ServiceLogsHistogramDataService, ServiceLogsTruncatedService, TabsService, - ComponentActionsService, { provide: HttpClientService, useValue: httpClient diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.ts index 32f6474..98758ab 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/timezone-picker/timezone-picker.component.ts @@ -16,21 +16,23 @@ * limitations under the License. */ -import {Component} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import * as $ from 'jquery'; import '@vendor/js/WorldMapGenerator.min'; import {AppSettingsService} from '@app/services/storage/app-settings.service'; -import {ComponentActionsService} from '@app/services/component-actions.service'; @Component({ selector: 'timezone-picker', templateUrl: './timezone-picker.component.html', styleUrls: ['./timezone-picker.component.less'] }) -export class TimeZonePickerComponent { +export class TimeZonePickerComponent implements OnInit { - constructor(private appSettings: AppSettingsService, private actions: ComponentActionsService) { - appSettings.getParameter('timeZone').subscribe(value => this.timeZone = value); + constructor(private appSettings: AppSettingsService) { + } + + ngOnInit() { + this.appSettings.getParameter('timeZone').subscribe((value: string) => this.timeZone = value); } readonly mapElementId = 'timezone-map'; @@ -70,7 +72,10 @@ export class TimeZonePickerComponent { setTimeZone(): void { const timeZone = this.timeZoneSelect.val(); - this.actions.setTimeZone(timeZone); + + // TODO replace with setTimeZone() method call from settings service as soon as it's implemented + this.appSettings.setParameter('timeZone', timeZone); + this.setTimeZonePickerDisplay(false); } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.html index 910e55f..71637bb 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.html @@ -19,10 +19,10 @@ <form [formGroup]="filtersForm" class="filters"> <filter-dropdown *ngIf="isClustersFilterDisplayed" formControlName="clusters" [options]="filters.clusters.options" [isMultipleChoice]="true" label="{{filters.clusters.label | translate}}" [isRightAlign]="true" - buttonClass="inherited-color"></filter-dropdown> + buttonClass="btn-link inherited-color"></filter-dropdown> </form> - <menu-button *ngFor="let item of items" label="{{item.label | translate}}" [action]="item.action" - [iconClass]="item.iconClass" [labelClass]="item.labelClass" [subItems]="item.subItems" - [hideCaret]="item.hideCaret" [badge]="item.badge" [isRightAlign]="item.isRightAlign"> + <menu-button *ngFor="let item of items" label="{{item.label | translate}}" [iconClass]="item.iconClass" + [labelClass]="item.labelClass" [subItems]="item.subItems" [hideCaret]="item.hideCaret" + [badge]="item.badge" [isRightAlign]="item.isRightAlign"> </menu-button> </div> diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.spec.ts index ce4fa1c..caeb197 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.spec.ts @@ -38,6 +38,8 @@ import {ComponentsService, components} from '@app/services/storage/components.se import {HostsService, hosts} from '@app/services/storage/hosts.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {HttpClientService} from '@app/services/http-client.service'; +import {AuthService} from '@app/services/auth.service'; +import {UtilsService} from '@app/services/utils.service'; import {TopMenuComponent} from './top-menu.component'; @@ -81,6 +83,8 @@ describe('TopMenuComponent', () => { provide: HttpClientService, useValue: httpClient }, + AuthService, + UtilsService, AuditLogsService, ServiceLogsService, AuditLogsFieldsService, diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts index 8d739ec..1d003ee 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts @@ -21,6 +21,7 @@ import {FormGroup} from '@angular/forms'; import {FilterCondition, TimeUnitListItem} from '@app/classes/filtering'; import {ListItem} from '@app/classes/list-item'; import {HomogeneousObject} from '@app/classes/object'; +import {AuthService} from '@app/services/auth.service'; import {LogsContainerService} from '@app/services/logs-container.service'; @Component({ @@ -30,7 +31,7 @@ import {LogsContainerService} from '@app/services/logs-container.service'; }) export class TopMenuComponent { - constructor(private logsContainer: LogsContainerService) { + constructor(private authService: AuthService, private logsContainer: LogsContainerService) { } get filtersForm(): FormGroup { @@ -41,7 +42,15 @@ export class TopMenuComponent { return this.logsContainer.filters; }; - //TODO implement loading of real data into subItems + openSettings = (): void => {}; + + /** + * Request a logout action from AuthService + */ + logout = (): void => { + this.authService.logout(); + }; + readonly items = [ { iconClass: 'fa fa-user grey', @@ -49,11 +58,17 @@ export class TopMenuComponent { isRightAlign: true, subItems: [ { - label: 'Options' + label: 'common.settings', + onSelect: this.openSettings, + iconClass: 'fa fa-cog' + }, + { + isDivider: true }, { label: 'authorization.logout', - action: 'logout' + onSelect: this.logout, + iconClass: 'fa fa-sign-out' } ] } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.spec.ts deleted file mode 100644 index 952c542..0000000 --- a/ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {TestBed, inject} from '@angular/core/testing'; -import {TranslationModules} from '@app/test-config.spec'; -import {StoreModule} from '@ngrx/store'; -import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service'; -import {AppStateService, appState} from '@app/services/storage/app-state.service'; -import {ClustersService, clusters} from '@app/services/storage/clusters.service'; -import {ComponentsService, components} from '@app/services/storage/components.service'; -import {HostsService, hosts} from '@app/services/storage/hosts.service'; -import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service'; -import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service'; -import {AuditLogsFieldsService, auditLogsFields} from '@app/services/storage/audit-logs-fields.service'; -import {AuditLogsGraphDataService, auditLogsGraphData} from '@app/services/storage/audit-logs-graph-data.service'; -import {ServiceLogsFieldsService, serviceLogsFields} from '@app/services/storage/service-logs-fields.service'; -import { - ServiceLogsHistogramDataService, serviceLogsHistogramData -} from '@app/services/storage/service-logs-histogram-data.service'; -import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service'; -import {TabsService, tabs} from '@app/services/storage/tabs.service'; -import {HttpClientService} from '@app/services/http-client.service'; -import {LogsContainerService} from '@app/services/logs-container.service'; -import {AuthService} from '@app/services/auth.service'; - -import {ComponentActionsService} from './component-actions.service'; - -describe('ComponentActionsService', () => { - const httpClient = { - get: () => { - return { - subscribe: () => { - } - }; - } - }; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - StoreModule.provideStore({ - appSettings, - appState, - clusters, - components, - hosts, - auditLogs, - serviceLogs, - auditLogsFields, - auditLogsGraphData, - serviceLogsFields, - serviceLogsHistogramData, - serviceLogsTruncated, - tabs - }), - ...TranslationModules - ], - providers: [ - ComponentActionsService, - AppSettingsService, - AppStateService, - ClustersService, - ComponentsService, - HostsService, - AuditLogsService, - ServiceLogsService, - AuditLogsFieldsService, - AuditLogsGraphDataService, - ServiceLogsFieldsService, - ServiceLogsHistogramDataService, - ServiceLogsTruncatedService, - TabsService, - { - provide: HttpClientService, - useValue: httpClient - }, - LogsContainerService, - AuthService - ] - }); - }); - - it('should create service', inject([ComponentActionsService], (service: ComponentActionsService) => { - expect(service).toBeTruthy(); - })); -}); diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.ts b/ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.ts deleted file mode 100644 index 36c4d8d..0000000 --- a/ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {Injectable} from '@angular/core'; -import {AppSettingsService} from '@app/services/storage/app-settings.service'; -import {TabsService} from '@app/services/storage/tabs.service'; -import {CollectionModelService} from '@app/classes/models/store'; -import {LogsContainerService} from '@app/services/logs-container.service'; -import {AuthService} from '@app/services/auth.service'; -import {ServiceLog} from '@app/classes/models/service-log'; -import {ListItem} from '@app/classes/list-item'; - -@Injectable() -export class ComponentActionsService { - - constructor( - private appSettings: AppSettingsService, private tabsStorage: TabsService, private authService: AuthService, - private logsContainer: LogsContainerService - ) { - } - - //TODO implement actions - - undo() { - } - - redo() { - } - - refresh(): void { - this.logsContainer.loadLogs(); - } - - openHistory() { - } - - copyLog(log: ServiceLog): void { - if (document.queryCommandSupported('copy')) { - const text = log.log_message, - node = document.createElement('textarea'); - node.value = text; - Object.assign(node.style, { - position: 'fixed', - top: '0', - left: '0', - width: '1px', - height: '1px', - border: 'none', - outline: 'none', - boxShadow: 'none', - backgroundColor: 'transparent', - padding: '0' - }); - document.body.appendChild(node); - node.select(); - if (document.queryCommandEnabled('copy')) { - document.execCommand('copy'); - } else { - // TODO open failed alert - } - // TODO success alert - document.body.removeChild(node); - } else { - // TODO failed alert - } - } - - openLog(log: ServiceLog): void { - const tab = { - id: log.id, - isCloseable: true, - label: `${log.host} >> ${log.type}`, - appState: { - activeLogsType: 'serviceLogs', - isServiceLogsFileView: true, - activeLog: { - id: log.id, - host_name: log.host, - component_name: log.type - }, - activeFilters: Object.assign(this.logsContainer.getFiltersData('serviceLogs'), { - components: this.logsContainer.filters.components.options.find((option: ListItem): boolean => { - return option.value === log.type; - }), - hosts: this.logsContainer.filters.hosts.options.find((option: ListItem): boolean => { - return option.value === log.host; - }) - }) - } - }; - this.tabsStorage.addInstance(tab); - this.logsContainer.switchTab(tab); - } - - openContext(log: ServiceLog): void { - this.logsContainer.loadLogContext(log.id, log.host, log.type); - } - - startCapture(): void { - this.logsContainer.startCaptureTimer(); - } - - stopCapture(): void { - this.logsContainer.stopCaptureTimer(); - } - - setTimeZone(timeZone: string): void { - this.appSettings.setParameter('timeZone', timeZone); - } - - updateSelectedColumns(columnNames: string[], model: CollectionModelService): void { - model.mapCollection(item => Object.assign({}, item, { - isDisplayed: columnNames.indexOf(item.name) > -1 - })); - } - - proceedWithExclude = (item: string): void => this.logsContainer.queryParameterNameChange.next({ - item: { - value: item - }, - isExclude: true - }); - - /** - * Request a login action from the AuthService - * @param {string} username - * @param {string} password - */ - login(username: string, password: string): void { - this.authService.login(username, password); - } - - /** - * Request a logout action from AuthService - */ - logout(): void { - this.authService.logout(); - } - -} diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts index a8ab5e8..b87fa8c 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts @@ -36,6 +36,7 @@ import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/s import {TabsService, tabs} from '@app/services/storage/tabs.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {HttpClientService} from '@app/services/http-client.service'; +import {UtilsService} from '@app/services/utils.service'; import {ComponentGeneratorService} from './component-generator.service'; @@ -75,6 +76,7 @@ describe('ComponentGeneratorService', () => { provide: HttpClientService, useValue: httpClient }, + UtilsService, HostsService, AuditLogsService, ServiceLogsService, diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.ts b/ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.ts index 43755c0..1f82367 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.ts @@ -21,6 +21,7 @@ import {HostsService} from '@app/services/storage/hosts.service'; import {ComponentsService} from '@app/services/storage/components.service'; import {LogsContainerService} from '@app/services/logs-container.service'; import {NodeBarComponent} from '@app/components/node-bar/node-bar.component'; +import {HistoryItemControlsComponent} from '@app/components/history-item-controls/history-item-controls.component'; @Injectable() export class ComponentGeneratorService { @@ -75,4 +76,9 @@ export class ComponentGeneratorService { }); } + getHistoryItemIcons(historyItem, container: ViewContainerRef): void { + // TODO implement View details and Save filter actions + this.createComponent(HistoryItemControlsComponent, container); + } + } diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/services/history-manager.service.spec.ts similarity index 66% copy from ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts copy to ambari-logsearch/ambari-logsearch-web/src/app/services/history-manager.service.spec.ts index a8ab5e8..68a0e99 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/services/component-generator.service.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/services/history-manager.service.spec.ts @@ -19,7 +19,6 @@ import {TestBed, inject} from '@angular/core/testing'; import {TranslationModules} from '@app/test-config.spec'; import {StoreModule} from '@ngrx/store'; -import {HostsService, hosts} from '@app/services/storage/hosts.service'; import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service'; import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service'; import {AuditLogsFieldsService, auditLogsFields} from '@app/services/storage/audit-logs-fields.service'; @@ -32,14 +31,16 @@ import {AppSettingsService, appSettings} from '@app/services/storage/app-setting import {AppStateService, appState} from '@app/services/storage/app-state.service'; import {ClustersService, clusters} from '@app/services/storage/clusters.service'; import {ComponentsService, components} from '@app/services/storage/components.service'; +import {HostsService, hosts} from '@app/services/storage/hosts.service'; import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service'; import {TabsService, tabs} from '@app/services/storage/tabs.service'; -import {LogsContainerService} from '@app/services/logs-container.service'; import {HttpClientService} from '@app/services/http-client.service'; +import {LogsContainerService} from '@app/services/logs-container.service'; +import {UtilsService} from '@app/services/utils.service'; -import {ComponentGeneratorService} from './component-generator.service'; +import {HistoryManagerService} from './history-manager.service'; -describe('ComponentGeneratorService', () => { +describe('HistoryService', () => { beforeEach(() => { const httpClient = { get: () => { @@ -51,8 +52,8 @@ describe('ComponentGeneratorService', () => { }; TestBed.configureTestingModule({ imports: [ + ...TranslationModules, StoreModule.provideStore({ - hosts, auditLogs, serviceLogs, auditLogsFields, @@ -63,19 +64,19 @@ describe('ComponentGeneratorService', () => { appState, clusters, components, + hosts, serviceLogsTruncated, tabs - }), - ...TranslationModules + }) ], providers: [ - ComponentGeneratorService, - LogsContainerService, + HistoryManagerService, { provide: HttpClientService, useValue: httpClient }, - HostsService, + LogsContainerService, + UtilsService, AuditLogsService, ServiceLogsService, AuditLogsFieldsService, @@ -86,13 +87,84 @@ describe('ComponentGeneratorService', () => { AppStateService, ClustersService, ComponentsService, + HostsService, ServiceLogsTruncatedService, TabsService ] }); }); - it('should create service', inject([ComponentGeneratorService], (service: ComponentGeneratorService) => { + it('should be created', inject([HistoryManagerService], (service: HistoryManagerService) => { expect(service).toBeTruthy(); })); + + describe('#isHistoryUnchanged()', () => { + const cases = [ + { + valueA: { + p0: 'v0', + p1: ['v1'], + p2: { + k2: 'v2' + } + }, + valueB: { + p0: 'v0', + p1: ['v1'], + p2: { + k2: 'v2' + } + }, + result: true, + title: 'no difference' + }, + { + valueA: { + p0: 'v0', + p1: ['v1'], + p2: { + k2: 'v2' + }, + page: 0 + }, + valueB: { + p0: 'v0', + p1: ['v1'], + p2: { + k2: 'v2' + }, + page: 1 + }, + result: true, + title: 'difference in ignored parameters' + }, + { + valueA: { + p0: 'v0', + p1: ['v1'], + p2: { + k2: 'v2' + }, + page: 0 + }, + valueB: { + p0: 'v0', + p1: ['v3'], + p2: { + k2: 'v4' + }, + page: 1 + }, + result: false, + title: 'difference in non-ignored parameters' + } + ]; + + cases.forEach(test => { + it(test.title, inject([HistoryManagerService], (service: HistoryManagerService) => { + const isHistoryUnchanged: (valueA: object, valueB: object) => boolean = service['isHistoryUnchanged']; + expect(isHistoryUnchanged(test.valueA, test.valueB)).toEqual(test.result); + })); + }); + }); }); diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/services/history-manager.service.ts b/ambari-logsearch/ambari-logsearch-web/src/app/services/history-manager.service.ts new file mode 100644 index 0000000..39cadc6 --- /dev/null +++ b/ambari-logsearch/ambari-logsearch-web/src/app/services/history-manager.service.ts @@ -0,0 +1,330 @@ +/** + * 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 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/takeUntil'; +import {TranslateService} from '@ngx-translate/core'; +import {SearchBoxParameter, TimeUnitListItem} from '@app/classes/filtering'; +import {ListItem} from '@app/classes/list-item'; +import {HomogeneousObject} from '@app/classes/object'; +import {History} from '@app/classes/models/app-state'; +import {Tab} from '@app/classes/models/tab'; +import {LogsContainerService} from '@app/services/logs-container.service'; +import {UtilsService} from '@app/services/utils.service'; +import {AppStateService} from '@app/services/storage/app-state.service'; +import {TabsService} from '@app/services/storage/tabs.service'; + +@Injectable() +export class HistoryManagerService { + + constructor( + private translate: TranslateService, private logsContainer: LogsContainerService, private utils: UtilsService, + private appState: AppStateService, private tabs: TabsService + ) { + // set labels for history list items + const filters = logsContainer.filters, + controlNames = Object.keys(filters).filter((name: string): boolean => { + const key = filters[name].label; + return key && this.ignoredParameters.indexOf(name) === -1; + }), + filterLabelKeys = controlNames.map((name: string): string => filters[name].label), + timeRangeLabels = filters.timeRange.options.reduce(( + currentArray: string[], group: TimeUnitListItem[] + ): string[] => { + return [...currentArray, ...group.map((option: TimeUnitListItem): string => option.label)]; + }, [logsContainer.customTimeRangeKey]); + + translate.get([ + 'filter.include', 'filter.exclude', ...filterLabelKeys, ...timeRangeLabels + ]).subscribe((translates: object): void => { + this.controlNameLabels = controlNames.reduce(( + currentObject: HomogeneousObject<string>, name: string + ): HomogeneousObject<string> => { + return Object.assign({}, currentObject, { + [name]: translates[filters[name].label] + }) + }, { + include: translates['filter.include'], + exclude: translates['filter.exclude'] + }); + this.timeRangeLabels = timeRangeLabels.reduce(( + currentObject: HomogeneousObject<string>, key: string + ): HomogeneousObject<string> => { + return Object.assign({}, currentObject, { + [key]: translates[key] + }); + }, {}); + }); + + // set default history state for each tab + tabs.mapCollection((tab: Tab): Tab => { + let currentAppState = tab.appState || {}; + const appState = Object.assign({}, currentAppState, { + history: { + items: [], + currentId: -1 + } + }); + return Object.assign({}, tab, { + appState + }); + }); + + // set current history items after switching tabs + appState.getParameter('history').subscribe((history: History): void => { + const filtersForm = logsContainer.filtersForm; + let defaultState; + if (history.items.length === 0) { + defaultState = filtersForm.value + } + this.activeHistory = history.items.slice(); + this.currentHistoryItemId = history.currentId; + + // handle filtering values changes + filtersForm.valueChanges + .distinctUntilChanged(this.isHistoryUnchanged) + .takeUntil(this.logsContainer.filtersFormChange) + .subscribe((value): void => { + if (this.hasNoPendingUndoOrRedo) { + const currentHistory = this.activeHistory, + previousValue = this.activeHistory.length ? this.activeHistory[0].value.currentValue : defaultState, + isUndoOrRedo = value.isUndoOrRedo; + const previousChangeId = this.currentHistoryItemId; + if (isUndoOrRedo) { + this.hasNoPendingUndoOrRedo = false; + filtersForm.patchValue({ + isUndoOrRedo: false + }); + this.hasNoPendingUndoOrRedo = true; + } else { + this.currentHistoryItemId = currentHistory.length; + } + this.activeHistory = [ + { + value: { + currentValue: Object.assign({}, value), + previousValue: Object.assign({}, previousValue), + changeId: this.currentHistoryItemId, + previousChangeId, + isUndoOrRedo + }, + label: this.getHistoryItemLabel(previousValue, value) + }, + ...currentHistory + ].slice(0, this.maxHistoryItemsCount); + + // update history for active tab + this.tabs.mapCollection((tab: Tab): Tab => { + const currentAppState = tab.appState || {}, + appState = Object.assign({}, currentAppState, tab.isActive ? { + history: { + items: this.activeHistory.slice(), + currentId: this.currentHistoryItemId + } + } : null); + return Object.assign({}, tab, { + appState + }); + }); + } + }); + }); + } + + /** + * List of filter parameters which shouldn't affect changes history (related to pagination and sorting) + * @type {string[]} + */ + private readonly ignoredParameters: string[] = ['page', 'pageSize', 'auditLogsSorting', 'serviceLogsSorting']; + + /** + * 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; + + /** + * 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; + + /** + * Contains i18n labels for filtering form control names + */ + private controlNameLabels; + + /** + * Contains i18n labels for time range options + */ + private timeRangeLabels; + + /** + * History items for current tab + * @type {Array} + */ + activeHistory: ListItem[] = []; + + /** + * List of filtering form control names for active tab + * @returns {Array} + */ + private get filterParameters(): string[] { + return this.logsContainer.logsTypeMap[this.logsContainer.activeLogsType].listFilters; + } + + /** + * List of changes that can be undone + * @returns {ListItem[]} + */ + get undoItems(): ListItem[] { + const allItems = this.activeHistory; + let startIndex = allItems.findIndex((item: ListItem): boolean => { + return item.value.changeId === this.currentHistoryItemId && !item.value.isUndoOrRedo; + }), + endIndex = allItems.slice(startIndex + 1).findIndex((item: ListItem): boolean => item.value.isUndoOrRedo); + if (startIndex > -1) { + if (endIndex === -1) { + endIndex = allItems.length; + return allItems.slice(startIndex, startIndex + endIndex + 1); + } + } else { + return []; + } + } + + /** + * List of changes that can be redone + * @returns {ListItem[]} + */ + get redoItems(): ListItem[] { + const allItems = this.activeHistory.slice().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); + if (startIndex === -1) { + startIndex = allItems.length; + } + if (endIndex === -1) { + endIndex = allItems.length; + } + return allItems.slice(startIndex, endIndex + startIndex + 1); + } + + /** + * Indicates whether there are no filtering form changes that should be tracked + * (all except the ones related to pagination and sorting) + * @param {object} valueA + * @param {object} valueB + * @returns {boolean} + */ + private isHistoryUnchanged = (valueA: object, valueB: object): boolean => { + const objectA = Object.assign({}, valueA), + objectB = Object.assign({}, valueB); + this.ignoredParameters.forEach((controlName: string): void => { + delete objectA[controlName]; + delete objectB[controlName]; + }); + return this.utils.isEqual(objectA, objectB); + }; + + /** + * Get label for certain form control change + * @param {string} controlName + * @param {any} selection + * @returns {string} + */ + private getItemValueString(controlName: string, selection: any): string { + switch (controlName) { + case 'timeRange': + return `${this.controlNameLabels[controlName]}: ${this.timeRangeLabels[selection.label]}`; + case 'query': + const includes = selection.filter((item: SearchBoxParameter): boolean => { + return !item.isExclude; + }).map((item: SearchBoxParameter): string => `${item.name}: ${item.value}`).join(', '), + excludes = selection.filter((item: SearchBoxParameter): boolean => { + return item.isExclude; + }).map((item: SearchBoxParameter): string => `${item.name}: ${item.value}`).join(', '), + includesString = includes.length ? `${this.controlNameLabels.include}: ${includes}` : '', + excludesString = excludes.length ? `${this.controlNameLabels.exclude}: ${excludes}`: ''; + return `${includesString} ${excludesString}`; + default: + const values = selection.map((option: ListItem) => option.value).join(', '); + return `${this.controlNameLabels[controlName]}: ${values}`; + } + } + + /** + * Get label for history list item (i.e., difference with the previous one) + * @param {object} previousFormValue + * @param {object} currentFormValue + * @returns {string} + */ + private getHistoryItemLabel(previousFormValue: object, currentFormValue: object): string { + return this.filterParameters.reduce((currentResult: string, currentName: string): string => { + const currentValue = currentFormValue[currentName]; + if (this.ignoredParameters.indexOf(currentName) > -1 + || this.utils.isEqual(previousFormValue[currentName], currentValue)) { + return currentResult; + } else { + const currentLabel = this.getItemValueString(currentName, currentValue); + return `${currentResult} ${currentLabel}`; + } + }, ''); + } + + /** + * Handle undo or redo action correctly + * @param {object} value + */ + private handleUndoOrRedo(value: object): void { + const filtersForm = this.logsContainer.filtersForm; + this.hasNoPendingUndoOrRedo = false; + this.filterParameters.forEach((controlName: string): void => { + if (this.ignoredParameters.indexOf(controlName) === -1) { + filtersForm.controls[controlName].setValue(value[controlName]); + } + }); + this.hasNoPendingUndoOrRedo = true; + filtersForm.controls.isUndoOrRedo.setValue(true); + } + + undo(item: ListItem): void { + this.hasNoPendingUndoOrRedo = false; + this.currentHistoryItemId = item.value.previousChangeId; + this.handleUndoOrRedo(item.value.previousValue); + } + + redo(item: ListItem): void { + this.hasNoPendingUndoOrRedo = false; + this.currentHistoryItemId = item.value.changeId; + this.handleUndoOrRedo(item.value.currentValue); + } + +} diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts index 5961309..a8e7c3f 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts @@ -35,6 +35,7 @@ import {HostsService, hosts} from '@app/services/storage/hosts.service'; import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service'; import {TabsService, tabs} from '@app/services/storage/tabs.service'; import {HttpClientService} from '@app/services/http-client.service'; +import {UtilsService} from '@app/services/utils.service'; import {ListItem} from '@app/classes/list-item'; import {NodeItem} from '@app/classes/models/node-item'; @@ -87,7 +88,8 @@ describe('LogsContainerService', () => { { provide: HttpClientService, useValue: httpClient - } + }, + UtilsService ] }); }); diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts b/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts index baa3972..6a2108b 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts @@ -23,11 +23,13 @@ import {Subject} from 'rxjs/Subject'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/observable/timer'; import 'rxjs/add/observable/combineLatest'; +import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/first'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/takeUntil'; import * as moment from 'moment-timezone'; import {HttpClientService} from '@app/services/http-client.service'; +import {UtilsService} from '@app/services/utils.service'; import {AuditLogsService} from '@app/services/storage/audit-logs.service'; import {AuditLogsFieldsService} from '@app/services/storage/audit-logs-fields.service'; import {AuditLogsGraphDataService} from '@app/services/storage/audit-logs-graph-data.service'; @@ -62,13 +64,13 @@ import {CommonEntry} from '@app/classes/models/common-entry'; export class LogsContainerService { constructor( - private httpClient: HttpClientService, private tabsStorage: TabsService, private componentsStorage: ComponentsService, - private hostsStorage: HostsService, private appState: AppStateService, private auditLogsStorage: AuditLogsService, + private httpClient: HttpClientService, private utils: UtilsService, + private tabsStorage: TabsService, private componentsStorage: ComponentsService, private hostsStorage: HostsService, + private appState: AppStateService, private auditLogsStorage: AuditLogsService, private auditLogsGraphStorage: AuditLogsGraphDataService, private auditLogsFieldsStorage: AuditLogsFieldsService, private serviceLogsStorage: ServiceLogsService, private serviceLogsFieldsStorage: ServiceLogsFieldsService, private serviceLogsHistogramStorage: ServiceLogsHistogramDataService, private clustersStorage: ClustersService, private serviceLogsTruncatedStorage: ServiceLogsTruncatedService, private appSettings: AppSettingsService - ) { const formItems = Object.keys(this.filters).reduce((currentObject: any, key: string): HomogeneousObject<FormControl> => { let formControl = new FormControl(), @@ -104,17 +106,20 @@ export class LogsContainerService { }); } this.loadLogs(); - this.filtersForm.valueChanges.takeUntil(this.filtersFormChange).subscribe((value: object): void => { - this.tabsStorage.mapCollection((tab: Tab): Tab => { - const currentAppState = tab.appState || {}, - appState = Object.assign({}, currentAppState, tab.isActive ? { - activeFilters: value - } : null); - return Object.assign({}, tab, { - appState + this.filtersForm.valueChanges + .distinctUntilChanged(this.isFormUnchanged) + .takeUntil(this.filtersFormChange) + .subscribe((value): void => { + this.tabsStorage.mapCollection((tab: Tab): Tab => { + const currentAppState = tab.appState || {}, + appState = Object.assign({}, currentAppState, tab.isActive ? { + activeFilters: value + } : null); + return Object.assign({}, tab, { + appState + }); }); - }); - this.loadLogs(); + this.loadLogs(); }); }); } @@ -129,6 +134,7 @@ export class LogsContainerService { fieldName: 'cluster' }, timeRange: { + label: 'filter.duration', options: [ [ { @@ -473,7 +479,12 @@ export class LogsContainerService { page: { defaultSelection: 0 }, - query: {} + query: { + defaultSelection: [] + }, + isUndoOrRedo: { + defaultSelection: false + } }; readonly colors = { @@ -508,6 +519,8 @@ export class LogsContainerService { query: ['includeQuery', 'excludeQuery'] }; + readonly customTimeRangeKey: string = 'filter.timeRange.custom'; + readonly topResourcesCount: string = '10'; readonly topUsersCount: string = '6'; @@ -569,7 +582,7 @@ export class LogsContainerService { activeLogsType: LogsType; - private filtersFormChange: Subject<void> = new Subject(); + filtersFormChange: Subject<void> = new Subject(); private columnsMapper<FieldT extends LogField>(fields: FieldT[]): ListItem[] { return fields.filter((field: FieldT): boolean => field.isAvailable).map((field: FieldT): ListItem => { @@ -649,6 +662,16 @@ export class LogsContainerService { topResourcesGraphData: HomogeneousObject<HomogeneousObject<number>> = {}; + private isFormUnchanged = (valueA: object, valueB: object): boolean => { + const trackedControlNames = this.logsTypeMap[this.activeLogsType].listFilters; + for (let name of trackedControlNames) { + if (!this.utils.isEqual(valueA[name], valueB[name])) { + return false; + } + } + return true; + }; + loadLogs = (logsType: LogsType = this.activeLogsType): void => { this.httpClient.get(logsType, this.getParams('listFilters')).subscribe((response: Response): void => { const jsonResponse = response.json(), @@ -992,7 +1015,7 @@ export class LogsContainerService { setCustomTimeRange(startTime: number, endTime: number): void { this.filtersForm.controls.timeRange.setValue({ - label: 'filter.timeRange.custom', + label: this.customTimeRangeKey, value: { type: 'CUSTOM', start: moment(startTime), @@ -1016,4 +1039,37 @@ export class LogsContainerService { && Boolean(this.filtersForm.controls[key]); } + updateSelectedColumns(columnNames: string[], logsType: string): void { + this.logsTypeMap[logsType].fieldsModel.mapCollection(item => Object.assign({}, item, { + isDisplayed: columnNames.indexOf(item.name) > -1 + })); + } + + openServiceLog(log: ServiceLog): void { + const tab = { + id: log.id, + isCloseable: true, + label: `${log.host} >> ${log.type}`, + appState: { + activeLogsType: 'serviceLogs', + isServiceLogsFileView: true, + activeLog: { + id: log.id, + host_name: log.host, + component_name: log.type + }, + activeFilters: Object.assign(this.getFiltersData('serviceLogs'), { + components: this.filters.components.options.find((option: ListItem): boolean => { + return option.value === log.type; + }), + hosts: this.filters.hosts.options.find((option: ListItem): boolean => { + return option.value === log.host; + }) + }) + } + }; + this.tabsStorage.addInstance(tab); + this.switchTab(tab); + } + } diff --git a/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json b/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json index 2b34b4d..1d8f6c4 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json +++ b/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json @@ -4,6 +4,9 @@ "common.auditLogs": "Audit Logs", "common.summary": "Summary", "common.logs": "Logs", + "common.name": "Name", + "common.value": "Value", + "common.settings": "Settings", "modal.submit": "OK", "modal.cancel": "Cancel", @@ -26,8 +29,10 @@ "filter.clusters": "Clusters", "filter.components": "Components", "filter.levels": "Levels", + "filter.include": "Include", "filter.exclude": "Exclude", "filter.hosts": "Hosts", + "filter.duration": "Duration", "filter.capture": "Capture", "filter.capture.triggeringRefresh": "Triggering auto-refresh in {{remainingSeconds}} sec", -- To stop receiving notification emails like this one, please contact ababiic...@apache.org.